diff --git a/cli/JSXV4.md b/cli/JSXV4.md new file mode 100644 index 00000000..2d463c53 --- /dev/null +++ b/cli/JSXV4.md @@ -0,0 +1,100 @@ +**Abbreviation** + +The placement of `@react.component` is an abbreviation as described below. + +**Normal Case** + +```rescript +@react.component +let make = (~x, ~y, ~z) => body + +// is an abbreviation for + +let make = @react.component (~x, ~y, ~z) => body +``` + +**Forward Ref** + +```rescript +@react.component +let make = React.forwardRef((~x, ~y, ref) => body) + +// is an abbreviation for + +let make = React.forwardRef({ + let fn = + @react.component (~x, ~y, ~ref=?) => { + let ref = ref->Js.Nullable.fromOption + body + } + (props, ref) => fn({...props, ref: {ref->Js.Nullable.toOption}}) +}) +``` + +**Conversion** + +Conversion applies to an arrow function definition where all the arguments are labelled. +It produces a type definition and a new function. + +**Definition** + +```rescript +@react.component (~x, ~y=3+x, ?z) => body + +// is converted to + +type props<'x, 'y, 'z> = {x: 'x, @optional y: 'y, @optional z: 'z, @optional key: string} + +({x, y, z}: props<_>) => { + let y = switch props.y { + | None => 3 + x + | Some(y) => y + } + body +} +``` + +**Application** + +```rescript + +// is converted to +React.createElement(Comp.make, {x}) + + +// is converted to +React.createElement(Comp.make, {x, y:7, @optional z}) + + +// is converted to +React.createElement(Comp.make, {x, key: "7"}) +``` + +**Interface And External** + +```rescript +@react.component (~x: int, ~y: int=?, ~z: int=?) => React.element + +// is converted to + +type props<'x, 'y, 'z> = {x: 'x, @optional y: 'y, @optional z: 'z} + +props => React.element +``` + +Since an external is a function declaration, it follows the same rule. + +**Component Name** + +Use the V3 convention for names, and make sure the generated +function has the name of the enclosing module/file. + +**Fragment** + +```rescript +<> comp1 comp2 comp3 + +// is converted to + +ReactDOMRe.createElement(ReasonReact.fragment, [comp1, comp2, comp3]) +``` diff --git a/cli/reactjs_jsx_ppx_v3.ml b/cli/reactjs_jsx_ppx_v3.ml index 25146acb..cbc481b3 100644 --- a/cli/reactjs_jsx_ppx_v3.ml +++ b/cli/reactjs_jsx_ppx_v3.ml @@ -10,28 +10,37 @@ let nolabel = Nolabel let labelled str = Labelled str -let optional str = Optional str - let isOptional str = match str with Optional _ -> true | _ -> false let isLabelled str = match str with Labelled _ -> true | _ -> false +let isForwardRef = function +| { pexp_desc = Pexp_ident { txt = (Ldot (Lident "React", "forwardRef")) } } -> true +| _ -> false + let getLabel str = match str with Optional str | Labelled str -> str | Nolabel -> "" let optionIdent = Lident "option" +let optionalAttr = [ ({ txt = "optional"; loc = Location.none }, PStr []) ] + let constantString ~loc str = Ast_helper.Exp.constant ~loc (Pconst_string (str, None)) +let recordWithOnlyKey ~loc = Exp.record ~loc + (* {key: @optional None} *) + [({loc; txt = Lident "key"}, Exp.construct ~attrs:optionalAttr {loc; txt = Lident "None"} None)] + None + let safeTypeFromValue valueStr = let valueStr = getLabel valueStr in match String.sub valueStr 0 1 with "_" -> "T" ^ valueStr | _ -> valueStr [@@raises Invalid_argument] -let keyType loc = Typ.constr ~loc { loc; txt = optionIdent } [ Typ.constr ~loc { loc; txt = Lident "string" } [] ] +let keyType loc = Typ.constr ~loc { loc; txt = Lident "string" } [] -type 'a children = ListLiteral of 'a | Exact of 'a +let refType loc = (Typ.constr ~loc { loc; txt = Ldot (Ldot (Lident "ReactDOM", "Ref"), "currentDomRef") } []) -type componentConfig = { propsName : string } +type 'a children = ListLiteral of 'a | Exact of 'a (* if children is a list, convert it to an array while mapping each element. If not, just map over it, as usual *) let transformChildrenIfListUpper ~loc ~mapper theList = @@ -112,30 +121,6 @@ let makeNewBinding binding expression newName = | _ -> raise (Invalid_argument "react.component calls cannot be destructured.") [@@raises Invalid_argument] -(* Lookup the value of `props` otherwise raise Invalid_argument error *) -let getPropsNameValue _acc (loc, exp) = - match (loc, exp) with - | { txt = Lident "props" }, { pexp_desc = Pexp_ident { txt = Lident str } } -> { propsName = str } - | { txt }, _ -> - raise (Invalid_argument ("react.component only accepts props as an option, given: " ^ Longident.last txt)) - [@@raises Invalid_argument] - -(* Lookup the `props` record or string as part of [@react.component] and store the name for use when rewriting *) -let getPropsAttr payload = - let defaultProps = { propsName = "Props" } in - match payload with - | Some (PStr ({ pstr_desc = Pstr_eval ({ pexp_desc = Pexp_record (recordFields, None) }, _) } :: _rest)) -> - List.fold_left getPropsNameValue defaultProps recordFields - | Some (PStr ({ pstr_desc = Pstr_eval ({ pexp_desc = Pexp_ident { txt = Lident "props" } }, _) } :: _rest)) -> - { propsName = "props" } - | Some (PStr ({ pstr_desc = Pstr_eval (_, _) } :: _rest)) -> - raise (Invalid_argument "react.component accepts a record config with props as an options.") - | _ -> defaultProps - [@@raises Invalid_argument] - -(* Plucks the label, loc, and type_ from an AST node *) -let pluckLabelDefaultLocType (label, default, _, _, loc, type_) = (label, default, loc, type_) - (* Lookup the filename from the location information on the AST node and turn it into a valid module identifier *) let filenameFromLoc (pstr_loc : Location.t) = let fileName = match pstr_loc.loc_start.pos_fname with "" -> !Location.input_name | fileName -> fileName in @@ -162,93 +147,101 @@ let makeModuleName fileName nestedModules fnName = constructor and a props external *) -(* Build an AST node representing all named args for the `external` definition for a component's props *) -let rec recursivelyMakeNamedArgsForExternal list args = - match list with - | (label, default, loc, interiorType) :: tl -> - recursivelyMakeNamedArgsForExternal tl - (Typ.arrow ~loc label - ( match (label, interiorType, default) with - (* ~foo=1 *) - | label, None, Some _ -> - { ptyp_desc = Ptyp_var (safeTypeFromValue label); ptyp_loc = loc; ptyp_attributes = [] } - (* ~foo: int=1 *) - | _label, Some type_, Some _ -> type_ - (* ~foo: option(int)=? *) - | label, Some { ptyp_desc = Ptyp_constr ({ txt = Lident "option" }, [ type_ ]) }, _ - | label, Some { ptyp_desc = Ptyp_constr ({ txt = Ldot (Lident "*predef*", "option") }, [ type_ ]) }, _ - (* ~foo: int=? - note this isnt valid. but we want to get a type error *) - | label, Some type_, _ - when isOptional label -> - type_ - (* ~foo=? *) - | label, None, _ when isOptional label -> - { ptyp_desc = Ptyp_var (safeTypeFromValue label); ptyp_loc = loc; ptyp_attributes = [] } - (* ~foo *) - | label, None, _ -> { ptyp_desc = Ptyp_var (safeTypeFromValue label); ptyp_loc = loc; ptyp_attributes = [] } - | _label, Some type_, _ -> type_ ) - args) - | [] -> args - [@@raises Invalid_argument] - -(* Build an AST node for the [@bs.obj] representing props for a component *) -let makePropsValue fnName loc namedArgListWithKeyAndRef propsType = - let propsName = fnName ^ "Props" in - { - pval_name = { txt = propsName; loc }; - pval_type = - recursivelyMakeNamedArgsForExternal namedArgListWithKeyAndRef - (Typ.arrow nolabel - { ptyp_desc = Ptyp_constr ({ txt = Lident "unit"; loc }, []); ptyp_loc = loc; ptyp_attributes = [] } - propsType); - pval_prim = [ "" ]; - pval_attributes = [ ({ txt = "bs.obj"; loc }, PStr []) ]; - pval_loc = loc; - } - [@@raises Invalid_argument] - -(* Build an AST node representing an `external` with the definition of the [@bs.obj] *) -let makePropsExternal fnName loc namedArgListWithKeyAndRef propsType = - { pstr_loc = loc; pstr_desc = Pstr_primitive (makePropsValue fnName loc namedArgListWithKeyAndRef propsType) } - [@@raises Invalid_argument] - -(* Build an AST node for the signature of the `external` definition *) -let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType = - { psig_loc = loc; psig_desc = Psig_value (makePropsValue fnName loc namedArgListWithKeyAndRef propsType) } - [@@raises Invalid_argument] - -(* Build an AST node for the props name when converted to an object inside the function signature *) -let makePropsName ~loc name = { ppat_desc = Ppat_var { txt = name; loc }; ppat_loc = loc; ppat_attributes = [] } - -let makeObjectField loc (str, attrs, type_) = Otag ({ loc; txt = str }, attrs, type_) - -(* Build an AST node representing a "closed" object representing a component's props *) -let makePropsType ~loc namedTypeList = - Typ.mk ~loc (Ptyp_object (List.map (makeObjectField loc) namedTypeList, Closed)) - -(* Builds an AST node for the entire `external` definition of props *) -let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList = - makePropsExternal fnName loc - (List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef) - (makePropsType ~loc namedTypeList) - [@@raises Invalid_argument] - -let newtypeToVar newtype type_ = - let var_desc = Ptyp_var ("type-" ^ newtype) in - let typ (mapper : Ast_mapper.mapper) typ = - match typ.ptyp_desc with - | Ptyp_constr ({txt = Lident name}, _) when name = newtype -> - {typ with ptyp_desc = var_desc} - | _ -> Ast_mapper.default_mapper.typ mapper typ +(* make record from props and spread props if exists *) +let recordFromProps { pexp_loc } callArguments = + let rec removeLastPositionUnitAux props acc = + match props with + | [] -> acc + | [ (Nolabel, { pexp_desc = Pexp_construct ({ txt = Lident "()" }, None) }) ] -> acc + | (Nolabel, _) :: _rest -> raise (Invalid_argument "JSX: found non-labelled argument before the last position") + | prop :: rest -> removeLastPositionUnitAux rest (prop :: acc) + in + let props, propsToSpread = removeLastPositionUnitAux callArguments [] + |> List.rev + |> List.partition (fun (label, _) -> label <> labelled "spreadProps") in + let fields = props |> List.map (fun (arg_label, ({ pexp_loc } as expr) ) -> + (* In case filed label is "key" only then change expression to option *) + if isOptional arg_label then + ({ txt = (Lident (getLabel arg_label)); loc = pexp_loc} , { expr with pexp_attributes = optionalAttr }) + else + ({ txt = (Lident (getLabel arg_label)); loc = pexp_loc} , expr)) in - let mapper = {Ast_mapper.default_mapper with typ} in - mapper.typ mapper type_ + let spreadFields = propsToSpread |> List.map (fun (_, expression) -> expression) in + match spreadFields with + | [] -> { pexp_desc=Pexp_record (fields, None); pexp_loc; pexp_attributes=[]} + | [ spreadProps] -> { pexp_desc=Pexp_record (fields, Some spreadProps); pexp_loc; pexp_attributes=[] } + | spreadProps :: _ -> { pexp_desc=Pexp_record (fields, Some spreadProps); pexp_loc; pexp_attributes=[] } + +(* make type params for type props<'id, 'name, ...> *) +let makePropsTypeParamsTvar namedTypeList = + namedTypeList + |> List.filter_map (fun (_, label, _, _) -> + if label = "key" || label = "ref" then None else Some (Typ.var label, Invariant)) + +let extractOptionalCoreType = function +| { ptyp_desc = Ptyp_constr ({txt}, [ coreType ])} when txt = optionIdent -> coreType +| t -> t + +(* make type params for make fn arguments *) +(* let make = ({id, name, children}: props<'id, 'name, 'children>) *) +let makePropsTypeParams namedTypeList = + namedTypeList + |> List.filter_map (fun (_isOptional, label, _, _interiorType) -> + if label = "key" || label = "ref" then None + else Some (Typ.var label)) + +(* make type params for make sig arguments *) +(* let make: React.componentLike>, React.element> *) +let makePropsTypeParamsSig namedTypeList = + namedTypeList + |> List.filter_map (fun (isOptional, label, _, interiorType) -> + if label = "key" || label = "ref" then None + else if isOptional then Some (extractOptionalCoreType interiorType) + else Some interiorType) + +(* type props<'id, 'name, ...> = { @optional key: string, @optional id: 'id, ... } *) +let makePropsRecordType propsName loc namedTypeList = + let labelDeclList = + namedTypeList + |> List.map (fun (isOptional, label, _, _interiorType) -> + if label = "key" then Type.field ~loc ~attrs:optionalAttr { txt = label; loc } (keyType Location.none) + else if label = "ref" then Type.field ~loc ~attrs:optionalAttr { txt = label; loc } (refType Location.none) + else if isOptional then Type.field ~loc ~attrs:optionalAttr { txt = label; loc } (Typ.var label) + else Type.field ~loc { txt = label; loc } (Typ.var label)) + in + (* 'id, 'className, ... *) + let params = makePropsTypeParamsTvar namedTypeList in + Str.type_ Nonrecursive + [ + Type.mk ~loc + ~params + { txt = propsName; loc } + ~kind:(Ptype_record labelDeclList); + ] + +(* type props<'id, 'name, ...> = { @optional key: string, @optional id: 'id, ... } *) +let makePropsRecordTypeSig propsName loc namedTypeList = + let labelDeclList = + namedTypeList + |> List.map (fun (isOptional, label, _, _interiorType) -> + if label = "key" then Type.field ~loc ~attrs:optionalAttr { txt = label; loc } (keyType Location.none) + else if isOptional then Type.field ~loc ~attrs:optionalAttr { txt = label; loc } (Typ.var label) + else Type.field ~loc { txt = label; loc } (Typ.var label)) + in + let params = makePropsTypeParamsTvar namedTypeList in + Sig.type_ Nonrecursive + [ + Type.mk ~loc + ~params + { txt = propsName ; loc } + ~kind:(Ptype_record labelDeclList); + ] (* TODO: some line number might still be wrong *) let jsxMapper () = let jsxVersion = ref None in - let transformUppercaseCall3 modulePath mapper loc attrs _ callArguments = + let transformUppercaseCall3 modulePath mapper loc attrs callExpression callArguments = let children, argsWithLabels = extractChildren ~loc ~removeLastPositionUnit:true callArguments in let argsForMake = argsWithLabels in let childrenExpr = transformChildrenIfListUpper ~loc ~mapper children in @@ -265,8 +258,8 @@ let jsxMapper () = (* this is a hack to support react components that introspect into their children *) childrenArg := Some expression; [ (labelled "children", Exp.ident ~loc { loc; txt = Ldot (Lident "React", "null") }) ] ) - @ [ (nolabel, Exp.construct ~loc { loc; txt = Lident "()" } None) ] in + let record = recordFromProps callExpression args in let isCap str = let first = String.sub str 0 1 [@@raises Invalid_argument] in let capped = String.uppercase_ascii first in @@ -279,13 +272,16 @@ let jsxMapper () = | Ldot (_modulePath, value) as fullPath when isCap value -> Ldot (fullPath, "make") | modulePath -> modulePath in - let propsIdent = - match ident with - | Lident path -> Lident (path ^ "Props") - | Ldot (ident, path) -> Ldot (ident, path ^ "Props") - | _ -> raise (Invalid_argument "JSX name can't be the result of function applications") + let isEmptyRecord { pexp_desc } = + match pexp_desc with + | Pexp_record (labelDecls, _) when List.length labelDecls = 0 -> true + | _ -> false in - let props = Exp.apply ~attrs ~loc (Exp.ident ~loc { loc; txt = propsIdent }) args in + (* check if record which goes to Foo.make({ ... } as record) empty or not + if empty then change it to {key: None} only for upper case jsx + This would be redundant regarding PR progress https://github.com/rescript-lang/syntax/pull/299 + *) + let props = if isEmptyRecord record then recordWithOnlyKey ~loc else record in (* handle key, ref, children *) (* React.createElement(Component.make, props, ...children) *) match !childrenArg with @@ -300,8 +296,10 @@ let jsxMapper () = [@@raises Invalid_argument] in - let transformLowercaseCall3 mapper loc attrs callArguments id = + let transformLowercaseCall3 mapper loc attrs _callExpression callArguments id = let children, nonChildrenProps = extractChildren ~loc callArguments in + (* keep the v3 *) + (* let record = recordFromProps callExpression nonChildrenProps in *) let componentNameExpr = constantString ~loc id in let childrenExpr = transformChildrenIfList ~loc ~mapper children in let createElementCall = @@ -333,7 +331,7 @@ let jsxMapper () = [ (* "div" *) (nolabel, componentNameExpr); - (* ReactDOMRe.props(~className=blabla, ~foo=bar, ()) *) + (* ReactDOMRe.domProps(~className=blabla, ~foo=bar, ()) *) (labelled "props", propsCall); (* [|moreCreateElementCallsHere|] *) (nolabel, childrenExpr); @@ -410,18 +408,21 @@ let jsxMapper () = let argToType types (name, default, _noLabelName, _alias, loc, type_) = match (type_, name, default) with | Some { ptyp_desc = Ptyp_constr ({ txt = Lident "option" }, [ type_ ]) }, name, _ when isOptional name -> - ( getLabel name, + ( true, + getLabel name, [], { type_ with ptyp_desc = Ptyp_constr ({ loc = type_.ptyp_loc; txt = optionIdent }, [ type_ ]) } ) :: types | Some type_, name, Some _default -> - ( getLabel name, + ( false, + getLabel name, [], { ptyp_desc = Ptyp_constr ({ loc; txt = optionIdent }, [ type_ ]); ptyp_loc = loc; ptyp_attributes = [] } ) :: types - | Some type_, name, _ -> (getLabel name, [], type_) :: types + | Some type_, name, _ -> (false, getLabel name, [], type_) :: types | None, name, _ when isOptional name -> - ( getLabel name, + ( true, + getLabel name, [], { ptyp_desc = @@ -433,16 +434,24 @@ let jsxMapper () = } ) :: types | None, name, _ when isLabelled name -> - (getLabel name, [], { ptyp_desc = Ptyp_var (safeTypeFromValue name); ptyp_loc = loc; ptyp_attributes = [] }) + (false, getLabel name, [], { ptyp_desc = Ptyp_var (safeTypeFromValue name); ptyp_loc = loc; ptyp_attributes = [] }) :: types | _ -> types [@@raises Invalid_argument] in + let argWithDefaultValue (name, default, _,_,_,_) = + match default with + | Some default when isOptional name -> + Some (getLabel name, default) + | _ -> None + [@@raises Invalid_argument] + in + let argToConcreteType types (name, loc, type_) = match name with - | name when isLabelled name -> (getLabel name, [], type_) :: types - | name when isOptional name -> (getLabel name, [], Typ.constr ~loc { loc; txt = optionIdent } [ type_ ]) :: types + | name when isLabelled name -> (false, getLabel name, [], type_) :: types + | name when isOptional name -> (true, getLabel name, [], Typ.constr ~loc { loc; txt = optionIdent } [ type_ ]) :: types | _ -> types in @@ -452,7 +461,7 @@ let jsxMapper () = (* external *) | { pstr_loc; - pstr_desc = Pstr_primitive ({ pval_name = { txt = fnName }; pval_attributes; pval_type } as value_description); + pstr_desc = Pstr_primitive ({ pval_attributes; pval_type } as value_description); } as pstr -> ( match List.filter hasAttr pval_attributes with | [] -> structure :: returnStructures @@ -469,12 +478,14 @@ let jsxMapper () = in let innerType, propTypes = getPropTypes [] pval_type in let namedTypeList = List.fold_left argToConcreteType [] propTypes in - let pluckLabelAndLoc (label, loc, type_) = (label, None (* default *), loc, Some type_) in - let retPropsType = makePropsType ~loc:pstr_loc namedTypeList in - let externalPropsDecl = - makePropsExternal fnName pstr_loc - ((optional "key", None, pstr_loc, Some (keyType pstr_loc)) :: List.map pluckLabelAndLoc propTypes) - retPropsType + let retPropsType = (Typ.constr ~loc:pstr_loc + (Location.mkloc (Lident "props") pstr_loc) + (makePropsTypeParams namedTypeList)) + in + (* type props<'id, 'name> = { @optional key: string, @optional id: 'id, ... } *) + let propsRecordType = + makePropsRecordType "props" Location.none + ((true, "key", [], keyType pstr_loc) :: (true, "ref", [], refType pstr_loc) :: namedTypeList) in (* can't be an arrow because it will defensively uncurry *) let newExternalType = @@ -492,7 +503,7 @@ let jsxMapper () = }; } in - externalPropsDecl :: newStructure :: returnStructures + propsRecordType :: newStructure :: returnStructures | _ -> raise (Invalid_argument "Only one react.component call can exist on a component at one time") ) (* let component = ... *) | { pstr_loc; pstr_desc = Pstr_value (recFlag, valueBindings) } -> @@ -558,9 +569,10 @@ let jsxMapper () = pattern, ({ pexp_desc = Pexp_fun _ } as internalExpression) ); } -> - let wrap, hasUnit, exp = spelunkForFunExpression internalExpression in + let wrap, hasUnit, hasForwardRef, exp = spelunkForFunExpression internalExpression in ( wrap, hasUnit, + hasForwardRef, unerasableIgnoreExp { expression with pexp_desc = Pexp_fun (label, default, pattern, exp) } ) (* let make = (()) => ... *) (* let make = (_) => ... *) @@ -572,13 +584,13 @@ let jsxMapper () = { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, _internalExpression ); } -> - ((fun a -> a), true, expression) + ((fun a -> a), true, false, expression) (* let make = (~prop) => ... *) | { pexp_desc = Pexp_fun ((Labelled _ | Optional _), _default, _pattern, _internalExpression) } -> - ((fun a -> a), false, unerasableIgnoreExp expression) + ((fun a -> a), false, false, unerasableIgnoreExp expression) (* let make = (prop) => ... *) | { pexp_desc = Pexp_fun (_nolabel, _default, pattern, _internalExpression) } -> - if hasApplication.contents then ((fun a -> a), false, unerasableIgnoreExp expression) + if !hasApplication then ((fun a -> a), false, false, unerasableIgnoreExp expression) else Location.raise_errorf ~loc:pattern.ppat_loc "React: props need to be labelled arguments.\n\ @@ -587,121 +599,79 @@ let jsxMapper () = (* let make = {let foo = bar in (~prop) => ...} *) | { pexp_desc = Pexp_let (recursive, vbs, internalExpression) } -> (* here's where we spelunk! *) - let wrap, hasUnit, exp = spelunkForFunExpression internalExpression in - (wrap, hasUnit, { expression with pexp_desc = Pexp_let (recursive, vbs, exp) }) + let wrap, hasUnit, hasForwardRef, exp = spelunkForFunExpression internalExpression in + (wrap, hasUnit, hasForwardRef, { expression with pexp_desc = Pexp_let (recursive, vbs, exp) }) (* let make = React.forwardRef((~prop) => ...) *) | { pexp_desc = Pexp_apply (wrapperExpression, [ (Nolabel, internalExpression) ]) } -> let () = hasApplication := true in - let _, hasUnit, exp = spelunkForFunExpression internalExpression in - ((fun exp -> Exp.apply wrapperExpression [ (nolabel, exp) ]), hasUnit, exp) + let _, hasUnit, _, exp = spelunkForFunExpression internalExpression in + let hasForwardRef = isForwardRef wrapperExpression in + ((fun exp -> Exp.apply wrapperExpression [ (nolabel, exp) ]), hasUnit, hasForwardRef, exp) | { pexp_desc = Pexp_sequence (wrapperExpression, internalExpression) } -> - let wrap, hasUnit, exp = spelunkForFunExpression internalExpression in - (wrap, hasUnit, { expression with pexp_desc = Pexp_sequence (wrapperExpression, exp) }) - | e -> ((fun a -> a), false, e) + let wrap, hasUnit, hasForwardRef, exp = spelunkForFunExpression internalExpression in + (wrap, hasUnit, hasForwardRef, { expression with pexp_desc = Pexp_sequence (wrapperExpression, exp) }) + | e -> ((fun a -> a), false, false, e) in - let wrapExpression, hasUnit, expression = spelunkForFunExpression expression in - (wrapExpressionWithBinding wrapExpression, hasUnit, expression) + let wrapExpression, hasUnit, hasForwardRef, expression = spelunkForFunExpression expression in + (wrapExpressionWithBinding wrapExpression, hasUnit, hasForwardRef, expression) in - let bindingWrapper, hasUnit, expression = modifiedBinding binding in - let reactComponentAttribute = - try Some (List.find hasAttr binding.pvb_attributes) with Not_found -> None - in - let _attr_loc, payload = - match reactComponentAttribute with - | Some (loc, payload) -> (loc.loc, Some payload) - | None -> (emptyLoc, None) - in - let props = getPropsAttr payload in + let bindingWrapper, _hasUnit, hasForwardRef, expression = modifiedBinding binding in (* do stuff here! *) - let namedArgList, newtypes, forwardRef = + let namedArgList, _newtypes, _forwardRef = recursivelyTransformNamedArgsForMake mapper (modifiedBindingOld binding) [] [] in - let namedArgListWithKeyAndRef = - (optional "key", None, Pat.var { txt = "key"; loc = emptyLoc }, "key", emptyLoc, Some (keyType emptyLoc)) - :: namedArgList - in - let namedArgListWithKeyAndRef = - match forwardRef with - | Some _ -> - (optional "ref", None, Pat.var { txt = "key"; loc = emptyLoc }, "ref", emptyLoc, None) - :: namedArgListWithKeyAndRef - | None -> namedArgListWithKeyAndRef - in - let namedArgListWithKeyAndRefForNew = - match forwardRef with - | Some txt -> namedArgList @ [ (nolabel, None, Pat.var { txt; loc = emptyLoc }, txt, emptyLoc, None) ] - | None -> namedArgList - in - let pluckArg (label, _, _, alias, loc, _) = - let labelString = - match label with label when isOptional label || isLabelled label -> getLabel label | _ -> "" - in - ( label, - match labelString with - | "" -> Exp.ident ~loc { txt = Lident alias; loc } - | labelString -> - Exp.apply ~loc - (Exp.ident ~loc { txt = Lident "##"; loc }) - [ - (nolabel, Exp.ident ~loc { txt = Lident props.propsName; loc }); - (nolabel, Exp.ident ~loc { txt = Lident labelString; loc }); - ] ) - in let namedTypeList = List.fold_left argToType [] namedArgList in - let loc = emptyLoc in - let externalArgs = (* translate newtypes to type variables *) - List.fold_left - (fun args newtype -> - List.map (fun (a, b, c, d, e, maybeTyp) -> - match maybeTyp with - | Some typ -> (a, b, c, d, e, Some (newtypeToVar newtype.txt typ)) - | None -> (a, b, c, d, e, None)) - args) - namedArgListWithKeyAndRef - newtypes - in - let externalTypes = (* translate newtypes to type variables *) - List.fold_left - (fun args newtype -> - List.map (fun (a, b, typ) -> (a, b, newtypeToVar newtype.txt typ)) args) - namedTypeList - newtypes + (* let _ = ref *) + let vbIgnoreUnusedRef = Vb.mk (Pat.any ()) (Exp.ident (Location.mknoloc (Lident "ref"))) in + (* let ref = ref->Js.Nullable.fromOption *) + let vbRefFromOption = Vb.mk (Pat.var @@ Location.mknoloc "ref") + (Exp.apply (Exp.ident (Location.mknoloc (Ldot (Ldot (Lident "Js", "Nullable"), "fromOption")))) + [(Nolabel, Exp.ident (Location.mknoloc @@ Lident "ref"))]) in - let externalDecl = makeExternalDecl fnName loc externalArgs externalTypes in - let innerExpressionArgs = - List.map pluckArg namedArgListWithKeyAndRefForNew - @ if hasUnit then [ (Nolabel, Exp.construct { loc; txt = Lident "()" } None) ] else [] + let namedArgWithDefaultValueList = List.filter_map argWithDefaultValue namedArgList in + let vbMatch ((label, default)) = + Vb.mk (Pat.var (Location.mknoloc label)) + (Exp.match_ (Exp.ident { txt = Lident label; loc=Location.none }) + [ + Exp.case + (Pat.construct (Location.mknoloc @@ Lident "Some") (Some (Pat.var ( Location.mknoloc label)))) + (Exp.ident (Location.mknoloc @@ Lident label)); + Exp.case + (Pat.construct (Location.mknoloc @@ Lident "None") None) + default + ]) in - let innerExpression = - Exp.apply - (Exp.ident - { loc; txt = Lident (match recFlag with Recursive -> internalFnName | Nonrecursive -> fnName) }) - innerExpressionArgs + let vbMatchList = List.map vbMatch namedArgWithDefaultValueList in + (* type props = { ... } *) + let propsRecordType = + makePropsRecordType "props" emptyLoc + ((true, "key", [], keyType emptyLoc) :: (true, "ref", [], refType pstr_loc) :: namedTypeList) in - let innerExpressionWithRef = - match forwardRef with - | Some txt -> - { - innerExpression with - pexp_desc = - Pexp_fun - ( nolabel, - None, - { ppat_desc = Ppat_var { txt; loc = emptyLoc }; ppat_loc = emptyLoc; ppat_attributes = [] }, - innerExpression ); - } - | None -> innerExpression + let innerExpression = if hasForwardRef then + Exp.apply (Exp.ident @@ Location.mknoloc @@ Lident "make") + [(Nolabel, Exp.record + [ (Location.mknoloc @@ Lident "ref", Exp.apply ~attrs:optionalAttr + (Exp.ident (Location.mknoloc (Ldot (Ldot (Lident "Js", "Nullable"), "toOption")))) + [ (Nolabel, Exp.ident (Location.mknoloc @@ Lident "ref")) ]) + ] + (Some (Exp.ident (Location.mknoloc @@ Lident "props"))))] + else Exp.apply (Exp.ident (Location.mknoloc @@ Lident "make")) + [(Nolabel, Exp.ident (Location.mknoloc @@ Lident "props"))] in let fullExpression = + (* React component name should start with uppercase letter *) + (* let make = { let \"App" = props => make(props); \"App" } *) + (* let make = React.forwardRef({ + let \"App" = (props, ref) => make({...props, ref: @optional (Js.Nullabel.toOption(ref))}) + })*) Exp.fun_ nolabel None - { - ppat_desc = - Ppat_constraint - (makePropsName ~loc:emptyLoc props.propsName, makePropsType ~loc:emptyLoc externalTypes); - ppat_loc = emptyLoc; - ppat_attributes = []; - } - innerExpressionWithRef + (match namedTypeList with + | [] -> (Pat.var @@ Location.mknoloc "props") + | _ -> (Pat.constraint_ + (Pat.var @@ Location.mknoloc "props") + (Typ.constr (Location.mknoloc @@ Lident "props")([Typ.any ()])))) + (if hasForwardRef then Exp.fun_ nolabel None (Pat.var @@ Location.mknoloc "ref") innerExpression + else innerExpression) in let fullExpression = match fullModuleName with @@ -711,36 +681,63 @@ let jsxMapper () = [ Vb.mk ~loc:emptyLoc (Pat.var ~loc:emptyLoc { loc = emptyLoc; txt }) fullExpression ] (Exp.ident ~loc:emptyLoc { loc = emptyLoc; txt = Lident txt }) in + let rec returnedExpression patterns ({ pexp_desc } as expr) = + match pexp_desc with + | Pexp_fun (_arg_label, _default, { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, expr) -> + (patterns, expr) + | Pexp_fun (arg_label, _default, { ppat_loc }, expr) -> + if isLabelled arg_label || isOptional arg_label then + returnedExpression (({loc = ppat_loc; txt = Lident (getLabel arg_label)}, Pat.var { txt = getLabel arg_label; loc = ppat_loc}) :: patterns) expr + else + returnedExpression patterns expr + | _ -> (patterns, expr) + in + let patternsWithLid, expression = returnedExpression [] expression in + let pattern = (Pat.record ((List.rev patternsWithLid) @ [(Location.mknoloc (Lident "ref"), Pat.var (Location.mknoloc "ref"))]) Closed) + in + (* add patttern matching for optional prop value *) + let expression = if List.length vbMatchList = 0 then expression else (Exp.let_ Nonrecursive vbMatchList expression) in + (* add let _ = ref to ignore unused warning *) + let expression = Exp.let_ Nonrecursive [ vbIgnoreUnusedRef ] expression in + let expression = Exp.let_ Nonrecursive [ vbRefFromOption ] expression in + let expression = Exp.fun_ Nolabel None + begin + Pat.constraint_ pattern + (Typ.constr ~loc:emptyLoc { txt = Lident "props"; loc=emptyLoc } + (makePropsTypeParams namedTypeList)) + end + expression + in + (* let make = ({id, name, ...}: props<'id, 'name, ...>) => { ... } *) let bindings, newBinding = match recFlag with | Recursive -> - ( [ - bindingWrapper - (Exp.let_ ~loc:emptyLoc Recursive - [ - makeNewBinding binding expression internalFnName; - Vb.mk (Pat.var { loc = emptyLoc; txt = fnName }) fullExpression; - ] - (Exp.ident { loc = emptyLoc; txt = Lident fnName })); - ], - None ) + ([ + bindingWrapper + (Exp.let_ ~loc:emptyLoc Recursive + [ + makeNewBinding binding expression internalFnName; + Vb.mk (Pat.var { loc = emptyLoc; txt = fnName }) fullExpression; + ] + (Exp.ident { loc = emptyLoc; txt = Lident fnName })); + ], None) | Nonrecursive -> - ([ { binding with pvb_expr = expression; pvb_attributes = [] } ], Some (bindingWrapper fullExpression)) + ([ { binding with pvb_expr = expression; pvb_attributes = [] } ], Some (bindingWrapper fullExpression)) in - (Some externalDecl, bindings, newBinding) + (Some propsRecordType, bindings, newBinding) else (None, [ binding ], None) [@@raises Invalid_argument] - in + in (* END of mapBinding fn *) let structuresAndBinding = List.map mapBinding valueBindings in - let otherStructures (extern, binding, newBinding) (externs, bindings, newBindings) = - let externs = match extern with Some extern -> extern :: externs | None -> externs in + let otherStructures (type_, binding, newBinding) (types, bindings, newBindings) = + let types = match type_ with Some type_ -> type_ :: types | None -> types in let newBindings = match newBinding with Some newBinding -> newBinding :: newBindings | None -> newBindings in - (externs, binding @ bindings, newBindings) + (types, binding @ bindings, newBindings) in - let externs, bindings, newBindings = List.fold_right otherStructures structuresAndBinding ([], [], []) in - externs + let types, bindings, newBindings = List.fold_right otherStructures structuresAndBinding ([], [], []) in + types @ [ { pstr_loc; pstr_desc = Pstr_value (recFlag, bindings) } ] @ ( match newBindings with | [] -> [] @@ -757,7 +754,7 @@ let jsxMapper () = let transformComponentSignature _mapper signature returnSignatures = match signature with - | { psig_loc; psig_desc = Psig_value ({ pval_name = { txt = fnName }; pval_attributes; pval_type } as psig_desc) } + | { psig_loc; psig_desc = Psig_value ({ pval_attributes; pval_type } as psig_desc) } as psig -> ( match List.filter hasAttr pval_attributes with | [] -> signature :: returnSignatures @@ -774,12 +771,12 @@ let jsxMapper () = in let innerType, propTypes = getPropTypes [] pval_type in let namedTypeList = List.fold_left argToConcreteType [] propTypes in - let pluckLabelAndLoc (label, loc, type_) = (label, None, loc, Some type_) in - let retPropsType = makePropsType ~loc:psig_loc namedTypeList in - let externalPropsDecl = - makePropsExternalSig fnName psig_loc - ((optional "key", None, psig_loc, Some (keyType psig_loc)) :: List.map pluckLabelAndLoc propTypes) - retPropsType + let retPropsType = (Typ.constr (Location.mkloc (Lident "props") psig_loc) + (makePropsTypeParamsSig namedTypeList)) + in + let propsRecordType = + makePropsRecordTypeSig "props" Location.none + ((true, "key", [], keyType Location.none) :: (true, "ref", [], refType Location.none) :: namedTypeList) in (* can't be an arrow because it will defensively uncurry *) let newExternalType = @@ -797,7 +794,7 @@ let jsxMapper () = }; } in - externalPropsDecl :: newStructure :: returnSignatures + propsRecordType :: newStructure :: returnSignatures | _ -> raise (Invalid_argument "Only one react.component call can exist on a component at one time") ) | signature -> signature :: returnSignatures [@@raises Invalid_argument] @@ -824,7 +821,7 @@ let jsxMapper () = ReactDOMRe.createElement(~props=ReactDOMRe.props(~props1=foo, ~props2=bar, ()), [|bla|]) *) | { loc; txt = Lident id } -> ( match !jsxVersion with - | None | Some 3 -> transformLowercaseCall3 mapper loc attrs callArguments id + | None | Some 3 -> transformLowercaseCall3 mapper loc attrs callExpression callArguments id | Some _ -> raise (Invalid_argument "JSX: the JSX version must be 3") ) | { txt = Ldot (_, anythingNotCreateElementOrMake) } -> raise diff --git a/cli/reactjs_jsx_ppx_v3.mli b/cli/reactjs_jsx_ppx_v3.mli index da60a051..db2dbcad 100644 --- a/cli/reactjs_jsx_ppx_v3.mli +++ b/cli/reactjs_jsx_ppx_v3.mli @@ -32,8 +32,20 @@ `React.createElementVariadic(Foo.make, Foo.makeProps(~foo=bar, ~children=React.null, ()), [|foo, bar|])` transform `[@JSX] [foo]` into `ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])` + + v4: + transform `[@JSX] div(~props1=a, ~props2=b, ~spreadProps=props3 ~children=[foo, bar], ())` into + `ReactDOMRe.createDOMElementVariadic("div", ~props=ReactDOMRe.domProps(~props1=1, ~props2=b), [|foo, bar|])`. + transform the upper-cased case + `[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~spreadProps=baz ~children=[], ())` into + `Foo.make({...baz, key: a, ref: b, foo: bar})` + transform the upper-cased case + `[@JSX] Foo.createElement(~foo=bar, ~spreadProps=baz, ~children=[foo, bar], ())` into + Foo.make({...baz, foo: bar, children: React.null}), [|foo, bar|])` + transform `[@JSX] [foo]` into + `ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])` *) val rewrite_implementation : Parsetree.structure -> Parsetree.structure -val rewrite_signature : Parsetree.signature -> Parsetree.signature +val rewrite_signature : Parsetree.signature -> Parsetree.signature \ No newline at end of file diff --git a/src/res_core.ml b/src/res_core.ml index 630aad2c..80733050 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -2655,6 +2655,7 @@ and parseJsxFragment p = * | ?lident * | lident = jsx_expr * | lident = ?jsx_expr + * | {...jsx_expr} *) and parseJsxProp p = match p.Parser.token with @@ -2692,6 +2693,30 @@ and parseJsxProp p = in Some (label, attrExpr) end + (* {...props} *) + | Lbrace -> + begin + Parser.next p; + match p.Parser.token with + | DotDotDot -> + begin + Parser.next p; + let loc = mkLoc p.Parser.startPos p.prevEndPos in + let propLocAttr = (Location.mkloc "ns.namedArgLoc" loc, Parsetree.PStr []) in + let attrExpr = + let e = parsePrimaryExpr ~operand:(parseAtomicExpr p) p in + {e with pexp_attributes = propLocAttr::e.pexp_attributes} + in + (* using label "spreadProps" to distinguish from others *) + let label = Asttypes.Labelled "spreadProps" in + match p.Parser.token with + | Rbrace -> + Parser.next p; + Some (label, attrExpr) + |_ -> None + end + | _ -> None + end | _ -> None diff --git a/src/res_grammar.ml b/src/res_grammar.ml index ac649a1e..df6e6494 100644 --- a/src/res_grammar.ml +++ b/src/res_grammar.ml @@ -176,7 +176,7 @@ let isExprStart = function | _ -> false let isJsxAttributeStart = function - | Token.Lident _ | Question -> true + | Token.Lident _ | Question | Lbrace -> true | _ -> false let isStructureItemStart = function diff --git a/tests/ppx/react/expected/commentAtTop.res.txt b/tests/ppx/react/expected/commentAtTop.res.txt index 89dd8093..b9c3259b 100644 --- a/tests/ppx/react/expected/commentAtTop.res.txt +++ b/tests/ppx/react/expected/commentAtTop.res.txt @@ -1,10 +1,14 @@ -@obj external makeProps: (~msg: 'msg, ~key: string=?, unit) => {"msg": 'msg} = "" // test React JSX file +type props<'msg> = {@optional key: string, @optional ref: ReactDOM.Ref.currentDomRef, msg: 'msg} // test React JSX file -let make = - (@warning("-16") ~msg) => { +let make = ({msg, ref}: props<'msg>) => { + let ref = Js.Nullable.fromOption(ref) + let _ = ref + + { ReactDOMRe.createDOMElementVariadic("div", [{msg->React.string}]) } +} let make = { - let \"CommentAtTop" = (\"Props": {"msg": 'msg}) => make(~msg=\"Props"["msg"]) + let \"CommentAtTop" = (props: props<_>) => make(props) \"CommentAtTop" } diff --git a/tests/ppx/react/expected/externalWithCustomName.res.txt b/tests/ppx/react/expected/externalWithCustomName.res.txt index 083aeead..72e5974c 100644 --- a/tests/ppx/react/expected/externalWithCustomName.res.txt +++ b/tests/ppx/react/expected/externalWithCustomName.res.txt @@ -1,9 +1,12 @@ module Foo = { - @obj - external componentProps: (~a: int, ~b: string, ~key: string=?, unit) => {"a": int, "b": string} = - "" + type props<'a, 'b> = { + @optional key: string, + @optional ref: ReactDOM.Ref.currentDomRef, + a: 'a, + b: 'b, + } @module("Foo") - external component: React.componentLike<{"a": int, "b": string}, React.element> = "component" + external component: React.componentLike, React.element> = "component" } -let t = React.createElement(Foo.component, Foo.componentProps(~a=1, ~b={"1"}, ())) +let t = React.createElement(Foo.component, {a: 1, b: "1"}) diff --git a/tests/ppx/react/expected/innerModule.res.txt b/tests/ppx/react/expected/innerModule.res.txt index 6c0e7369..23145271 100644 --- a/tests/ppx/react/expected/innerModule.res.txt +++ b/tests/ppx/react/expected/innerModule.res.txt @@ -1,25 +1,37 @@ module Bar = { - @obj external makeProps: (~a: 'a, ~b: 'b, ~key: string=?, unit) => {"a": 'a, "b": 'b} = "" - let make = - (@warning("-16") ~a, @warning("-16") ~b, _) => { - Js.log("This function should be named `InnerModule.react$Bar`") - ReactDOMRe.createDOMElementVariadic("div", []) - } + type props<'a, 'b> = { + @optional key: string, + @optional ref: ReactDOM.Ref.currentDomRef, + a: 'a, + b: 'b, + } + let make = ({a, b, ref}: props<'a, 'b>) => { + let ref = Js.Nullable.fromOption(ref) + let _ = ref + + Js.log("This function should be named `InnerModule.react$Bar`") + ReactDOMRe.createDOMElementVariadic("div", []) + } let make = { - let \"InnerModule$Bar" = (\"Props": {"a": 'a, "b": 'b}) => - make(~b=\"Props"["b"], ~a=\"Props"["a"], ()) + let \"InnerModule$Bar" = (props: props<_>) => make(props) \"InnerModule$Bar" } - @obj external componentProps: (~a: 'a, ~b: 'b, ~key: string=?, unit) => {"a": 'a, "b": 'b} = "" + type props<'a, 'b> = { + @optional key: string, + @optional ref: ReactDOM.Ref.currentDomRef, + a: 'a, + b: 'b, + } + + let component = ({a, b, ref}: props<'a, 'b>) => { + let ref = Js.Nullable.fromOption(ref) + let _ = ref - let component = - (@warning("-16") ~a, @warning("-16") ~b, _) => { - Js.log("This function should be named `InnerModule.react$Bar$component`") - ReactDOMRe.createDOMElementVariadic("div", []) - } + Js.log("This function should be named `InnerModule.react$Bar$component`") + ReactDOMRe.createDOMElementVariadic("div", []) + } let component = { - let \"InnerModule$Bar$component" = (\"Props": {"a": 'a, "b": 'b}) => - component(~b=\"Props"["b"], ~a=\"Props"["a"], ()) + let \"InnerModule$Bar$component" = (props: props<_>) => make(props) \"InnerModule$Bar$component" } } diff --git a/tests/ppx/react/expected/newtype.res.txt b/tests/ppx/react/expected/newtype.res.txt index ace5106c..91e0cc9d 100644 --- a/tests/ppx/react/expected/newtype.res.txt +++ b/tests/ppx/react/expected/newtype.res.txt @@ -1,15 +1,17 @@ -@obj -external makeProps: ( - ~a: '\"type-a", - ~b: array>, - ~c: 'a, - ~key: string=?, - unit, -) => {"a": '\"type-a", "b": array>, "c": 'a} = "" -let make = (type a, ~a: a, ~b: array>, ~c: 'a, _) => - ReactDOMRe.createDOMElementVariadic("div", []) +type props<'a, 'b, 'c> = { + @optional key: string, + @optional ref: ReactDOM.Ref.currentDomRef, + a: 'a, + b: 'b, + c: 'c, +} +let make = ({ref}: props<'a, 'b, 'c>) => { + let ref = Js.Nullable.fromOption(ref) + let _ = ref + (type a, ~a: a, ~b: array>, ~c: 'a, _) => + ReactDOMRe.createDOMElementVariadic("div", []) +} let make = { - let \"Newtype" = (\"Props": {"a": '\"type-a", "b": array>, "c": 'a}) => - make(~c=\"Props"["c"], ~b=\"Props"["b"], ~a=\"Props"["a"]) + let \"Newtype" = (props: props<_>) => make(props) \"Newtype" } diff --git a/tests/ppx/react/expected/topLevel.res.txt b/tests/ppx/react/expected/topLevel.res.txt index b14eee2a..8860b44d 100644 --- a/tests/ppx/react/expected/topLevel.res.txt +++ b/tests/ppx/react/expected/topLevel.res.txt @@ -1,10 +1,17 @@ -@obj external makeProps: (~a: 'a, ~b: 'b, ~key: string=?, unit) => {"a": 'a, "b": 'b} = "" -let make = - (@warning("-16") ~a, @warning("-16") ~b, _) => { - Js.log("This function should be named 'TopLevel.react'") - ReactDOMRe.createDOMElementVariadic("div", []) - } +type props<'a, 'b> = { + @optional key: string, + @optional ref: ReactDOM.Ref.currentDomRef, + a: 'a, + b: 'b, +} +let make = ({a, b, ref}: props<'a, 'b>) => { + let ref = Js.Nullable.fromOption(ref) + let _ = ref + + Js.log("This function should be named 'TopLevel.react'") + ReactDOMRe.createDOMElementVariadic("div", []) +} let make = { - let \"TopLevel" = (\"Props": {"a": 'a, "b": 'b}) => make(~b=\"Props"["b"], ~a=\"Props"["a"], ()) + let \"TopLevel" = (props: props<_>) => make(props) \"TopLevel" } diff --git a/tests/ppx/react/expected/typeConstraint.res.txt b/tests/ppx/react/expected/typeConstraint.res.txt index 8940b164..b11ba87b 100644 --- a/tests/ppx/react/expected/typeConstraint.res.txt +++ b/tests/ppx/react/expected/typeConstraint.res.txt @@ -1,8 +1,16 @@ -@obj external makeProps: (~a: 'a, ~b: 'b, ~key: string=?, unit) => {"a": 'a, "b": 'b} = "" -let make: - type a. (~a: a, ~b: a, a) => React.element = - (~a, ~b, _) => ReactDOMRe.createDOMElementVariadic("div", []) +type props<'a, 'b> = { + @optional key: string, + @optional ref: ReactDOM.Ref.currentDomRef, + a: 'a, + b: 'b, +} +let make: 'a. (~a: 'a, ~b: 'a, 'a) => React.element = ({ref}: props<'a, 'b>) => { + let ref = Js.Nullable.fromOption(ref) + let _ = ref + (type a): ((~a: a, ~b: a, a) => React.element) => + (~a, ~b, _) => ReactDOMRe.createDOMElementVariadic("div", []) +} let make = { - let \"TypeConstraint" = (\"Props": {"a": 'a, "b": 'b}) => make(~b=\"Props"["b"], ~a=\"Props"["a"]) + let \"TypeConstraint" = (props: props<_>) => make(props) \"TypeConstraint" }