Skip to content

Commit 246d069

Browse files
authored
fix: i18n and dynamic routes (#275)
1 parent b1b27ec commit 246d069

File tree

8 files changed

+83
-10
lines changed

8 files changed

+83
-10
lines changed

lib/presets/custom/next/compute/default/handler/routing/index.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,16 @@ async function generateResponse(
6767
* @param {object} reqCtx Request Context object (contains all we need in to know regarding the request in order to handle it).
6868
* @param {object} config The processed Vercel build output config.
6969
* @param {object} output Vercel build output.
70+
* @param {Array} buildMetadata Information about the build to be used in the routing.
7071
* @returns {Response} An instance of the router.
7172
*/
72-
async function handleRequest(reqCtx, config, output) {
73-
const matcher = new RoutesMatcher(config.routes, output, reqCtx);
73+
async function handleRequest(reqCtx, config, output, buildMetadata) {
74+
const matcher = new RoutesMatcher(
75+
config.routes,
76+
output,
77+
reqCtx,
78+
buildMetadata,
79+
);
7480
const match = await findMatch(matcher);
7581

7682
return generateResponse(reqCtx, match, output);

lib/presets/custom/next/compute/default/handler/routing/pcre.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ function matchPCRE(expr, val, caseSensitive) {
2929
* @returns {string} The processed string with replaced parameters.
3030
*/
3131
function applyPCREMatches(rawStr, match, captureGroupKeys) {
32-
return rawStr.replace(/\$([a-zA-Z0-9]+)/g, (_, key) => {
32+
const rawPath = rawStr.replace(/\$([a-zA-Z0-9]+)/g, (_, key) => {
3333
const index = captureGroupKeys.indexOf(key);
34+
3435
// If the extracted key does not exist as a named capture group from the matcher, we can
3536
// reasonably assume it's a number and return the matched index. Fallback to an empty string.
3637
return (index === -1 ? match[parseInt(key, 10)] : match[index + 1]) || '';
3738
});
39+
// In some cases the path may have multiple slashes, so we need to normalize it.
40+
return rawPath.replace(/^(\/)+/, '/');
3841
}
3942

4043
export { applyPCREMatches, matchPCRE };

lib/presets/custom/next/compute/default/handler/routing/routes-matcher.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class RoutesMatcher {
2121
* @param {object} routes The processed Vercel build output config routes.
2222
* @param {object} output Vercel build output.
2323
* @param {object} reqCtx Request context object; request object, assets fetcher, and execution context.
24+
* @param {Array} buildMetadata Information about the build to be used in the routing.
2425
* @returns {void} The matched set of path, status, headers, and search params.
2526
*/
2627
constructor(
@@ -30,6 +31,7 @@ class RoutesMatcher {
3031
output,
3132
/** Request Context object for the request to match */
3233
reqCtx,
34+
buildMetadata,
3335
) {
3436
this.routes = routes;
3537
this.output = output;
@@ -47,6 +49,7 @@ class RoutesMatcher {
4749

4850
this.checkPhaseCounter = 0;
4951
this.middlewareInvoked = [];
52+
this.locales = new Set(buildMetadata);
5053
}
5154

5255
/**
@@ -238,7 +241,8 @@ class RoutesMatcher {
238241
* @returns {string }The previous path for the route before applying the destination.
239242
*/
240243
applyRouteDest(route, srcMatch, captureGroupKeys) {
241-
if (!route.dest) return this.path;
244+
const localesMatchesRoute = this.locales.has(route.dest?.replace('/', ''));
245+
if (!route.dest || localesMatchesRoute) return this.path;
242246

243247
const prevPath = this.path;
244248

@@ -476,6 +480,23 @@ class RoutesMatcher {
476480
return 'done';
477481
}
478482

483+
if (phase === 'none') {
484+
// applications using the Pages router with i18n plus a catch-all root route
485+
// redirect all requests (including /api/ ones) to the catch-all route, the only
486+
// way to prevent this erroneous behavior is to remove the locale here if the
487+
// path without the locale exists in the vercel build output
488+
// eslint-disable-next-line no-restricted-syntax
489+
for (const locale of this.locales) {
490+
const localeRegExp = new RegExp(`/${locale}(/.*)`);
491+
const match = this.path.match(localeRegExp);
492+
const pathWithoutLocale = match?.[1];
493+
if (pathWithoutLocale && pathWithoutLocale in this.output) {
494+
this.path = pathWithoutLocale;
495+
break;
496+
}
497+
}
498+
}
499+
479500
let pathExistsInOutput = this.path in this.output;
480501

481502
// If a path with a trailing slash entered the `rewrite` phase and didn't find a match, it might

lib/presets/custom/next/compute/default/prebuild/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { VercelUtils, feedback, getAbsoluteLibDirPath } from '#utils';
66
import { mapAndAdaptFunctions } from './mapping/index.js';
77
import { assetsPaths } from './mapping/assets.js';
88
import { processVercelOutput } from './mapping/process-mapping.js';
9+
import { getNextProjectConfig } from '../../utils/next.js';
910

1011
const { loadVercelConfigs } = VercelUtils;
1112

@@ -108,6 +109,9 @@ async function run(prebuildContext) {
108109
processedVercelOutput.vercelOutput,
109110
);
110111

112+
const nextProjectConfig = await getNextProjectConfig();
113+
const locales = nextProjectConfig?.i18n?.locales || [];
114+
111115
return {
112116
// onEntry
113117
filesToInject: [
@@ -128,6 +132,7 @@ async function run(prebuildContext) {
128132
// defineVars (bundlers - define)
129133
defineVars: {
130134
__CONFIG__: JSON.stringify(processedVercelOutput.vercelConfig),
135+
__BUILD_METADATA__: JSON.stringify(locales),
131136
},
132137
builderPlugins: [],
133138
};

lib/presets/custom/next/compute/default/prebuild/mapping/process-mapping.js

+42-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,46 @@ import { rmSync, statSync } from 'fs';
44
import { feedback } from '#utils';
55
import { addLeadingSlash, stripIndexRoute } from '../../../utils/routing.js';
66

7+
/**
8+
* Given a source route it normalizes its src value if needed.
9+
*
10+
* (In this context normalization means tweaking the src value so that it follows
11+
* a format which Vercel expects).
12+
* Note: this function applies the change side-effectfully to the route object.
13+
* @param {object} route Route which src we want to potentially normalize
14+
*/
15+
function normalizeRouteSrc(route) {
16+
if (!route.src) return;
17+
18+
// we rely on locale root routes pointing to '/' to perform runtime checks
19+
// so we cannot normalize such src values as that would break things later on
20+
// see: https://github.com/cloudflare/next-on-pages/blob/654545/packages/next-on-pages/templates/_worker.js/routes-matcher.ts#L353-L358
21+
if (route.locale && route.src === '/') return;
22+
23+
// Route src should always start with a '^'
24+
// see: https://github.com/vercel/vercel/blob/ea5bc88/packages/routing-utils/src/index.ts#L77
25+
if (!route.src.startsWith('^')) {
26+
// eslint-disable-next-line no-param-reassign
27+
route.src = `^${route.src}`;
28+
}
29+
30+
// Route src should always end with a '$'
31+
// see: https://github.com/vercel/vercel/blob/ea5bc88/packages/routing-utils/src/index.ts#L82
32+
if (!route.src.endsWith('$')) {
33+
// eslint-disable-next-line no-param-reassign
34+
route.src = `${route.src}$`;
35+
}
36+
}
37+
38+
/**
39+
* Check if a route is a Vercel handler.
40+
* @param {object} route - The route to check.
41+
* @returns {boolean} - Whether the route is a Vercel handler.
42+
*/
43+
export function isVercelHandler(route) {
44+
return 'handle' in route;
45+
}
46+
747
/**
848
* Process the Vercel config.
949
* @param {object} config - The Vercel config.
@@ -25,13 +65,13 @@ function processVercelConfig(config) {
2565

2666
let currentPhase = 'none';
2767
config.routes?.forEach((route) => {
28-
if ('handle' in route) {
68+
if (isVercelHandler(route)) {
2969
currentPhase = route.handle;
3070
} else {
71+
normalizeRouteSrc(route);
3172
processedConfig.routes[currentPhase].push(route);
3273
}
3374
});
34-
3575
return processedConfig;
3676
}
3777

lib/presets/custom/next/compute/default/prebuild/validation/support.js

-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ export async function validationSupportAndRetrieveFromVcConfig(
120120
throw new Error('No .vc-config.json files found');
121121

122122
vcConfigObjects = await modifyVcConfigObjects(framework, vcConfigObjects);
123-
124123
const runtimesConfig = [
125124
...new Set(vcConfigObjects.map((config) => config.content.runtime)),
126125
];

lib/presets/custom/next/compute/handler.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ async function main(request, env, ctx) {
3939
}
4040

4141
const adjustedRequest = adjustRequestForVercel(request);
42-
4342
return handleRequest(
4443
{
4544
request: adjustedRequest,
@@ -48,6 +47,7 @@ async function main(request, env, ctx) {
4847
},
4948
__CONFIG__,
5049
__BUILD_OUTPUT__,
50+
__BUILD_METADATA__,
5151
);
5252
});
5353
}

lib/presets/custom/next/compute/utils/next.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ async function getNextProjectConfig() {
2626
*/
2727
function isLocalePath(path, locales) {
2828
let isLangRoute = false;
29-
3029
if (locales && locales?.length > 0) {
3130
locales.forEach((lang) => {
32-
if (path.includes(`/${lang}/`)) {
31+
if (path.includes(`/${lang}/`) || path.includes(`/${lang}.func/`)) {
3332
isLangRoute = true;
3433
}
3534
});

0 commit comments

Comments
 (0)