From 16e120438c5332d7590bc74b2cbd882f19a0e3b1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 30 Nov 2018 11:52:34 +0000 Subject: [PATCH] [Fire] Add initial build infrastructure (#14359) --- fixtures/unstable-fire/index.html | 14 + packages/react-dom/package.json | 1 + packages/react-dom/src/fire/ReactFire.js | 855 ++++++++++++++++++ .../react-dom/src/fire/ReactFireHostConfig.js | 14 + packages/react-dom/unstable-fire.js | 16 + packages/react-reconciler/inline.fire.js | 11 + .../src/forks/ReactFiberHostConfig.fire.js | 10 + scripts/jest/setupFire.js | 2 + scripts/rollup/bundles.js | 21 +- scripts/shared/inlinedHostConfigs.js | 5 + 10 files changed, 948 insertions(+), 1 deletion(-) create mode 100644 fixtures/unstable-fire/index.html create mode 100644 packages/react-dom/src/fire/ReactFire.js create mode 100644 packages/react-dom/src/fire/ReactFireHostConfig.js create mode 100644 packages/react-dom/unstable-fire.js create mode 100644 packages/react-reconciler/inline.fire.js create mode 100644 packages/react-reconciler/src/forks/ReactFiberHostConfig.fire.js diff --git a/fixtures/unstable-fire/index.html b/fixtures/unstable-fire/index.html new file mode 100644 index 0000000000..3e1cd74fe3 --- /dev/null +++ b/fixtures/unstable-fire/index.html @@ -0,0 +1,14 @@ + + + + + +
+ + + \ No newline at end of file diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index d92c95f7d3..6c4e6255ac 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -31,6 +31,7 @@ "server.browser.js", "server.node.js", "test-utils.js", + "unstable-fire.js", "unstable-native-dependencies.js", "cjs/", "umd/" diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js new file mode 100644 index 0000000000..b1a7855ab2 --- /dev/null +++ b/packages/react-dom/src/fire/ReactFire.js @@ -0,0 +1,855 @@ +/** + * 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 + */ + +// This file is copy paste from ReactDOM with adjusted paths +// and a different host config import (react-reconciler/inline.fire). +// TODO: real implementation. +// console.log('Hello from Fire entry point.'); + +import type {ReactNodeList} from 'shared/ReactTypes'; +// TODO: This type is shared between the reconciler and ReactDOM, but will +// eventually be lifted out to the renderer. +import type { + FiberRoot, + Batch as FiberRootBatch, +} from 'react-reconciler/src/ReactFiberRoot'; +import type {Container} from '../client/ReactDOMHostConfig'; + +import '../shared/checkReact'; +import '../client/ReactDOMClientInjection'; + +import { + computeUniqueAsyncExpiration, + findHostInstanceWithNoPortals, + updateContainerAtExpirationTime, + flushRoot, + createContainer, + updateContainer, + batchedUpdates, + unbatchedUpdates, + interactiveUpdates, + flushInteractiveUpdates, + flushSync, + flushControlled, + injectIntoDevTools, + getPublicRootInstance, + findHostInstance, + findHostInstanceWithWarning, +} from 'react-reconciler/inline.fire'; +import {createPortal as createPortalImpl} from 'shared/ReactPortal'; +import {canUseDOM} from 'shared/ExecutionEnvironment'; +import {setBatchingImplementation} from 'events/ReactGenericBatching'; +import { + setRestoreImplementation, + enqueueStateRestore, + restoreStateIfNeeded, +} from 'events/ReactControlledComponent'; +import { + injection as EventPluginHubInjection, + runEventsInBatch, +} from 'events/EventPluginHub'; +import {eventNameDispatchConfigs} from 'events/EventPluginRegistry'; +import { + accumulateTwoPhaseDispatches, + accumulateDirectDispatches, +} from 'events/EventPropagators'; +import {has as hasInstance} from 'shared/ReactInstanceMap'; +import ReactVersion from 'shared/ReactVersion'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; +import lowPriorityWarning from 'shared/lowPriorityWarning'; +import warningWithoutStack from 'shared/warningWithoutStack'; +import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags'; + +import { + getInstanceFromNode, + getNodeFromInstance, + getFiberCurrentPropsFromNode, + getClosestInstanceFromNode, +} from '../client/ReactDOMComponentTree'; +import {restoreControlledState} from '../client/ReactDOMComponent'; +import {dispatchEvent} from '../events/ReactDOMEventListener'; +import { + ELEMENT_NODE, + COMMENT_NODE, + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, +} from '../shared/HTMLNodeType'; +import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + +let topLevelUpdateWarnings; +let warnOnInvalidCallback; +let didWarnAboutUnstableCreatePortal = false; + +if (__DEV__) { + if ( + typeof Map !== 'function' || + // $FlowIssue Flow incorrectly thinks Map has no prototype + Map.prototype == null || + typeof Map.prototype.forEach !== 'function' || + typeof Set !== 'function' || + // $FlowIssue Flow incorrectly thinks Set has no prototype + Set.prototype == null || + typeof Set.prototype.clear !== 'function' || + typeof Set.prototype.forEach !== 'function' + ) { + warningWithoutStack( + false, + 'React depends on Map and Set built-in types. Make sure that you load a ' + + 'polyfill in older browsers. https://fb.me/react-polyfills', + ); + } + + topLevelUpdateWarnings = (container: DOMContainer) => { + if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) { + const hostInstance = findHostInstanceWithNoPortals( + container._reactRootContainer._internalRoot.current, + ); + if (hostInstance) { + warningWithoutStack( + hostInstance.parentNode === container, + 'render(...): It looks like the React-rendered content of this ' + + 'container was removed without using React. This is not ' + + 'supported and will cause errors. Instead, call ' + + 'ReactDOM.unmountComponentAtNode to empty a container.', + ); + } + } + + const isRootRenderedBySomeReact = !!container._reactRootContainer; + const rootEl = getReactRootElementInContainer(container); + const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); + + warningWithoutStack( + !hasNonRootReactChild || isRootRenderedBySomeReact, + 'render(...): Replacing React-rendered children with a new root ' + + 'component. If you intended to update the children of this node, ' + + 'you should instead have the existing children update their state ' + + 'and render the new components instead of calling ReactDOM.render.', + ); + + warningWithoutStack( + container.nodeType !== ELEMENT_NODE || + !((container: any): Element).tagName || + ((container: any): Element).tagName.toUpperCase() !== 'BODY', + 'render(): Rendering components directly into document.body is ' + + 'discouraged, since its children are often manipulated by third-party ' + + 'scripts and browser extensions. This may lead to subtle ' + + 'reconciliation issues. Try rendering into a container element created ' + + 'for your app.', + ); + }; + + warnOnInvalidCallback = function(callback: mixed, callerName: string) { + warningWithoutStack( + callback === null || typeof callback === 'function', + '%s(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callerName, + callback, + ); + }; +} + +setRestoreImplementation(restoreControlledState); + +export type DOMContainer = + | (Element & { + _reactRootContainer: ?Root, + }) + | (Document & { + _reactRootContainer: ?Root, + }); + +type Batch = FiberRootBatch & { + render(children: ReactNodeList): Work, + then(onComplete: () => mixed): void, + commit(): void, + + // The ReactRoot constructor is hoisted but the prototype methods are not. If + // we move ReactRoot to be above ReactBatch, the inverse error occurs. + // $FlowFixMe Hoisting issue. + _root: Root, + _hasChildren: boolean, + _children: ReactNodeList, + + _callbacks: Array<() => mixed> | null, + _didComplete: boolean, +}; + +type Root = { + render(children: ReactNodeList, callback: ?() => mixed): Work, + unmount(callback: ?() => mixed): Work, + legacy_renderSubtreeIntoContainer( + parentComponent: ?React$Component, + children: ReactNodeList, + callback: ?() => mixed, + ): Work, + createBatch(): Batch, + + _internalRoot: FiberRoot, +}; + +function ReactBatch(root: ReactRoot) { + const expirationTime = computeUniqueAsyncExpiration(); + this._expirationTime = expirationTime; + this._root = root; + this._next = null; + this._callbacks = null; + this._didComplete = false; + this._hasChildren = false; + this._children = null; + this._defer = true; +} +ReactBatch.prototype.render = function(children: ReactNodeList) { + invariant( + this._defer, + 'batch.render: Cannot render a batch that already committed.', + ); + this._hasChildren = true; + this._children = children; + const internalRoot = this._root._internalRoot; + const expirationTime = this._expirationTime; + const work = new ReactWork(); + updateContainerAtExpirationTime( + children, + internalRoot, + null, + expirationTime, + work._onCommit, + ); + return work; +}; +ReactBatch.prototype.then = function(onComplete: () => mixed) { + if (this._didComplete) { + onComplete(); + return; + } + let callbacks = this._callbacks; + if (callbacks === null) { + callbacks = this._callbacks = []; + } + callbacks.push(onComplete); +}; +ReactBatch.prototype.commit = function() { + const internalRoot = this._root._internalRoot; + let firstBatch = internalRoot.firstBatch; + invariant( + this._defer && firstBatch !== null, + 'batch.commit: Cannot commit a batch multiple times.', + ); + + if (!this._hasChildren) { + // This batch is empty. Return. + this._next = null; + this._defer = false; + return; + } + + let expirationTime = this._expirationTime; + + // Ensure this is the first batch in the list. + if (firstBatch !== this) { + // This batch is not the earliest batch. We need to move it to the front. + // Update its expiration time to be the expiration time of the earliest + // batch, so that we can flush it without flushing the other batches. + if (this._hasChildren) { + expirationTime = this._expirationTime = firstBatch._expirationTime; + // Rendering this batch again ensures its children will be the final state + // when we flush (updates are processed in insertion order: last + // update wins). + // TODO: This forces a restart. Should we print a warning? + this.render(this._children); + } + + // Remove the batch from the list. + let previous = null; + let batch = firstBatch; + while (batch !== this) { + previous = batch; + batch = batch._next; + } + invariant( + previous !== null, + 'batch.commit: Cannot commit a batch multiple times.', + ); + previous._next = batch._next; + + // Add it to the front. + this._next = firstBatch; + firstBatch = internalRoot.firstBatch = this; + } + + // Synchronously flush all the work up to this batch's expiration time. + this._defer = false; + flushRoot(internalRoot, expirationTime); + + // Pop the batch from the list. + const next = this._next; + this._next = null; + firstBatch = internalRoot.firstBatch = next; + + // Append the next earliest batch's children to the update queue. + if (firstBatch !== null && firstBatch._hasChildren) { + firstBatch.render(firstBatch._children); + } +}; +ReactBatch.prototype._onComplete = function() { + if (this._didComplete) { + return; + } + this._didComplete = true; + const callbacks = this._callbacks; + if (callbacks === null) { + return; + } + // TODO: Error handling. + for (let i = 0; i < callbacks.length; i++) { + const callback = callbacks[i]; + callback(); + } +}; + +type Work = { + then(onCommit: () => mixed): void, + _onCommit: () => void, + _callbacks: Array<() => mixed> | null, + _didCommit: boolean, +}; + +function ReactWork() { + this._callbacks = null; + this._didCommit = false; + // TODO: Avoid need to bind by replacing callbacks in the update queue with + // list of Work objects. + this._onCommit = this._onCommit.bind(this); +} +ReactWork.prototype.then = function(onCommit: () => mixed): void { + if (this._didCommit) { + onCommit(); + return; + } + let callbacks = this._callbacks; + if (callbacks === null) { + callbacks = this._callbacks = []; + } + callbacks.push(onCommit); +}; +ReactWork.prototype._onCommit = function(): void { + if (this._didCommit) { + return; + } + this._didCommit = true; + const callbacks = this._callbacks; + if (callbacks === null) { + return; + } + // TODO: Error handling. + for (let i = 0; i < callbacks.length; i++) { + const callback = callbacks[i]; + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback(); + } +}; + +function ReactRoot( + container: Container, + isConcurrent: boolean, + hydrate: boolean, +) { + const root = createContainer(container, isConcurrent, hydrate); + this._internalRoot = root; +} +ReactRoot.prototype.render = function( + children: ReactNodeList, + callback: ?() => mixed, +): Work { + const root = this._internalRoot; + const work = new ReactWork(); + callback = callback === undefined ? null : callback; + if (__DEV__) { + warnOnInvalidCallback(callback, 'render'); + } + if (callback !== null) { + work.then(callback); + } + updateContainer(children, root, null, work._onCommit); + return work; +}; +ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work { + const root = this._internalRoot; + const work = new ReactWork(); + callback = callback === undefined ? null : callback; + if (__DEV__) { + warnOnInvalidCallback(callback, 'render'); + } + if (callback !== null) { + work.then(callback); + } + updateContainer(null, root, null, work._onCommit); + return work; +}; +ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function( + parentComponent: ?React$Component, + children: ReactNodeList, + callback: ?() => mixed, +): Work { + const root = this._internalRoot; + const work = new ReactWork(); + callback = callback === undefined ? null : callback; + if (__DEV__) { + warnOnInvalidCallback(callback, 'render'); + } + if (callback !== null) { + work.then(callback); + } + updateContainer(children, root, parentComponent, work._onCommit); + return work; +}; +ReactRoot.prototype.createBatch = function(): Batch { + const batch = new ReactBatch(this); + const expirationTime = batch._expirationTime; + + const internalRoot = this._internalRoot; + const firstBatch = internalRoot.firstBatch; + if (firstBatch === null) { + internalRoot.firstBatch = batch; + batch._next = null; + } else { + // Insert sorted by expiration time then insertion order + let insertAfter = null; + let insertBefore = firstBatch; + while ( + insertBefore !== null && + insertBefore._expirationTime >= expirationTime + ) { + insertAfter = insertBefore; + insertBefore = insertBefore._next; + } + batch._next = insertBefore; + if (insertAfter !== null) { + insertAfter._next = batch; + } + } + + return batch; +}; + +/** + * True if the supplied DOM node is a valid node element. + * + * @param {?DOMElement} node The candidate DOM node. + * @return {boolean} True if the DOM is a valid DOM node. + * @internal + */ +function isValidContainer(node) { + return !!( + node && + (node.nodeType === ELEMENT_NODE || + node.nodeType === DOCUMENT_NODE || + node.nodeType === DOCUMENT_FRAGMENT_NODE || + (node.nodeType === COMMENT_NODE && + node.nodeValue === ' react-mount-point-unstable ')) + ); +} + +function getReactRootElementInContainer(container: any) { + if (!container) { + return null; + } + + if (container.nodeType === DOCUMENT_NODE) { + return container.documentElement; + } else { + return container.firstChild; + } +} + +function shouldHydrateDueToLegacyHeuristic(container) { + const rootElement = getReactRootElementInContainer(container); + return !!( + rootElement && + rootElement.nodeType === ELEMENT_NODE && + rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) + ); +} + +setBatchingImplementation( + batchedUpdates, + interactiveUpdates, + flushInteractiveUpdates, +); + +let warnedAboutHydrateAPI = false; + +function legacyCreateRootFromDOMContainer( + container: DOMContainer, + forceHydrate: boolean, +): Root { + const shouldHydrate = + forceHydrate || shouldHydrateDueToLegacyHeuristic(container); + // First clear any existing content. + if (!shouldHydrate) { + let warned = false; + let rootSibling; + while ((rootSibling = container.lastChild)) { + if (__DEV__) { + if ( + !warned && + rootSibling.nodeType === ELEMENT_NODE && + (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) + ) { + warned = true; + warningWithoutStack( + false, + 'render(): Target node has markup rendered by React, but there ' + + 'are unrelated nodes as well. This is most commonly caused by ' + + 'white-space inserted around server-rendered markup.', + ); + } + } + container.removeChild(rootSibling); + } + } + if (__DEV__) { + if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) { + warnedAboutHydrateAPI = true; + lowPriorityWarning( + false, + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', + ); + } + } + // Legacy roots are not async by default. + const isConcurrent = false; + return new ReactRoot(container, isConcurrent, shouldHydrate); +} + +function legacyRenderSubtreeIntoContainer( + parentComponent: ?React$Component, + children: ReactNodeList, + container: DOMContainer, + forceHydrate: boolean, + callback: ?Function, +) { + // TODO: Ensure all entry points contain this check + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + + if (__DEV__) { + topLevelUpdateWarnings(container); + } + + // TODO: Without `any` type, Flow says "Property cannot be accessed on any + // member of intersection type." Whyyyyyy. + let root: Root = (container._reactRootContainer: any); + if (!root) { + // Initial mount + root = container._reactRootContainer = legacyCreateRootFromDOMContainer( + container, + forceHydrate, + ); + if (typeof callback === 'function') { + const originalCallback = callback; + callback = function() { + const instance = getPublicRootInstance(root._internalRoot); + originalCallback.call(instance); + }; + } + // Initial mount should not be batched. + unbatchedUpdates(() => { + if (parentComponent != null) { + root.legacy_renderSubtreeIntoContainer( + parentComponent, + children, + callback, + ); + } else { + root.render(children, callback); + } + }); + } else { + if (typeof callback === 'function') { + const originalCallback = callback; + callback = function() { + const instance = getPublicRootInstance(root._internalRoot); + originalCallback.call(instance); + }; + } + // Update + if (parentComponent != null) { + root.legacy_renderSubtreeIntoContainer( + parentComponent, + children, + callback, + ); + } else { + root.render(children, callback); + } + } + return getPublicRootInstance(root._internalRoot); +} + +function createPortal( + children: ReactNodeList, + container: DOMContainer, + key: ?string = null, +) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + // TODO: pass ReactDOM portal implementation as third argument + return createPortalImpl(children, container, null, key); +} + +const ReactDOM: Object = { + createPortal, + + findDOMNode( + componentOrElement: Element | ?React$Component, + ): null | Element | Text { + if (__DEV__) { + let owner = (ReactCurrentOwner.current: any); + if (owner !== null && owner.stateNode !== null) { + const warnedAboutRefsInRender = + owner.stateNode._warnedAboutRefsInRender; + warningWithoutStack( + warnedAboutRefsInRender, + '%s is accessing findDOMNode inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + getComponentName(owner.type) || 'A component', + ); + owner.stateNode._warnedAboutRefsInRender = true; + } + } + if (componentOrElement == null) { + return null; + } + if ((componentOrElement: any).nodeType === ELEMENT_NODE) { + return (componentOrElement: any); + } + if (__DEV__) { + return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); + } + return findHostInstance(componentOrElement); + }, + + hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { + // TODO: throw or warn if we couldn't hydrate? + return legacyRenderSubtreeIntoContainer( + null, + element, + container, + true, + callback, + ); + }, + + render( + element: React$Element, + container: DOMContainer, + callback: ?Function, + ) { + return legacyRenderSubtreeIntoContainer( + null, + element, + container, + false, + callback, + ); + }, + + unstable_renderSubtreeIntoContainer( + parentComponent: React$Component, + element: React$Element, + containerNode: DOMContainer, + callback: ?Function, + ) { + invariant( + parentComponent != null && hasInstance(parentComponent), + 'parentComponent must be a valid React Component', + ); + return legacyRenderSubtreeIntoContainer( + parentComponent, + element, + containerNode, + false, + callback, + ); + }, + + unmountComponentAtNode(container: DOMContainer) { + invariant( + isValidContainer(container), + 'unmountComponentAtNode(...): Target container is not a DOM element.', + ); + + if (container._reactRootContainer) { + if (__DEV__) { + const rootEl = getReactRootElementInContainer(container); + const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl); + warningWithoutStack( + !renderedByDifferentReact, + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by another copy of React.', + ); + } + + // Unmount should not be batched. + unbatchedUpdates(() => { + legacyRenderSubtreeIntoContainer(null, null, container, false, () => { + container._reactRootContainer = null; + }); + }); + // If you call unmountComponentAtNode twice in quick succession, you'll + // get `true` twice. That's probably fine? + return true; + } else { + if (__DEV__) { + const rootEl = getReactRootElementInContainer(container); + const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); + + // Check if the container itself is a React root node. + const isContainerReactRoot = + container.nodeType === ELEMENT_NODE && + isValidContainer(container.parentNode) && + !!container.parentNode._reactRootContainer; + + warningWithoutStack( + !hasNonRootReactChild, + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by React and is not a top-level container. %s', + isContainerReactRoot + ? 'You may have accidentally passed in a React root node instead ' + + 'of its container.' + : 'Instead, have the parent component update its state and ' + + 'rerender in order to remove this component.', + ); + } + + return false; + } + }, + + // Temporary alias since we already shipped React 16 RC with it. + // TODO: remove in React 17. + unstable_createPortal(...args) { + if (!didWarnAboutUnstableCreatePortal) { + didWarnAboutUnstableCreatePortal = true; + lowPriorityWarning( + false, + 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + + 'and will be removed in React 17+. Update your code to use ' + + 'ReactDOM.createPortal() instead. It has the exact same API, ' + + 'but without the "unstable_" prefix.', + ); + } + return createPortal(...args); + }, + + unstable_batchedUpdates: batchedUpdates, + + unstable_interactiveUpdates: interactiveUpdates, + + flushSync: flushSync, + + unstable_createRoot: createRoot, + unstable_flushControlled: flushControlled, + + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + // Keep in sync with ReactDOMUnstableNativeDependencies.js + // and ReactTestUtils.js. This is an array for better minification. + Events: [ + getInstanceFromNode, + getNodeFromInstance, + getFiberCurrentPropsFromNode, + EventPluginHubInjection.injectEventPluginsByName, + eventNameDispatchConfigs, + accumulateTwoPhaseDispatches, + accumulateDirectDispatches, + enqueueStateRestore, + restoreStateIfNeeded, + dispatchEvent, + runEventsInBatch, + ], + }, +}; + +type RootOptions = { + hydrate?: boolean, +}; + +function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot { + const functionName = enableStableConcurrentModeAPIs + ? 'createRoot' + : 'unstable_createRoot'; + invariant( + isValidContainer(container), + '%s(...): Target container is not a DOM element.', + functionName, + ); + const hydrate = options != null && options.hydrate === true; + return new ReactRoot(container, true, hydrate); +} + +if (enableStableConcurrentModeAPIs) { + ReactDOM.createRoot = createRoot; + ReactDOM.unstable_createRoot = undefined; +} + +const foundDevTools = injectIntoDevTools({ + findFiberByHostInstance: getClosestInstanceFromNode, + bundleType: __DEV__ ? 1 : 0, + version: ReactVersion, + rendererPackageName: 'react-dom', +}); + +if (__DEV__) { + if (!foundDevTools && canUseDOM && window.top === window.self) { + // If we're in Chrome or Firefox, provide a download link if not installed. + if ( + (navigator.userAgent.indexOf('Chrome') > -1 && + navigator.userAgent.indexOf('Edge') === -1) || + navigator.userAgent.indexOf('Firefox') > -1 + ) { + const protocol = window.location.protocol; + // Don't warn in exotic cases like chrome-extension://. + if (/^(https?|file):$/.test(protocol)) { + console.info( + '%cDownload the React DevTools ' + + 'for a better development experience: ' + + 'https://fb.me/react-devtools' + + (protocol === 'file:' + ? '\nYou might need to use a local HTTP server (instead of file://): ' + + 'https://fb.me/react-devtools-faq' + : ''), + 'font-weight:bold', + ); + } + } + } +} + +export default ReactDOM; diff --git a/packages/react-dom/src/fire/ReactFireHostConfig.js b/packages/react-dom/src/fire/ReactFireHostConfig.js new file mode 100644 index 0000000000..129f511e31 --- /dev/null +++ b/packages/react-dom/src/fire/ReactFireHostConfig.js @@ -0,0 +1,14 @@ +/** + * 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 + */ + +// This is just to get the setup running. +// TODO: real implementation. +// console.log('Hello from Fire host config.'); + +export * from '../client/ReactDOMHostConfig'; diff --git a/packages/react-dom/unstable-fire.js b/packages/react-dom/unstable-fire.js new file mode 100644 index 0000000000..e92fb9f098 --- /dev/null +++ b/packages/react-dom/unstable-fire.js @@ -0,0 +1,16 @@ +/** + * 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 + */ + +'use strict'; + +const ReactFire = require('./src/fire/ReactFire'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactFire.default || ReactFire; diff --git a/packages/react-reconciler/inline.fire.js b/packages/react-reconciler/inline.fire.js new file mode 100644 index 0000000000..0d0c1546cc --- /dev/null +++ b/packages/react-reconciler/inline.fire.js @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.fire.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.fire.js new file mode 100644 index 0000000000..738d1219bf --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.fire.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 * from 'react-dom/src/fire/ReactFireHostConfig'; diff --git a/scripts/jest/setupFire.js b/scripts/jest/setupFire.js index 363c8180e3..a54f280c77 100644 --- a/scripts/jest/setupFire.js +++ b/scripts/jest/setupFire.js @@ -1,5 +1,7 @@ 'use strict'; +jest.mock('react-dom', () => require.requireActual('react-dom/unstable-fire')); + jest.mock('shared/ReactFeatureFlags', () => { const ReactFeatureFlags = require.requireActual('shared/ReactFeatureFlags'); ReactFeatureFlags.disableInputAttributeSyncing = true; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index f8ec115438..c250600b99 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -91,7 +91,26 @@ const bundles = [ externals: ['react'], }, - //******* Test Utils *******/ + /******* React Fire *******/ + { + bundleTypes: [ + UMD_DEV, + UMD_PROD, + UMD_PROFILING, + NODE_DEV, + NODE_PROD, + NODE_PROFILING, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + ], + moduleType: RENDERER, + entry: 'react-dom/unstable-fire', + global: 'ReactFire', + externals: ['react'], + }, + + /******* Test Utils *******/ { moduleType: RENDERER_UTILS, bundleTypes: [FB_WWW_DEV, NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 487a9bb9df..2892a9abeb 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -12,6 +12,11 @@ module.exports = [ entryPoints: ['react-dom'], isFlowTyped: true, }, + { + shortName: 'fire', + entryPoints: ['react-dom/unstable-fire'], + isFlowTyped: true, + }, { shortName: 'art', entryPoints: ['react-art'],