Blob Blame History Raw
import React from "react";

function decodeParam(val) {
  if (!(typeof val === "string" || val.length === 0)) {
    return val;
  }

  try {
    return decodeURIComponent(val);
  } catch (err) {
    if (err instanceof URIError) {
      err.message = `Failed to decode param '${val}'`;
      err.status = 400;
    }

    throw err;
  }
}

// Match the provided URL path pattern to an actual URI string. For example:
//   matchURI({ path: '/posts/:id' }, '/dummy') => null
//   matchURI({ path: '/posts/:id' }, '/posts/123') => { id: 123 }
function matchURI(route, path) {
  const match = route.pattern.exec(path);

  if (!match) {
    return null;
  }

  const params = Object.create(null);

  for (let i = 1; i < match.length; i += 1) {
    params[route.keys[i - 1].name] = match[i] !== undefined ? decodeParam(match[i]) : undefined;
  }

  return params;
}

// Find the route matching the specified location (context), fetch the required data,
// instantiate and return a React component
function resolve(routes, context) {
  for (const route of routes) {
    const params = matchURI(route, context.error ? "/error" : context.pathname);

    if (!params) {
      continue;
    }

    // Check if the route has any data requirements, for example:
    // { path: '/tasks/:id', data: { task: 'GET /api/tasks/$id' }, page: './pages/task' }
    if (route.data) {
      // Load page component and all required data in parallel
      const keys = Object.keys(route.data);
      return Promise.all([
        route.load(),
        ...keys.map((key) => {
          const query = route.data[key];
          const method = query.substring(0, query.indexOf(" ")); // GET
          let url = query.substr(query.indexOf(" ") + 1); // /api/tasks/$id
          // TODO: Optimize
          Object.keys(params).forEach((k) => {
            url = url.replace(`${k}`, params[k]);
          });
          return fetch(url, { method, credentials: "same-origin" }).then((resp) => resp.json());
        }),
      ]).then(([Page, ...data]) => {
        const props = keys.reduce((result, key, i) => ({ ...result, [key]: data[i] }), {});
        return <Page route={{ ...route, params }} error={context.error} {...props} />;
      });
    }

    return route.load().then((Page) => <Page route={{ ...route, params }} error={context.error} />);
  }

  const error = new Error("Page not found");
  error.status = 404;
  return Promise.reject(error);
}

export default { resolve };