diff --git a/packages/react-is/README.md b/packages/react-is/README.md new file mode 100644 index 0000000000000..f744d7d1de708 --- /dev/null +++ b/packages/react-is/README.md @@ -0,0 +1,83 @@ +# `react-is` + +This package allows you to test arbitrary values and see if they're a particular React type, e.g. React Elements. + +## Installation + +```sh +# Yarn +yarn add react-is + +# NPM +npm install react-is --save +``` + +## Usage + +### AsyncMode + +```js +import React from "react"; +import * as ReactIs from 'react-is'; + +ReactIs.isAsyncMode(); // true +ReactIs.typeOf() === ReactIs.AsyncMode; // true +``` + +### Context + +```js +import React from "react"; +import * as ReactIs from 'react-is'; + +const ThemeContext = React.createContext("blue"); + +ReactIs.isContextConsumer(); // true +ReactIs.isContextProvider(); // true +ReactIs.typeOf() === ReactIs.ContextProvider; // true +ReactIs.typeOf() === ReactIs.ContextConsumer; // true +``` + +### Element + +```js +import React from "react"; +import * as ReactIs from 'react-is'; + +ReactIs.isElement(
); // true +ReactIs.typeOf(
) === ReactIs.Element; // true +``` + +### Fragment + +```js +import React from "react"; +import * as ReactIs from 'react-is'; + +ReactIs.isFragment(<>); // true +ReactIs.typeOf(<>) === ReactIs.Fragment; // true +``` + +### Portal + +```js +import React from "react"; +import ReactDOM from "react-dom"; +import * as ReactIs from 'react-is'; + +const div = document.createElement("div"); +const portal = ReactDOM.createPortal(
, div); + +ReactIs.isPortal(portal); // true +ReactIs.typeOf(portal) === ReactIs.Portal; // true +``` + +### StrictMode + +```js +import React from "react"; +import * as ReactIs from 'react-is'; + +ReactIs.isStrictMode(); // true +ReactIs.typeOf() === ReactIs.StrictMode; // true +``` diff --git a/packages/react-is/index.js b/packages/react-is/index.js new file mode 100644 index 0000000000000..f8c9ffdcd287c --- /dev/null +++ b/packages/react-is/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +export * from './src/ReactIs'; diff --git a/packages/react-is/npm/index.js b/packages/react-is/npm/index.js new file mode 100644 index 0000000000000..3ae098d078776 --- /dev/null +++ b/packages/react-is/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-is.production.min.js'); +} else { + module.exports = require('./cjs/react-is.development.js'); +} diff --git a/packages/react-is/package.json b/packages/react-is/package.json new file mode 100644 index 0000000000000..8fd899a90e05b --- /dev/null +++ b/packages/react-is/package.json @@ -0,0 +1,23 @@ +{ + "name": "react-is", + "version": "16.3.0-alpha.0", + "description": "Brand checking of React Elements.", + "main": "index.js", + "repository": "facebook/react", + "keywords": ["react"], + "license": "MIT", + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://reactjs.org/", + "peerDependencies": { + "react": "^16.0.0 || 16.3.0-alpha.0" + }, + "files": [ + "LICENSE", + "README.md", + "index.js", + "cjs/", + "umd/" + ] +} \ No newline at end of file diff --git a/packages/react-is/src/ReactIs.js b/packages/react-is/src/ReactIs.js new file mode 100644 index 0000000000000..d26bc82f5e8c1 --- /dev/null +++ b/packages/react-is/src/ReactIs.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +import { + REACT_ASYNC_MODE_TYPE, + REACT_CONTEXT_TYPE, + REACT_ELEMENT_TYPE, + REACT_FRAGMENT_TYPE, + REACT_PORTAL_TYPE, + REACT_PROVIDER_TYPE, + REACT_STRICT_MODE_TYPE, +} from 'shared/ReactSymbols'; + +export function typeOf(object: any) { + if (typeof object === 'object' && object !== null) { + const $$typeof = object.$$typeof; + + switch ($$typeof) { + case REACT_ELEMENT_TYPE: + const type = object.type; + + switch (type) { + case REACT_ASYNC_MODE_TYPE: + case REACT_FRAGMENT_TYPE: + case REACT_STRICT_MODE_TYPE: + return type; + default: + const $$typeofType = type.$$typeof; + + switch ($$typeofType) { + case REACT_CONTEXT_TYPE: + case REACT_PROVIDER_TYPE: + return $$typeofType; + default: + return $$typeof; + } + } + case REACT_PORTAL_TYPE: + return $$typeof; + } + } + + return undefined; +} + +export const AsyncMode = REACT_ASYNC_MODE_TYPE; +export const ContextConsumer = REACT_CONTEXT_TYPE; +export const ContextProvider = REACT_PROVIDER_TYPE; +export const Element = REACT_ELEMENT_TYPE; +export const Fragment = REACT_FRAGMENT_TYPE; +export const Portal = REACT_PORTAL_TYPE; +export const StrictMode = REACT_STRICT_MODE_TYPE; + +export function isAsyncMode(object: any) { + return typeOf(object) === REACT_ASYNC_MODE_TYPE; +} +export function isContextConsumer(object: any) { + return typeOf(object) === REACT_CONTEXT_TYPE; +} +export function isContextProvider(object: any) { + return typeOf(object) === REACT_PROVIDER_TYPE; +} +export function isElement(object: any) { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_ELEMENT_TYPE + ); +} +export function isFragment(object: any) { + return typeOf(object) === REACT_FRAGMENT_TYPE; +} +export function isPortal(object: any) { + return typeOf(object) === REACT_PORTAL_TYPE; +} +export function isStrictMode(object: any) { + return typeOf(object) === REACT_STRICT_MODE_TYPE; +} diff --git a/packages/react-is/src/__tests__/ReactIs-test.js b/packages/react-is/src/__tests__/ReactIs-test.js new file mode 100644 index 0000000000000..6200388fd525d --- /dev/null +++ b/packages/react-is/src/__tests__/ReactIs-test.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactIs; + +describe('ReactIs', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactDOM = require('react-dom'); + ReactIs = require('react-is'); + }); + + it('should return undefined for unknown/invalid types', () => { + expect(ReactIs.typeOf('abc')).toBe(undefined); + expect(ReactIs.typeOf(true)).toBe(undefined); + expect(ReactIs.typeOf(123)).toBe(undefined); + expect(ReactIs.typeOf({})).toBe(undefined); + expect(ReactIs.typeOf(null)).toBe(undefined); + expect(ReactIs.typeOf(undefined)).toBe(undefined); + }); + + it('should identify async mode', () => { + expect(ReactIs.typeOf()).toBe( + ReactIs.AsyncMode, + ); + expect(ReactIs.isAsyncMode()).toBe(true); + expect(ReactIs.isAsyncMode({type: ReactIs.AsyncMode})).toBe(false); + expect(ReactIs.isAsyncMode()).toBe(false); + expect(ReactIs.isAsyncMode(
)).toBe(false); + }); + + it('should identify context consumers', () => { + const Context = React.createContext(false); + expect(ReactIs.typeOf()).toBe(ReactIs.ContextConsumer); + expect(ReactIs.isContextConsumer()).toBe(true); + expect(ReactIs.isContextConsumer()).toBe(false); + expect(ReactIs.isContextConsumer(
)).toBe(false); + }); + + it('should identify context providers', () => { + const Context = React.createContext(false); + expect(ReactIs.typeOf()).toBe(ReactIs.ContextProvider); + expect(ReactIs.isContextProvider()).toBe(true); + expect(ReactIs.isContextProvider()).toBe(false); + expect(ReactIs.isContextProvider(
)).toBe(false); + }); + + it('should identify elements', () => { + expect(ReactIs.typeOf(
)).toBe(ReactIs.Element); + expect(ReactIs.isElement(
)).toBe(true); + expect(ReactIs.isElement('div')).toBe(false); + expect(ReactIs.isElement(true)).toBe(false); + expect(ReactIs.isElement(123)).toBe(false); + expect(ReactIs.isElement(null)).toBe(false); + expect(ReactIs.isElement(undefined)).toBe(false); + expect(ReactIs.isElement({})).toBe(false); + + // It should also identify more specific types as elements + const Context = React.createContext(false); + expect(ReactIs.isElement()).toBe(true); + expect(ReactIs.isElement()).toBe(true); + expect(ReactIs.isElement()).toBe(true); + expect(ReactIs.isElement()).toBe(true); + expect(ReactIs.isElement()).toBe(true); + }); + + it('should identify fragments', () => { + expect(ReactIs.typeOf()).toBe(ReactIs.Fragment); + expect(ReactIs.isFragment()).toBe(true); + expect(ReactIs.isFragment({type: ReactIs.Fragment})).toBe(false); + expect(ReactIs.isFragment('React.Fragment')).toBe(false); + expect(ReactIs.isFragment(
)).toBe(false); + expect(ReactIs.isFragment([])).toBe(false); + }); + + it('should identify portals', () => { + const div = document.createElement('div'); + const portal = ReactDOM.createPortal(
, div); + expect(ReactIs.typeOf(portal)).toBe(ReactIs.Portal); + expect(ReactIs.isPortal(portal)).toBe(true); + expect(ReactIs.isPortal(div)).toBe(false); + }); + + it('should identify strict mode', () => { + expect(ReactIs.typeOf()).toBe(ReactIs.StrictMode); + expect(ReactIs.isStrictMode()).toBe(true); + expect(ReactIs.isStrictMode({type: ReactIs.StrictMode})).toBe(false); + expect(ReactIs.isStrictMode()).toBe(false); + expect(ReactIs.isStrictMode(
)).toBe(false); + }); +}); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index ff57b766998e0..b29c382d51d15 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -234,6 +234,16 @@ const bundles = [ global: 'ReactCallReturn', externals: [], }, + + /******* React Is *******/ + { + label: 'react-is', + bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], + moduleType: ISOMORPHIC, + entry: 'react-is', + global: 'ReactIs', + externals: [], + }, ]; // Based on deep-freeze by substack (public domain) diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 78fc29e1035e8..57d2013e37f29 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -398,6 +398,34 @@ "packageName": "react-reconciler", "size": 41327, "gzip": 13133 + }, + { + "filename": "react-is.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-is", + "size": 3358, + "gzip": 1015 + }, + { + "filename": "react-is.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-is", + "size": 1433, + "gzip": 607 + }, + { + "filename": "react-is.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-is", + "size": 3547, + "gzip": 1071 + }, + { + "filename": "react-is.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-is", + "size": 1515, + "gzip": 670 } ] } \ No newline at end of file diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js index bfc5396ce52ec..7ee1e112f2b09 100644 --- a/scripts/rollup/validate/eslintrc.umd.js +++ b/scripts/rollup/validate/eslintrc.umd.js @@ -17,6 +17,7 @@ module.exports = { // UMD wrapper code // TODO: this is too permissive. // Ideally we should only allow these *inside* the UMD wrapper. + exports: true, module: true, define: true, require: true,