Permalink
Browse files

`@phenomic/plugin-renderer-react`: `withInitialProps` HOC

that supports `static async getInitialProps({pathname, params})` and `static async getAllPossibleUrls({path})` to generate pages with any source of data (fetch from any kind of REST, Grapql APIs). Inspired by Next.js

 `@phenomic/plugin-renderer-react`: `withPhenomicApi` HOC (previously named `createContainer`)

⚠️ `@phenomic/plugin-renderer-react`: `createContainer` is now `withPhenomicApi`(you will get a warning until you update your import)
  • Loading branch information...
MoOx committed Apr 4, 2018
1 parent f8f61d7 commit a4e12968f9d8d871e82e24d14c5b11cd4d1b6f59
@@ -0,0 +1,12 @@
declare module "hoist-non-react-statics" {
/*
S - source component statics
TP - target component props
SP - additional source component props
*/
declare module.exports: <TP, SP, S>(
target: React$ComponentType<TP>,
source: React$ComponentType<TP & SP> & S,
blacklist?: { [key: $Keys<S>]: boolean }
) => React$ComponentType<TP> & $Shape<S>;
}
View
@@ -227,7 +227,9 @@ declare type PhenomicRoute = {|
path: string,
params?: { [key: string]: any },
component: {
getQueries?: (props: { params: { [key: string]: any } }) => {
getInitialProps?: ({ params: { [key: string]: any } }) => Object,
getAllPossibleUrls?: ({ path: string }) => Array<string>,
getQueries?: ({ params: { [key: string]: any } }) => {
[key: string]: PhenomicQueryConfig
}
}
@@ -28,6 +28,7 @@
"chalk": "^1.1.3",
"classnames": "^2.2.5",
"debug": "^2.6.0",
"hoist-non-react-statics": "^2.5.0",
"prop-types": "^15.5.8",
"react-hot-loader": "^3.0.0-beta.7",
"socket.io-client": "^1.7.2",
@@ -12,3 +12,12 @@ Array [
"",
]
`;
exports[`should be able to get urls from the static method 1`] = `
Array [
"",
"test/1",
"test/2",
"test/3",
]
`;
@@ -18,7 +18,7 @@ it("should be able to resolve dynamic urls", async () => {
component: {
getQueries: (/*params: PhenomicQueryConfig*/) => ({
test: query({
id: "one"
// id: "one"
// path?: string,
// after?: string,
// by?: string,
@@ -40,3 +40,29 @@ it("should be able to generate a multiple static url", async () => {
})
).toMatchSnapshot();
});
it("should be able to get urls from the static method", async () => {
function getAllPossibleUrls({ path }) {
if (!path.includes(":arg")) return [path];
return [1, 2, 3].map(i => path.replace(":arg", String(i)));
}
expect(
await resolve({
routes: [
{
path: "/",
component: {
getAllPossibleUrls
}
},
{
path: "/test/:arg",
component: {
getAllPossibleUrls
}
}
]
})
).toMatchSnapshot();
});
@@ -1,7 +1,9 @@
import query from "@phenomic/api-client/lib/query";
import createApp, { renderApp } from "./createApp.js";
import createContainer from "./components/Container";
import createContainer from "./deprecated-createContainer.js";
import withInitialProps from "./withInitialProps";
import withPhenomicApi from "./withPhenomicApi";
import Provider from "./components/Provider";
import BodyRenderer from "./components/BodyRenderer";
import textRenderer from "./components/textRenderer";
@@ -11,6 +13,8 @@ export {
renderApp,
createApp,
createContainer,
withInitialProps,
withPhenomicApi,
Provider,
query,
BodyRenderer,
@@ -0,0 +1,16 @@
import * as React from "react";
import { getDisplayName } from "./utils";
import withPhenomicApi from "./withPhenomicApi";
export default function deprecatedCreateContainer<P>(
ComposedComponent: React.ComponentType<P>,
getQueries: (props: Object) => Object = () => ({})
) {
const displayName = getDisplayName(ComposedComponent);
console.warn(
"`createContainer` has been renamed to `withPhenomicApi`. You can just replace this and this warning will go away (you can import the new name from the same place as before), in " +
displayName
);
return withPhenomicApi(ComposedComponent, getQueries);
}
@@ -1,14 +1,27 @@
[@bs.module "@phenomic/plugin-renderer-react/lib/client"]
external originalCreateContainer :
external createContainer_ :
(ReasonReact.reactClass, Js.t({..})) => ReasonReact.reactClass =
"createContainer";
[@bs.module "@phenomic/plugin-renderer-react/lib/client"]
external withPhenomicApi_ :
(ReasonReact.reactClass, Js.t({..})) => ReasonReact.reactClass =
"withPhenomicApi";
[@bs.module "@phenomic/plugin-renderer-react/lib/client"]
external withInitialProps_ :
(ReasonReact.reactClass, Js.t({..})) => ReasonReact.reactClass =
"withInitialProps";
module BodyRenderer = BodyRenderer;
module Link = Link;
let createContainer = (comp, queries) =>
originalCreateContainer(comp, queries);
let createContainer = (comp, queries) => createContainer_(comp, queries);
let withPhenomicApi = (comp, queries) => withPhenomicApi_(comp, queries);
let withInitialProps = comp => withInitialProps_(comp);
type jsNodeList('a) = {
.
@@ -1,10 +1,5 @@
import fetchRestApi from "@phenomic/api-client/lib/fetch";
import query from "@phenomic/api-client/lib/query";
import { encode } from "@phenomic/core/lib/api/helpers";
import resolveUrlsFromPhenomicApi from "./resolveUrlsFromPhenomicApi";
const defaultQueryKey = "default";
const mainKey = "id";
const debug = require("debug")("phenomic:plugin:renderer-react");
const arrayUnique = array => [...new Set(array)];
@@ -19,7 +14,7 @@ const flatten = (array: $ReadOnlyArray<any>) => {
return flattenedArray;
};
const getRouteQueries = route => {
const resolveUrlsForDynamicParams = async function(route: PhenomicRoute) {
if (
// reference is incorrect? (eg: import { Thing } instead of import Thing)
!route.component ||
@@ -35,155 +30,42 @@ const getRouteQueries = route => {
"Check the component reference and its origin. Are the import/export correct?"
);
}
const initialRouteParams: Object = route.params || {};
if (!route.component.getQueries) {
debug(route.path, "have no queries");
return {};
}
return route.component.getQueries({
params: initialRouteParams
});
};
const getMainQuery = (routeQueries, route) => {
const keys = Object.keys(routeQueries);
const firstKey = keys[0];
const firstKeyAsInt = parseInt(firstKey, 10);
// parseInt("12.") == "12"
if (
// $FlowFixMe it's on purpose
firstKeyAsInt == firstKey &&
String(firstKeyAsInt).length == firstKey.length
) {
console.warn(`The main path used for ${route.path} is ${firstKey}`);
}
return { key: firstKey, item: routeQueries[firstKey] };
};
const resolveURLsForDynamicParams = async function(route: PhenomicRoute) {
// If the path doesn't contain any kind of parameter, no need to
// iterate over the path
if (!route.path.includes("*") && !route.path.includes(":")) {
debug("not a dynamic route");
return route;
}
const routeQueries = getRouteQueries(route);
const mainQuery = getMainQuery(routeQueries, route);
if (!mainQuery.item) {
debug("no query detected for", route.path);
return route;
const maybeResolvedRoute = await resolveUrlsFromPhenomicApi(route);
if (maybeResolvedRoute !== false) {
return maybeResolvedRoute;
}
debug(route.path, `fetching path '${mainQuery.key}'`, routeQueries);
let key =
(routeQueries[mainQuery.key] && routeQueries[mainQuery.key].by) || mainKey;
if (key === defaultQueryKey) {
key = mainKey;
}
let queryResult;
try {
queryResult = await fetchRestApi(query({ path: mainQuery.item.path }));
} catch (e) {
// log simple-json-fetch error if any
throw e.error || e;
if (route.component.getAllPossibleUrls) {
return await route.component.getAllPossibleUrls({ path: route.path });
}
debug(
route.path,
`path fetched. ${queryResult.list.length} items (id: ${key})`
);
// get all possible values for the query
const list = arrayUnique(
queryResult.list.reduce((acc, item) => {
if (!item[key]) return acc;
if (Array.isArray(item[key])) acc = acc.concat(item[key]);
else acc.push(item[key]);
return acc;
}, [])
);
debug(route.path, "list (unique)", list);
const urlsData = list.reduce((acc, value) => {
let resolvedPath = route.path.replace(":" + key, value);
let params = { [key]: value };
// try * if url has not param
if (key === mainKey && resolvedPath === route.path) {
resolvedPath = resolvedPath.replace("*", value);
// react-router splat is considered as the id
params = { splat: value };
}
if (route.path !== resolvedPath)
acc.push({ ...route, path: resolvedPath, params });
return acc;
}, []);
debug(route.path, "urls data", urlsData);
// if no data found, we still try to render something
const finalUrlsData = urlsData.length ? urlsData : [{ path: route.path }];
// try :after with key
const reAfter = /:after\b/;
return finalUrlsData.reduce((acc, routeData) => {
if (!routeData.path.match(reAfter)) {
acc.push(routeData);
} else {
queryResult.list.map(item => {
// $FlowFixMe params[key] act as a truthy value
if (routeData.params && routeData.params[key]) {
if (
(Array.isArray(item[key]) &&
item[key].includes(routeData.params[key])) ||
item[key] === routeData.params[key]
) {
acc.push({
...routeData,
path: routeData.path.replace(reAfter, encode(item.id))
});
}
} else {
acc.push({
...routeData,
path: routeData.path.replace(reAfter, encode(item.id))
});
}
});
}
return acc;
}, []);
return route.path;
};
const normalizePath = (path: string) => path.replace(/^\//, "");
const resolveURLsToPrerender = async function({
const resolveUrls = async function({
routes
}: {
routes: $ReadOnlyArray<PhenomicRoute>
}) {
const dynamicRoutes = await Promise.all(
routes.map(route => resolveURLsForDynamicParams(route))
routes.map(route => resolveUrlsForDynamicParams(route))
);
const flattenedDynamicRoutes = flatten(dynamicRoutes);
const filtredDynamicRoutes = flattenedDynamicRoutes.filter(url => {
if (url.path && url.path.includes("*")) {
if (url.includes("*")) {
debug(
`${
url.path
} is including a '*' but it has not been resolved: url is skipped`
`${url} is including a '*' but it has not been resolved: url is skipped`
);
return false;
}
return true;
});
// debug("filtred dynamic routes", filtredDynamicRoutes)
const normalizedURLs = filtredDynamicRoutes.map(routeData =>
normalizePath(routeData.path)
);
const normalizedURLs = filtredDynamicRoutes.map(normalizePath);
debug("normalize urls", normalizedURLs);
const uniqsNormalizedPath = [...new Set(normalizedURLs)];
return uniqsNormalizedPath;
return arrayUnique(normalizedURLs);
};
export default resolveURLsToPrerender;
export default resolveUrls;
Oops, something went wrong.

0 comments on commit a4e1296

Please sign in to comment.