diff --git a/package-lock.json b/package-lock.json index 56484d8a6..fe8a6b257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "devDependencies": { "jest": "^26.0.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.0.0-beta-26f2496093-20240514", + "react-dom": "^19.0.0-beta-26f2496093-20240514", "react-test-renderer": "^18.2.0" } }, @@ -4414,49 +4414,38 @@ "dev": true }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "19.0.0-beta-26f2496093-20240514", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-beta-26f2496093-20240514.tgz", + "integrity": "sha512-ZsU/WjNZ6GfzMWsq2DcGjElpV9it8JmETHm9mAJuOJNhuJcWJxt8ltCJabONFRpDFq1A/DP0d0KFj9CTJVM4VA==", "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "19.0.0-beta-26f2496093-20240514", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-beta-26f2496093-20240514.tgz", + "integrity": "sha512-UvQ+K1l3DFQ34LDgfFSNuUGi9EC+yfE9tS6MdpNTd5fx7qC7KLfepfC/KpxWMQZ7JfE3axD4ZO6H4cBSpAZpqw==", "dev": true, "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "0.25.0-beta-26f2496093-20240514" }, "peerDependencies": { - "react": "^18.2.0" + "react": "19.0.0-beta-26f2496093-20240514" } }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.25.0-beta-26f2496093-20240514", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-beta-26f2496093-20240514.tgz", + "integrity": "sha512-vDwOytLHFnA3SW2B1lNcbO+/qKVyLCX+KLpm+tRGNDsXpyxzRgkIaYGWmX+S70AJGchUHCtuqQ50GFeFgDbXUw==", + "dev": true + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "node_modules/react-shallow-renderer": { - "version": "16.15.0", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", - "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-test-renderer": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", @@ -4477,6 +4466,19 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-test-renderer/node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", diff --git a/package.json b/package.json index 63bf49935..e5eeec6de 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "homepage": "https://reasonml.github.io/reason-react/", "devDependencies": { "jest": "^26.0.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.0.0-beta-26f2496093-20240514", + "react-dom": "^19.0.0-beta-26f2496093-20240514", "react-test-renderer": "^18.2.0" }, "jest": { diff --git a/ppx/reason_react_ppx.ml b/ppx/reason_react_ppx.ml index cbceb3437..d47f95c5d 100644 --- a/ppx/reason_react_ppx.ml +++ b/ppx/reason_react_ppx.ml @@ -47,8 +47,8 @@ let labelled str = Labelled str let optional str = Optional str module Binding = struct - (* Binding is the interface that the ppx uses to interact with the bindings. - Here we define the same APIs as the bindings but it generates Parsetree *) + (* Binding is the interface that the ppx relies on to interact with the react bindings. + Here we define the same APIs as the bindings but it generates Parsetree nodes *) module ReactDOM = struct let domProps ~applyLoc ~loc props = Builder.pexp_apply ~loc:applyLoc @@ -58,9 +58,6 @@ module Binding = struct end module React = struct - let null ~loc = - Builder.pexp_ident ~loc { loc; txt = Ldot (Lident "React", "null") } - let array ~loc children = Builder.pexp_apply ~loc (Builder.pexp_ident ~loc @@ -92,18 +89,22 @@ let rec find_opt p = function | [] -> None | x :: l -> if p x then Some x else find_opt p l -let getLabel str = +let getLabelOrEmpty str = match str with Optional str | Labelled str -> str | Nolabel -> "" +let getLabel str = + match str with Optional str | Labelled str -> Some str | Nolabel -> None + let optionIdent = Lident "option" let constantString ~loc str = Builder.pexp_constant ~loc (Pconst_string (str, Location.none, None)) let safeTypeFromValue valueStr = - let valueStr = getLabel valueStr in - match String.sub valueStr 0 1 with "_" -> "T" ^ valueStr | _ -> valueStr -[@@raises Invalid_argument] + match getLabel valueStr with + | Some valueStr when String.sub valueStr 0 1 = "_" -> ("T" ^ valueStr) + | Some valueStr -> valueStr + | None -> "" let keyType loc = Builder.ptyp_constr ~loc { loc; txt = Lident "string" } [] @@ -218,14 +219,12 @@ let otherAttrsPure { attr_name = loc; _ } = loc.txt <> "react.component" let hasAttrOnBinding { pvb_attributes; _ } = find_opt hasAttr pvb_attributes <> None -(* Finds the name of the variable the binding is assigned to, otherwise raises - Invalid_argument *) +(* Finds the name of the variable the binding is assigned to, otherwise raises an error *) let getFnName binding = match binding with | { pvb_pat = { ppat_desc = Ppat_var { txt; _ }; _ }; _ } -> txt - | _ -> - raise (Invalid_argument "react.component calls cannot be destructured.") -[@@raises Invalid_argument] + | { pvb_loc; _} -> + Location.raise_errorf ~loc:pvb_loc "[@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead." let makeNewBinding binding expression newName = match binding with @@ -237,22 +236,17 @@ let makeNewBinding binding expression newName = pvb_expr = expression; pvb_attributes = [ merlinFocus ]; } - | _ -> - raise (Invalid_argument "react.component calls cannot be destructured.") -[@@raises Invalid_argument] + | { pvb_loc; _ } -> + Location.raise_errorf ~loc:pvb_loc "[@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead." -(* Lookup the value of `props` otherwise raise Invalid_argument error *) -let getPropsNameValue _acc (loc, exp) = - match (loc, exp) with +(* Lookup the value of `props` otherwise raise errorf *) +let getPropsNameValue _acc (loc, expr) = + match (loc, expr) 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_exn txt)) -[@@raises Invalid_argument] + | { txt; loc }, _ -> + Location.raise_errorf ~loc "[@react.component] only accepts 'props' as a field, given: %s" (Longident.last_exn txt) (* Lookup the `props` record or string as part of [@react.component] and store the name for use when rewriting *) @@ -278,12 +272,10 @@ let getPropsAttr payload = } :: _rest)) -> { propsName = "props" } - | Some (PStr ({ pstr_desc = Pstr_eval (_, _); _ } :: _rest)) -> - raise - (Invalid_argument - "react.component accepts a record config with props as an options.") + | Some (PStr ({ pstr_desc = Pstr_eval (_, _); pstr_loc; _ } :: _rest)) -> + Location.raise_errorf ~loc:pstr_loc + "[@react.component] accepts a record config with 'props' as a field." | _ -> defaultProps -[@@raises Invalid_argument] (* Plucks the label, loc, and type_ from an AST node *) let pluckLabelDefaultLocType (label, default, _, _, loc, type_) = @@ -364,7 +356,6 @@ let rec recursivelyMakeNamedArgsForExternal ~types_come_from_signature list args | _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 ~types_come_from_signature loc @@ -394,7 +385,6 @@ let makePropsValue fnName ~types_come_from_signature loc ]; pval_loc = loc; } -[@@raises Invalid_argument] (* Build an AST node representing an `external` with the definition of the [@bs.obj] *) @@ -407,7 +397,6 @@ let makePropsExternal fnName loc ~component_is_external (makePropsValue ~types_come_from_signature:component_is_external fnName loc namedArgListWithKeyAndRef propsType); } -[@@raises Invalid_argument] (* Build an AST node for the signature of the `external` definition *) let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType = @@ -418,7 +407,6 @@ let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType = (makePropsValue ~types_come_from_signature:true fnName loc namedArgListWithKeyAndRef propsType); } -[@@raises Invalid_argument] (* Build an AST node for the props name when converted to an object inside the function signature *) @@ -502,7 +490,6 @@ let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList = makePropsExternal ~component_is_external:false fnName loc (List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef) (makePropsType ~loc namedTypeList) -[@@raises Invalid_argument] (* TODO: some line number might still be wrong *) let jsxMapper = @@ -513,7 +500,7 @@ let jsxMapper = let argsForMake = argsWithLabels in let keyProps, otherProps = List.partition - (fun (arg_label, _) -> "key" = getLabel arg_label) + (fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label) argsForMake in let jsxExpr, key, childrenProp = @@ -527,10 +514,12 @@ let jsxMapper = (label, mapper#expression ctxt expression)) in let isCap str = - let first = String.sub str 0 1 [@@raises Invalid_argument] in - let capped = String.uppercase_ascii first in - first = capped - [@@raises Invalid_argument] + match String.length str with + | 0 -> false + | _ -> + let first = String.sub str 0 1 in + let capped = String.uppercase_ascii first in + first = capped in let ident = match modulePath with @@ -592,7 +581,7 @@ let jsxMapper = let componentNameExpr = constantString ~loc:callerLoc id in let keyProps, nonChildrenProps = List.partition - (fun (arg_label, _) -> "key" = getLabel arg_label) + (fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label) nonChildrenProps in @@ -641,17 +630,9 @@ let jsxMapper = let rec recursivelyTransformNamedArgsForMake ~ctxt mapper expr list = let expr = mapper#expression ctxt expr in match expr.pexp_desc with - (* TODO: make this show up with a loc. *) | Pexp_fun (Labelled "key", _, _, _) | Pexp_fun (Optional "key", _, _, _) -> - raise - (Invalid_argument - "Key cannot be accessed inside of a component. Don't worry - you \ - can always key a component from its parent!") - | Pexp_fun (Labelled "ref", _, _, _) | Pexp_fun (Optional "ref", _, _, _) -> - raise - (Invalid_argument - "Ref cannot be passed as a normal prop. Please use `forwardRef` \ - API instead.") + Location.raise_errorf ~loc:expr.pexp_loc + ("~key cannot be accessed from the component props. Please set the key where the component is being used.") | Pexp_fun ( ((Optional label | Labelled label) as arg), default, @@ -698,7 +679,6 @@ let jsxMapper = "reason-react-ppx: react.component refs only support plain arguments \ and type annotations." | _ -> (list, None) - [@@raises Invalid_argument] in let argToType types (name, default, _noLabelName, _alias, loc, type_) = @@ -720,7 +700,7 @@ let jsxMapper = } ) :: types | Some type_, name, Some _default -> - ( getLabel name, + ( getLabelOrEmpty name, [], { ptyp_desc = Ptyp_constr ({ loc; txt = optionIdent }, [ type_ ]); @@ -729,7 +709,7 @@ let jsxMapper = ptyp_attributes = []; } ) :: types - | Some type_, name, _ -> (getLabel name, [], type_) :: types + | Some type_, name, _ -> (getLabelOrEmpty name, [], type_) :: types | None, Optional label, _ -> ( label, [], @@ -761,7 +741,6 @@ let jsxMapper = } ) :: types | _ -> types - [@@raises Invalid_argument] in let argToConcreteType types (name, loc, type_) = @@ -1094,7 +1073,7 @@ let jsxMapper = in let pluckArg (label, _, _, alias, loc, _) = ( label, - match getLabel label with + match getLabelOrEmpty label with | "" -> Builder.pexp_ident ~loc { txt = Lident alias; loc } | labelString -> Builder.pexp_apply ~loc diff --git a/ppx/test/component-without-make.t/input.re b/ppx/test/component-without-make.t/input.re new file mode 100644 index 000000000..2e2e266e9 --- /dev/null +++ b/ppx/test/component-without-make.t/input.re @@ -0,0 +1,17 @@ +module Component_with_x_as_main_function = { + [@react.component] + let x = () =>
; +}; + +module Component_with_createElement_as_main_function = { + [@react.component] + let createElement = (~lola) =>