Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Centralize route matching in one place #2021

Open
wants to merge 1 commit into
base: v1.x-2022-07
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions packages/hydrogen/src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {
RunSsrParams,
RunRscParams,
ResolvedHydrogenConfig,
ResolvedHydrogenRoutes,
RequestHandler,
} from './types.js';
import type {RequestHandlerOptions} from './shared-types.js';
Expand All @@ -27,11 +26,7 @@ import {
} from './foundation/ServerRequestProvider/index.js';
import type {ServerResponse} from 'http';
import type {PassThrough as PassThroughType} from 'stream';
import {
getApiRouteFromURL,
renderApiRoute,
getApiRoutes,
} from './utilities/apiRoutes.js';
import {getApiRouteFromURL, renderApiRoute} from './utilities/apiRoutes.js';
import {ServerPropsProvider} from './foundation/ServerPropsProvider/index.js';
import {isBotUA} from './utilities/bot-ua.js';
import {getCache, setCache} from './foundation/runtime.js';
Expand All @@ -58,6 +53,7 @@ import {
import {CacheShort, NO_STORE} from './foundation/Cache/strategies/index.js';
import {getBuiltInRoute} from './foundation/BuiltInRoutes/BuiltInRoutes.js';
import {FORM_REDIRECT_COOKIE} from './constants.js';
import {findRouteMatches, createRoutes} from './utilities/routes.js';

declare global {
// This is provided by a Vite plugin
Expand Down Expand Up @@ -94,7 +90,7 @@ export const renderHydrogen = (App: any) => {

const hydrogenConfig: ResolvedHydrogenConfig = {
...inlineHydrogenConfig,
routes: hydrogenRoutes,
routes: createRoutes(hydrogenRoutes),
};

request.ctx.hydrogenConfig = hydrogenConfig;
Expand Down Expand Up @@ -136,7 +132,6 @@ export const renderHydrogen = (App: any) => {
{
resource: builtInRouteResource,
params: {},
hasServerComponent: false,
},
hydrogenConfig,
{
Expand Down Expand Up @@ -236,18 +231,28 @@ async function processRequest(

const log = getLoggerWithContext(request);
const isRSCRequest = request.isRscRequest();
const apiRoute = !isRSCRequest && getApiRoute(url, hydrogenConfig.routes);
const decodedPathname = decodeURIComponent(
new URL(request.normalizedUrl).pathname
);
const matchedRoutes = findRouteMatches(
hydrogenConfig.routes,
decodedPathname
);

// These matched routes are used later in <FileRoutes> component
request.ctx.matchedRoutes = matchedRoutes;

const apiRoute =
!isRSCRequest && getApiRouteFromURL(matchedRoutes, request.method);

// The API Route might have a default export, making it also a server component
// If it does, only render the API route if the request method is GET
if (apiRoute && (!apiRoute.hasServerComponent || request.method !== 'GET')) {
if (apiRoute) {
const apiResponse = await renderApiRoute(
request,
apiRoute,
hydrogenConfig,
{
session: sessionApi,
}
{session: sessionApi}
);

return apiResponse instanceof Request
Expand All @@ -262,7 +267,7 @@ async function processRequest(
const state: Record<string, any> = isRSCRequest
? parseJSON(decodeURIComponent(url.searchParams.get('state') || '{}'))
: {
pathname: decodeURIComponent(url.pathname),
pathname: decodedPathname,
search: decodeURIComponent(url.search),
};

Expand Down Expand Up @@ -316,11 +321,6 @@ async function getTemplate(
return template;
}

function getApiRoute(url: URL, routes: ResolvedHydrogenRoutes) {
const apiRoutes = getApiRoutes(routes);
return getApiRouteFromURL(url, apiRoutes);
}

function assembleHtml({
ssrHtml,
rscPayload,
Expand Down
119 changes: 26 additions & 93 deletions packages/hydrogen/src/foundation/FileRoutes/FileRoutes.server.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import React, {useMemo} from 'react';
import {matchPath} from '../../utilities/matchPath.js';
import {log} from '../../utilities/log/index.js';
import {extractPathFromRoutesKey} from '../../utilities/apiRoutes.js';
import {useServerRequest} from '../ServerRequestProvider/index.js';

import type {ImportGlobEagerOutput} from '../../types.js';
import {RouteParamsProvider} from '../useRouteParams/RouteParamsProvider.client.js';
import {createRoutes, findRouteMatches} from '../../utilities/routes.js';

interface FileRoutesProps {
/** The routes defined by Vite's [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. */
routes?: ImportGlobEagerOutput;
/** A path that's prepended to all file routes. You can modify `basePath` if you want to prefix all file routes. For example, you can prefix all file routes with a locale. */
basePath?: string;
/** The portion of the file route path that shouldn't be a part of the URL. You need to modify this if you want to import routes from a location other than the default `src/routes`. */
dirPrefix?: string | RegExp;
dirPrefix?: string;
}

/**
Expand All @@ -27,94 +25,29 @@ export function FileRoutes({routes, basePath, dirPrefix}: FileRoutesProps) {

if (routeRendered) return null;

if (!routes) {
const fileRoutes = request.ctx.hydrogenConfig!.routes;
routes = fileRoutes.files;
dirPrefix ??= fileRoutes.dirPrefix;
basePath ??= fileRoutes.basePath;
}

basePath ??= '/';

const pageRoutes = useMemo(
() => createPageRoutes(routes!, basePath, dirPrefix),
[routes, basePath, dirPrefix]
const {match: route, details} = routes
? useMemo(
() =>
findRouteMatches(
createRoutes({files: routes, basePath, dirPrefix}),
serverProps.pathname
),
[routes, basePath, dirPrefix, serverProps.pathname]
)
: request.ctx.matchedRoutes!;

if (!route || !details || !route.resource.default) return null;

request.ctx.router.routeRendered = true;
request.ctx.router.routeParams = details.params;
const ServerComponent = route.resource.default;

return (
<RouteParamsProvider
routeParams={details.params}
basePath={route.basePath ?? '/'}
>
<ServerComponent params={details.params} {...serverProps} />
</RouteParamsProvider>
);

let foundRoute, foundRouteDetails;

for (let i = 0; i < pageRoutes.length; i++) {
foundRouteDetails = matchPath(serverProps.pathname, pageRoutes[i]);

if (foundRouteDetails) {
foundRoute = pageRoutes[i];
break;
}
}

if (foundRoute) {
request.ctx.router.routeRendered = true;
request.ctx.router.routeParams = foundRouteDetails.params;
return (
<RouteParamsProvider
routeParams={foundRouteDetails.params}
basePath={basePath}
>
<foundRoute.component
params={foundRouteDetails.params}
{...serverProps}
/>
</RouteParamsProvider>
);
}

return null;
}

interface HydrogenRoute {
component: any;
path: string;
exact: boolean;
}

export function createPageRoutes(
pages: ImportGlobEagerOutput,
topLevelPath = '*',
dirPrefix: string | RegExp = ''
): HydrogenRoute[] {
const topLevelPrefix = topLevelPath.replace('*', '').replace(/\/$/, '');

const keys = Object.keys(pages);

const routes = keys
.map((key) => {
const path = extractPathFromRoutesKey(key, dirPrefix);

/**
* Catch-all routes [...handle].jsx don't need an exact match
* https://reactrouter.com/core/api/Route/exact-bool
*/
const exact = !/\[(?:[.]{3})(\w+?)\]/.test(key);

if (!pages[key].default && !pages[key].api) {
log?.warn(
`${key} doesn't export a default React component or an API function`
);
}

return {
path: topLevelPrefix + path,
component: pages[key].default,
exact,
};
})
.filter((route) => route.component);

/**
* Place static paths BEFORE dynamic paths to grant priority.
*/
return [
...routes.filter((route) => !route.path.includes(':')),
...routes.filter((route) => route.path.includes(':')),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {HelmetData as HeadData} from 'react-helmet-async';
import {RSC_PATHNAME} from '../../constants.js';
import type {SessionSyncApi} from '../session/session-types.js';
import {parseJSON} from '../../utilities/parse.js';
import type {RouteMatches} from '../../utilities/routes.js';

export type PreloadQueryEntry = {
key: QueryKey;
Expand Down Expand Up @@ -81,6 +82,7 @@ export class HydrogenRequest extends Request {
runtime?: RuntimeContext;
scopes: Map<string, Record<string, any>>;
localization?: LocalizationContextValue;
matchedRoutes?: RouteMatches;
[key: string]: any;
};

Expand Down
Loading