--!strict
-- ROBLOX upstream: https://github.com/facebook/react/blob/702fad4b1b48ac8f626ed3f35e8f86f5ea728084/packages/react/src/ReactElement.js
--[[*
* 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.
]]
local __DEV__ = _G.__DEV__ :: boolean
local Packages = script.Parent.Parent
local LuauPolyfill = require(Packages.LuauPolyfill)
local Error = LuauPolyfill.Error
type Object = LuauPolyfill.Object
-- ROBLOX: use patched console from shared
local console = require(Packages.Shared).console
local ReactTypes = require(Packages.Shared)
type React_StatelessFunctionalComponent
= ReactTypes.React_StatelessFunctionalComponent<
P
>
type React_ComponentType
= ReactTypes.React_ComponentType
type React_AbstractComponent
= ReactTypes.React_AbstractComponent
type ReactProviderType = ReactTypes.ReactProviderType
type React_ElementProps = ReactTypes.React_ElementProps
type React_Node = ReactTypes.React_Node
type ReactElement = ReactTypes.ReactElement
type ReactContext = ReactTypes.ReactContext
type Source = ReactTypes.Source
local ReactLazy = require(script.Parent.ReactLazy)
type LazyComponent = ReactLazy.LazyComponent
local getComponentName = require(Packages.Shared).getComponentName
-- ROBLOX deviation START: we eliminate invariant like in React 18 to avoid string formatting and function call overhead
-- local invariant = require(Packages.Shared).invariant
-- ROBLOX deviation END
local REACT_ELEMENT_TYPE = require(Packages.Shared).ReactSymbols.REACT_ELEMENT_TYPE
local ReactCurrentOwner = require(Packages.Shared).ReactSharedInternals.ReactCurrentOwner
--local hasOwnProperty = Object.prototype.hasOwnProperty
-- ROBLOX deviation START: upstream iterates over this table, but we manually unroll those loops for hot path performance
-- IF THIS TABLE UPDATES, YOU MUST UPDATE THE UNROLLED LOOPS AS WELL
local RESERVED_PROPS = {
key = true,
ref = true,
__self = true,
__source = true,
}
-- ROBLOX deviation END
local specialPropKeyWarningShown, specialPropRefWarningShown, didWarnAboutStringRefs
if __DEV__ then
didWarnAboutStringRefs = {}
end
local exports = {}
local function hasValidRef(config)
if __DEV__ then
-- ROBLOX DEVIATION: instead of getters, use `__index` metamethod to
-- detect if it's a warning object
if config.ref ~= nil and type(config.ref) == "table" then
if (config.ref :: any).isReactWarning then
return false
end
end
end
return config.ref ~= nil
end
local function hasValidKey(config)
if __DEV__ then
-- ROBLOX DEVIATION: instead of getters, use `__index` metamethod to
-- detect if it's a warning object
if config.key ~= nil and type(config.key) == "table" then
if (config.key :: any).isReactWarning then
return false
end
end
end
return config.key ~= nil
end
local reactWarning = { isReactWarning = true }
-- ROBLOX FIXME: These two warning 'getter' definitions both override the
-- metatable, and won't both work at the same time. The easy solution is to
-- define one metatable that does both instead of overwriting
local function defineKeyPropWarningGetter(props, displayName: string)
local warnAboutAccessingKey = function()
if __DEV__ then
if not specialPropKeyWarningShown then
specialPropKeyWarningShown = true
console.error(
"%s: `key` is not a prop. Trying to access it will result "
.. "in `nil` being returned. If you need to access the same "
.. "value within the child component, you should pass it as a different "
.. "prop. (https://reactjs.org/link/special-props)",
displayName
)
end
end
end
-- ROBLOX deviation: clear key to ensure metamethod is called,
-- then set key getter to call warnAboutAccessingKey
props.key = nil
setmetatable(props, {
__index = function(t, k)
if k == "key" then
warnAboutAccessingKey()
-- ROBLOX deviation: returns sentinel object that mimics upstream ability to check isReactWarning field
return reactWarning
end
-- ROBLOX FIXME Luau: needs deferred constraint resolution
return nil :: any
end,
})
end
local function defineRefPropWarningGetter(props, displayName: string)
-- deviation: Use a __call metamethod here to make this function-like, but
-- still able to have the `isReactWarning` flag defined on it
local warnAboutAccessingRef = function()
if __DEV__ then
if not specialPropRefWarningShown then
specialPropRefWarningShown = true
console.error(
"%s: `ref` is not a prop. Trying to access it will result "
.. "in `nil` being returned. If you need to access the same "
.. "value within the child component, you should pass it as a different "
.. "prop. (https://reactjs.org/link/special-props)",
displayName
)
end
end
end
-- ROBLOX deviation: clear key to ensure metamethod is called,
-- then set key getter to call warnAboutAccessingKey
props.ref = nil
setmetatable(props :: any, {
__index = function(t, k)
if k == "ref" then
warnAboutAccessingRef()
-- ROBLOX deviation: returns sentinel object that mimics upstream ability to check isReactWarning field
return reactWarning
end
-- ROBLOX FIXME Luau: needs deferred constraint resolution
return nil :: any
end,
})
end
local function warnIfStringRefCannotBeAutoConverted(config)
if __DEV__ then
if
-- ROBLOX deviation: We removed support for string refs, so all stringrefs cannot be auto-converted regardless
type(config.ref) == "string" and ReactCurrentOwner.current
-- and config.__self
-- and ReactCurrentOwner.current.stateNode ~= config.__self
then
local componentName = getComponentName(ReactCurrentOwner.current.type)
-- ROBLOX deviation: we don't support string refs and hard error instead of warn
if not didWarnAboutStringRefs[componentName] then
error(
string.format(
'Component "%s" contains the string ref "%s". '
.. "Support for string refs has been removed. "
.. "We recommend using useRef() or createRef() instead. "
.. "Learn more about using refs safely here: "
.. "https://reactjs.org/link/strict-mode-string-ref",
componentName or "Unknown",
config.ref
)
)
-- didWarnAboutStringRefs[componentName] = true
end
end
end
end
--[[*
* 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
]]
-- ROBLOX deviation BEGIN: extra annotations here inspired by TS and flowtype to facilitate prop checking at analyze-time
local function ReactElement(
type_: T,
key,
ref,
self,
source: Source?,
owner,
props: P
): ReactElement
-- ROBLOX deviation END
local element = {
-- 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,
}
-- This tag allows us to uniquely identify this as a React Element
element["$$typeof"] = REACT_ELEMENT_TYPE
if __DEV__ then
-- 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.
local nonEnumerable = {
validated = false,
}
element._store = setmetatable({}, {
-- 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.
__index = nonEnumerable,
__newindex = function(table, key, value)
if key == "validated" then
nonEnumerable.validated = value
else
rawset(table, key, value)
end
end,
})
-- self and source are DEV only properties.
setmetatable(element, {
__index = {
_self = self,
-- Two elements created in two different places should be considered
-- equal for testing purposes and therefore we hide it from enumeration.
_source = source,
},
})
end
-- ROBLOX FIXME Luau: this cast is needed until normalization lands
return element :: any
end
----[[*
-- * https://github.com/reactjs/rfcs/pull/107
-- * @param *} type
-- * @param object} props
-- * @param string} key
-- ]]
--
--
exports.jsx = function(type, config, maybeKey)
-- ROBLOX deviation START: skipping JSX for now, as it may never apply to Roblox
error("JSX is currently unsupported")
-- local propName; -- Reserved names are extracted
--
-- local props = }
-- local key = nil
-- local ref = nil; -- 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 nil or not.
--
-- if maybeKey ~= nil)
-- key = '' .. maybeKey
-- end
--
-- if hasValidKey(config))
-- key = '' .. config.key
-- end
--
-- if hasValidRef(config))
-- ref = config.ref
-- } -- Remaining properties are added to a new props object
--
--
-- for (propName in config)
-- if hasOwnProperty.call(config, propName) and !RESERVED_PROPS.hasOwnProperty(propName))
-- props[propName] = config[propName]
-- end
-- } -- Resolve default props
--
--
-- if type and type.defaultProps)
-- local defaultProps = type.defaultProps
--
-- for (propName in defaultProps)
-- if props[propName] == nil)
-- props[propName] = defaultProps[propName]
-- end
-- end
-- end
--
-- return ReactElement(type, key, ref, nil, nil, ReactCurrentOwner.current, props)
-- ROBLOX deviation END
end
--[[*
-- * https://github.com/reactjs/rfcs/pull/107
-- * @param *} type
-- * @param object} props
-- * @param string} key
-- ]]
--
exports.jsxDEV = function(type, config, maybeKey, source, self)
-- ROBLOX deviation START: we may never support JSX
error("JSX is currently unsupported")
-- local propName; -- Reserved names are extracted
--
-- local props = }
-- local key = nil
-- local ref = nil; -- 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 nil or not.
--
-- if maybeKey ~= nil)
-- key = '' .. maybeKey
-- end
--
-- if hasValidKey(config))
-- key = '' .. config.key
-- end
--
-- 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) and !RESERVED_PROPS.hasOwnProperty(propName))
-- props[propName] = config[propName]
-- end
-- } -- Resolve default props
--
--
-- if type and type.defaultProps)
-- local defaultProps = type.defaultProps
--
-- for (propName in defaultProps)
-- if props[propName] == nil)
-- props[propName] = defaultProps[propName]
-- end
-- end
-- end
--
-- if key or ref)
-- local displayName = function ()
-- if typeof type == 'function')
-- return type.displayName or type.name or 'Unknown'
-- end
--
-- return type
-- }()
--
-- if key)
-- defineKeyPropWarningGetter(props, displayName)
-- end
--
-- if ref)
-- defineRefPropWarningGetter(props, displayName)
-- end
--
-- return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
return nil
-- ROBLOX deviation END
end
--[[*
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
]]
-- ROBLOX deviation: this is TypeScript-derived annotation, but using flowtypes
-- function createElement(
-- type: FunctionComponent
| ComponentClass
| string,
-- props?: Attributes & P | null,
-- ...children: ReactNode[]): ReactElement
;
local function createElement
(
type_: React_StatelessFunctionalComponent
| React_ComponentType
| React_AbstractComponent
| ReactContext | LazyComponent | ReactProviderType | string,
config: P?,
...: React_Node | (...any) -> React_Node
): ReactElement
-- ROBLOX deviation START: extreme hot path, so manually unroll RESERVED_PROPS loop and use table.clone
local props = if config ~= nil then table.clone(config :: any) :: any else {}
-- ROBLOX deviation END
local key: (string | number)? = nil
local ref = nil
local self = nil
local source: Source? = nil
if config ~= nil then
-- ROBLOX deviation START: inline hasValidRef and hasValidKey success in hot path, still call in error case for warning
-- ROBLOX FIXME Luau: needs normalization: Type 'P & React_ElementProps' could not be converted into 'React_ElementProps'; none of the intersection parts are compatible
if hasValidRef(config :: any) then
ref = ((config :: any) :: React_ElementProps).ref
if __DEV__ then
warnIfStringRefCannotBeAutoConverted(
(config :: any) :: React_ElementProps
)
end
end
-- ROBLOX FIXME Luau: when configKey is inline: Type 'P & React_ElementProps' could not be converted into 'React_ElementProps'; none of the intersection parts are compatible
if hasValidKey(config :: any) then
local configKey = (config :: any).key
-- ROBLOX deviation: call tostring instead of concatenating with an
-- empty string, which can throw in luau. If the string is a number,
-- then do not use tostring
if type(configKey) == "number" then
key = configKey
else
-- ROBLOX FIXME Luau: narrowing bug: Type 'string' could not be converted into 'number'
key = tostring(configKey :: any)
end
end
-- ROBLOX deviation END
-- ROBLOX deviation START: seemingly only used for string ref warnings, which we don't support
-- self = if config.__self == nil then nil else config.__self
-- ROBLOX deviation END
source = if ((config :: any) :: React_ElementProps).__source == nil
then nil
else ((config :: any) :: React_ElementProps).__source
-- Remaining properties are added to a new props object
-- ROBLOX deviation START: extreme hot path, so manually unroll RESERVED_PROPS loop and use table.clone
if props.key ~= nil then
props.key = nil
end
if props.ref ~= nil then
props.ref = nil
end
if props.__self ~= nil then
props.__self = nil
end
if props.__source ~= nil then
props.__source = nil
end
-- ROBLOX deviation END
end
-- Children can be more than one argument, and those are transferred onto
-- the newly allocated props object.
-- ROBLOX deviation START: we have a shortcut for capturing varargs into an array in Lua, which is more performant
local childrenLength = select("#", ...)
if childrenLength == 1 then
props.children = select(1, ...)
elseif childrenLength > 1 then
-- ROBLOX TODO: there's a snapshot difference in storeOwners where key is 2 instead of 1 if we do `{...}`. does it matter?
-- local childArray = {...}
local childArray = table.create(childrenLength)
for i = 1, childrenLength do
local toInsert = select(i, ...)
table.insert(childArray, toInsert)
end
-- ROBLOX deviation END
if __DEV__ then
table.freeze(childArray)
end
props.children = childArray
end
-- Resolve default props
-- ROBLOX deviation START: Lua can't index defaultProps on a function
-- ROBLOX FIXME Luau: should know this can be a table due to type_ intersection with React_ComponentType<>. needs normalization?
if
type(type_ :: any) == "table"
and (type_ :: T & React_ComponentType).defaultProps
then
-- ROBLOX deviation END
-- ROBLOX FIXME Luau: defaultProps isn't narrowed by the guard above
local defaultProps = (type_ :: T & React_ComponentType
).defaultProps :: P
-- ROBLOX Luau TODO: defaultProps isn't known to be a table, since Luau doesn't allow us to do `
` yet
for propName, _ in (defaultProps :: any) :: Object do
if props[propName] == nil then
props[propName] = ((defaultProps :: any) :: Object)[propName]
end
end
end
if __DEV__ then
if key or ref then
-- ROBLOX deviation START: Lua can't store fields like displayName on functions
local displayName
if type(type_) == "function" then
-- displayName = (type_.displayName or type_.name) or "Unknown"
displayName = debug.info(type_, "n") or ""
elseif type(type_) == "table" then
displayName = (
(type_ :: T & React_ComponentType).displayName
or (type_ :: T & React_ComponentType
).name
) or "Unknown"
else
-- ROBLOX Luau FIXME: Luau should have narrowed type_ to string based on this above branches
displayName = type_ :: string
end
-- ROBLOX deviation END
if key then
defineKeyPropWarningGetter(props, displayName)
end
if ref then
defineRefPropWarningGetter(props, displayName)
end
end
-- ROBLOX deviation START: In upstream, JSX transformation is what
-- produces the `__source` field, so we'll just simulate it here for now
if source == nil then
-- go up one more because of ReactElementValidator indirection
source = {
fileName = debug.info(3, "s"),
lineNumber = debug.info(3, "l"),
}
end
-- ROBLOX deviation END
end
-- ROBLOX FIXME Luau: this cast is needed until normalization lands
return ReactElement(
type_,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
) :: any
end
exports.createElement = createElement
----[[*
-- * Return a function that produces ReactElements of a given type.
-- * See https://reactjs.org/docs/react-api.html#createfactory
-- ]]
--
--export function createFactory(type)
-- local factory = createElement.bind(null, type); -- Expose the type on the factory and the prototype so that it can be
-- -- easily accessed on elements. E.g. `.type == Foo`.
-- -- This should not be named `constructor` since this may not be the function
-- -- that created the element, and it may not even be a constructor.
-- -- Legacy hook: remove it
--
-- factory.type = type
-- return factory
--end
exports.cloneAndReplaceKey =
function
(oldElement: ReactElement
, newKey: any): ReactElement
local newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props
)
return newElement
end
--[[*
* Clone and return a new ReactElement using element as the starting point.
* See https://reactjs.org/docs/react-api.html#cloneelement
]]
exports.cloneElement = function
(
element: ReactElement
,
config: (P & React_ElementProps)?,
...: React_Node
): ReactElement
-- ROBLOX deviation START: use if instead of variant to avoid error message formatting even when there's no problem
if element == nil then
error(
Error.new(
"React.cloneElement(...): The argument must be a React element, but you passed "
.. tostring(element)
)
)
end
-- Original props are copied
local elementProps = element.props
local props: P & React_ElementProps = if elementProps ~= nil
then table.clone(elementProps :: P & React_ElementProps) :: any
else {} :: P & React_ElementProps
-- Reserved names are extracted
local key = element.key
local ref = element.ref
-- Self is preserved since the owner is preserved.
-- ROBLOX deviation: _self field only used for string ref checking
-- local self = element._self
-- Source is preserved since cloneElement is unlikely to be targeted by a
-- transpiler, and the original source is probably a better indicator of the
-- true owner.
local source = element._source
-- Owner will be preserved, unless ref is overridden
local owner = element._owner
if config ~= nil then
-- ROBLOX deviation START: inline hasValidRef and hasValidKey success in hot path, still call in error case for warning
local configRef = config.ref
if configRef ~= nil then
-- Silently steal the ref from the parent.
ref = configRef
owner = ReactCurrentOwner.current
else
hasValidRef(config)
end
local configKey = config.key
-- ROBLOX FIXME Luau: needs normalization, generic subtype escaping scope
if configKey ~= nil then
if type(configKey) == "number" then
key = configKey
else
-- ROBLOX FIXME Luau: narrowing bug: Type 'string' could not be converted into 'number'
key = configKey :: any or "nil"
end
else
hasValidKey((config :: any) :: React_ElementProps)
end
-- ROBLOX deviation END
end
-- Remaining properties override existing props
local elementType = element.type
local defaultProps: P? = if type(elementType) == "table"
then elementType.defaultProps
else nil
-- ROBLOX deviation: cannot call pairs on nil the way you can use `for...in`
-- on nil in JS, so we check for nil before iterating
if config ~= nil then
for propName, _ in config :: any do
if (config :: any)[propName] ~= nil and not RESERVED_PROPS[propName] then
if (config :: any)[propName] == nil and defaultProps ~= nil then
-- Resolve default props
-- ROBLOX FIXME Luau: force-cast required to avoid TypeError: Expected type table, got 'P' instead
(props :: any)[propName] = (defaultProps :: any)[propName]
else
(props :: any)[propName] = (config :: any)[propName]
end
end
end
end
-- Children can be more than one argument, and those are transferred onto
-- the newly allocated props object.
-- ROBLOX deviation START: we have a shortcut for capturing varargs into an array in Lua, which is more performant
local childrenLength = select("#", ...)
if childrenLength == 1 then
props.children = select(1, ...)
elseif childrenLength > 1 then
(props :: any).children = { ... }
end
-- ROBLOX deviation END
-- ROBLOX FIXME Luau: this cast is needed until normalization lands
return ReactElement(
element.type,
key,
ref,
nil,
source,
owner,
(props :: any) :: P & React_ElementProps
) :: any
end
--[[*
* 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
]]
exports.isValidElement = function(object)
return type(object) == "table" and object["$$typeof"] == REACT_ELEMENT_TYPE
end
return exports