diff --git a/.changeset/fresh-shrimps-scream.md b/.changeset/fresh-shrimps-scream.md new file mode 100644 index 0000000..63e9da4 --- /dev/null +++ b/.changeset/fresh-shrimps-scream.md @@ -0,0 +1,6 @@ +--- +"@rescript-relay-router-example/client-rendering": patch +"rescript-relay-router": patch +--- + +fix parseRoute for routes with only query params or only path params diff --git a/examples/client-rendering/src/routes/Root__Home_route_renderer.res b/examples/client-rendering/src/routes/Root__Home_route_renderer.res new file mode 100644 index 0000000..9593e2c --- /dev/null +++ b/examples/client-rendering/src/routes/Root__Home_route_renderer.res @@ -0,0 +1,8 @@ +let renderer = Route__Root__Home_route.makeRenderer( + ~prepare=({environment}) => { + LayoutQuery_graphql.load(~environment, ~variables=(), ~fetchPolicy=StoreOrNetwork) + }, + ~render=props => { + {props.childRoutes} + }, +) diff --git a/examples/client-rendering/src/routes/Root__PathParamsOnly_route_renderer.res b/examples/client-rendering/src/routes/Root__PathParamsOnly_route_renderer.res new file mode 100644 index 0000000..d8e4e66 --- /dev/null +++ b/examples/client-rendering/src/routes/Root__PathParamsOnly_route_renderer.res @@ -0,0 +1,8 @@ +let renderer = Route__Root__PathParamsOnly_route.makeRenderer( + ~prepare=({environment}) => { + LayoutQuery_graphql.load(~environment, ~variables=(), ~fetchPolicy=StoreOrNetwork) + }, + ~render=props => { + {props.childRoutes} + }, +) diff --git a/examples/client-rendering/src/routes/__generated__/RouteDeclarations.res b/examples/client-rendering/src/routes/__generated__/RouteDeclarations.res index 39e0cfb..8b373ae 100644 --- a/examples/client-rendering/src/routes/__generated__/RouteDeclarations.res +++ b/examples/client-rendering/src/routes/__generated__/RouteDeclarations.res @@ -520,6 +520,147 @@ let make = (~prepareDisposeTimeout=5 * 60 * 1000): array (() => import(Root__PathParamsOnly_route_renderer.renderer))->Obj.magic->doLoadRouteRenderer(~routeName, ~loadedRouteRenderers) + let makePrepareProps = (. + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ): prepareProps => { + ignore(queryParams) + let prepareProps: Route__Root__PathParamsOnly_route.Internal.prepareProps = { + environment: environment, + location: location, + pageSlug: pathParams->Dict.getUnsafe("pageSlug"), + } + prepareProps->unsafe_toPrepareProps + } + + { + path: "other/:pageSlug", + name: routeName, + chunk: "Root__PathParamsOnly_route_renderer", + loadRouteRenderer, + preloadCode: ( + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ) => preloadCode( + ~loadedRouteRenderers, + ~routeName, + ~loadRouteRenderer, + ~environment, + ~location, + ~makePrepareProps, + ~pathParams, + ~queryParams, + ), + prepare: ( + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ~intent: RelayRouter.Types.prepareIntent, + ) => prepareRoute( + ~environment, + ~pathParams, + ~queryParams, + ~location, + ~getPrepared, + ~loadRouteRenderer, + ~makePrepareProps, + ~makeRouteKey=( + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t + ): string => { + ignore(queryParams) + + "Root__PathParamsOnly:" + ++ pathParams->Dict.get("pageSlug")->Option.getOr("") + + } + + , + ~routeName, + ~intent + ), + children: [], + } + }, + { + let routeName = "Root__Home" + let loadRouteRenderer = () => (() => import(Root__Home_route_renderer.renderer))->Obj.magic->doLoadRouteRenderer(~routeName, ~loadedRouteRenderers) + let makePrepareProps = (. + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ): prepareProps => { + ignore(pathParams) + ignore(queryParams) + let prepareProps: Route__Root__Home_route.Internal.prepareProps = { + environment: environment, + location: location, + } + prepareProps->unsafe_toPrepareProps + } + + { + path: "home", + name: routeName, + chunk: "Root__Home_route_renderer", + loadRouteRenderer, + preloadCode: ( + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ) => preloadCode( + ~loadedRouteRenderers, + ~routeName, + ~loadRouteRenderer, + ~environment, + ~location, + ~makePrepareProps, + ~pathParams, + ~queryParams, + ), + prepare: ( + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ~intent: RelayRouter.Types.prepareIntent, + ) => prepareRoute( + ~environment, + ~pathParams, + ~queryParams, + ~location, + ~getPrepared, + ~loadRouteRenderer, + ~makePrepareProps, + ~makeRouteKey=( + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t + ): string => { + ignore(pathParams) + ignore(queryParams) + + "Root__Home:" + + + } + + , + ~routeName, + ~intent + ), + children: [], + } }], } } diff --git a/examples/client-rendering/src/routes/__generated__/Route__Root__Home_route.res b/examples/client-rendering/src/routes/__generated__/Route__Root__Home_route.res new file mode 100644 index 0000000..a3de66a --- /dev/null +++ b/examples/client-rendering/src/routes/__generated__/Route__Root__Home_route.res @@ -0,0 +1,70 @@ +// @generated +// This file is autogenerated from `todoRoutes.json`, do not edit manually. +module Internal = { + @live + type prepareProps = { + environment: RescriptRelay.Environment.t, + location: RelayRouter.History.location, + } + + @live + type renderProps<'prepared> = { + childRoutes: React.element, + prepared: 'prepared, + environment: RescriptRelay.Environment.t, + location: RelayRouter.History.location, + } + + @live + type renderers<'prepared> = { + prepare: prepareProps => 'prepared, + prepareCode: option<(. prepareProps) => array>, + render: renderProps<'prepared> => React.element, + } + @live + let makePrepareProps = (. + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ): prepareProps => { + ignore(pathParams) + ignore(queryParams) + { + environment: environment, + location: location, + } + } + +} + + + +@inline +let routePattern = "/home" + +@live +let makeLink = () => { + RelayRouter.Bindings.generatePath(routePattern, Dict.fromArray([])) +} + +@live +let isRouteActive = (~exact: bool=false, {pathname}: RelayRouter.History.location): bool => { + RelayRouter.Internal.matchPathWithOptions({"path": routePattern, "end": exact}, pathname)->Option.isSome +} + +@live +let useIsRouteActive = (~exact=false) => { + let location = RelayRouter.Utils.useLocation() + React.useMemo(() => location->isRouteActive(~exact), (location, exact)) +} + + + +@obj +external makeRenderer: ( + ~prepare: Internal.prepareProps => 'prepared, + ~prepareCode: Internal.prepareProps => array=?, + ~render: Internal.renderProps<'prepared> => React.element, +) => Internal.renderers<'prepared> = "" + diff --git a/examples/client-rendering/src/routes/__generated__/Route__Root__PathParamsOnly_route.res b/examples/client-rendering/src/routes/__generated__/Route__Root__PathParamsOnly_route.res new file mode 100644 index 0000000..fb5f239 --- /dev/null +++ b/examples/client-rendering/src/routes/__generated__/Route__Root__PathParamsOnly_route.res @@ -0,0 +1,93 @@ +// @generated +// This file is autogenerated from `todoRoutes.json`, do not edit manually. +@live +type pathParams = { + pageSlug: string, +} + +module Internal = { + @live + type prepareProps = { + environment: RescriptRelay.Environment.t, + location: RelayRouter.History.location, + ...pathParams, + } + + @live + type renderProps<'prepared> = { + childRoutes: React.element, + prepared: 'prepared, + environment: RescriptRelay.Environment.t, + location: RelayRouter.History.location, + ...pathParams, + } + + @live + type renderers<'prepared> = { + prepare: prepareProps => 'prepared, + prepareCode: option<(. prepareProps) => array>, + render: renderProps<'prepared> => React.element, + } + @live + let makePrepareProps = (. + ~environment: RescriptRelay.Environment.t, + ~pathParams: dict, + ~queryParams: RelayRouter.Bindings.QueryParams.t, + ~location: RelayRouter.History.location, + ): prepareProps => { + ignore(queryParams) + { + environment: environment, + location: location, + pageSlug: pathParams->Dict.getUnsafe("pageSlug"), + } + } + +} + + + +@inline +let routePattern = "/other/:pageSlug" + +@live +let makeLink = (~pageSlug: string) => { + RelayRouter.Bindings.generatePath(routePattern, Dict.fromArray([("pageSlug", (pageSlug :> string)->encodeURIComponent)])) +} + +@live +let isRouteActive = (~exact: bool=false, {pathname}: RelayRouter.History.location): bool => { + RelayRouter.Internal.matchPathWithOptions({"path": routePattern, "end": exact}, pathname)->Option.isSome +} + +@live +let useIsRouteActive = (~exact=false) => { + let location = RelayRouter.Utils.useLocation() + React.useMemo(() => location->isRouteActive(~exact), (location, exact)) +} + +@live +let usePathParams = (): option => { + let {pathname} = RelayRouter.Utils.useLocation() + switch RelayRouter.Internal.matchPath(routePattern, pathname) { + | Some({params}) => Some(Obj.magic(params)) + | None => None + } +} + +@obj +external makeRenderer: ( + ~prepare: Internal.prepareProps => 'prepared, + ~prepareCode: Internal.prepareProps => array=?, + ~render: Internal.renderProps<'prepared> => React.element, +) => Internal.renderers<'prepared> = "" + + +@live +let parseRoute: ( + string, + ~exact: bool=?, +) => option = RelayRouter.Internal.parseRoute( + PathParams({routePattern: routePattern}), +) + diff --git a/examples/client-rendering/src/routes/__generated__/Route__Root_route.res b/examples/client-rendering/src/routes/__generated__/Route__Root_route.res index af3ea05..00e977d 100644 --- a/examples/client-rendering/src/routes/__generated__/Route__Root_route.res +++ b/examples/client-rendering/src/routes/__generated__/Route__Root_route.res @@ -6,6 +6,7 @@ module Internal = { byStatus: option<[#"completed" | #"not-completed"]>, byStatusDecoded: option, todoId: option, + pageSlug: option, } @live @@ -68,20 +69,22 @@ let useIsRouteActive = (~exact=false) => { React.useMemo(() => location->isRouteActive(~exact), (location, exact)) } @live -type subRoute = [#Todos] +type subRoute = [#Todos | #Home] @live -let getActiveSubRoute = (location: RelayRouter.History.location): option<[#Todos]> => { +let getActiveSubRoute = (location: RelayRouter.History.location): option<[#Todos | #Home]> => { let {pathname} = location if RelayRouter.Internal.matchPath("/todos", pathname)->Option.isSome { Some(#Todos) + } else if RelayRouter.Internal.matchPath("/home", pathname)->Option.isSome { + Some(#Home) } else { None } } @live -let useActiveSubRoute = (): option<[#Todos]> => { +let useActiveSubRoute = (): option<[#Todos | #Home]> => { let location = RelayRouter.Utils.useLocation() React.useMemo(() => { getActiveSubRoute(location) diff --git a/examples/client-rendering/src/routes/__generated__/Routes.res b/examples/client-rendering/src/routes/__generated__/Routes.res index 2cee768..810951d 100644 --- a/examples/client-rendering/src/routes/__generated__/Routes.res +++ b/examples/client-rendering/src/routes/__generated__/Routes.res @@ -35,4 +35,14 @@ module Root = { } } + /** [See route renderer](./Root__PathParamsOnly_route_renderer.res)*/ + module PathParamsOnly = { + module Route = Route__Root__PathParamsOnly_route + + } + /** [See route renderer](./Root__Home_route_renderer.res)*/ + module Home = { + module Route = Route__Root__Home_route + + } } \ No newline at end of file diff --git a/examples/client-rendering/src/routes/todoRoutes.json b/examples/client-rendering/src/routes/todoRoutes.json index 431809f..22791d7 100644 --- a/examples/client-rendering/src/routes/todoRoutes.json +++ b/examples/client-rendering/src/routes/todoRoutes.json @@ -28,5 +28,13 @@ "children": [] } ] + }, + { + "path": "other/:pageSlug", + "name": "PathParamsOnly" + }, + { + "path": "home", + "name": "Home" } ] diff --git a/examples/client-rendering/test/UrlEncodingDecoding.test.res b/examples/client-rendering/test/UrlEncodingDecoding.test.res index e8638ba..4a6bfab 100644 --- a/examples/client-rendering/test/UrlEncodingDecoding.test.res +++ b/examples/client-rendering/test/UrlEncodingDecoding.test.res @@ -27,6 +27,22 @@ describe("makeLink", () => { }) describe("parsing", () => { + test("parseRoute correctly decode query params", _t => { + let queryParams = + Routes.Root.Todos.Route.parseRoute( + "/todos?byValue=%2Fincorrect%20value%2C%20for%20url", + )->Option.getExn + + expect(queryParams.byValue->Option.getUnsafe)->Expect.toBe("/incorrect value, for url") + }) + test("parseRoute correctly decode path params", _t => { + let pathParams = + Routes.Root.PathParamsOnly.Route.parseRoute( + "/other/%2Fincorrect%20value%2C%20for%20url", + )->Option.getExn + + expect(pathParams.pageSlug)->Expect.toBe("/incorrect value, for url") + }) test("parseRoute correctly decode path and query params", _t => { let (pathParams, queryParams) = Routes.Root.Todos.Single.Route.parseRoute("/todos/123?showMore=false")->Option.getExn diff --git a/packages/rescript-relay-router/cli/RescriptRelayRouterCli__Codegen.res b/packages/rescript-relay-router/cli/RescriptRelayRouterCli__Codegen.res index 0f8b1eb..1a41758 100644 --- a/packages/rescript-relay-router/cli/RescriptRelayRouterCli__Codegen.res +++ b/packages/rescript-relay-router/cli/RescriptRelayRouterCli__Codegen.res @@ -791,7 +791,7 @@ let parseRoute: ( string, ~exact: bool=?, ) => option = RelayRouter.Internal.parseRoute( - PathParams({routePattern}), + PathParams({routePattern: routePattern}), ) \n` } else { diff --git a/packages/rescript-relay-router/src/RelayRouter__Internal.res b/packages/rescript-relay-router/src/RelayRouter__Internal.res index c498c86..15b63b3 100644 --- a/packages/rescript-relay-router/src/RelayRouter__Internal.res +++ b/packages/rescript-relay-router/src/RelayRouter__Internal.res @@ -220,37 +220,40 @@ type rec routeKind<_> = let parseRoute: type params. routeKind => (string, ~exact: bool=?) => option = - routeKind => (route, ~exact=false) => + routeKind => switch routeKind { | PathAndQueryParams({routePattern, parseQueryParams}) => - switch route->String.split("?") { - | [pathName, search] => - matchPathWithOptions({"path": routePattern, "end": exact}, pathName)->Option.map(({ - params, - }) => { - let params = Obj.magic(params) - let queryParams = + (route, ~exact=false) => + switch route->String.split("?") { + | [pathName, search] => + matchPathWithOptions({"path": routePattern, "end": exact}, pathName)->Option.map(({ + params, + }) => { + let pathParams = Obj.magic(params) + let queryParams = + search + ->RelayRouter__Bindings.QueryParams.parse + ->parseQueryParams + (pathParams, queryParams) + }) + | _ => None + } + | QueryParams({routePattern, parseQueryParams}) => + (route, ~exact=false) => + switch route->String.split("?") { + | [pathName, search] => + matchPathWithOptions({"path": routePattern, "end": exact}, pathName)->Option.map(_ => { search ->RelayRouter__Bindings.QueryParams.parse ->parseQueryParams - (params, queryParams) - }) - | _ => None - } - | QueryParams({routePattern, parseQueryParams}) => - matchPathWithOptions({"path": routePattern, "end": exact}, route)->Option.map(_ => { - route - ->RelayRouter__Bindings.QueryParams.parse - ->parseQueryParams - }) + }) + | _ => None + } | PathParams({routePattern}) => - switch route->String.split("?") { - | [pathName, _search] => - matchPathWithOptions({"path": routePattern, "end": exact}, pathName)->Option.map(({ + (route, ~exact=false) => + matchPathWithOptions({"path": routePattern, "end": exact}, route)->Option.map(({ params, }) => { Obj.magic(params) }) - | _ => None - } }