From d9fae6faafa9b1d2cf5ae9e202375671eb302934 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 27 Apr 2020 12:32:04 -0500 Subject: [PATCH 1/2] Add namedRegex and routeKeys to routes manifest --- packages/next/build/index.ts | 54 +++++++++++++------ .../lib/router/utils/route-regex.ts | 38 +++++++++++-- .../dynamic-routing/pages/index.js | 8 +-- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index a05cc889f778f..751160a680ef3 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -299,10 +299,15 @@ export default async function build(dir: string, conf = null): Promise { redirects: redirects.map(r => buildCustomRoute(r, 'redirect')), rewrites: rewrites.map(r => buildCustomRoute(r, 'rewrite')), headers: headers.map(r => buildCustomRoute(r, 'header')), - dynamicRoutes: getSortedRoutes(dynamicRoutes).map(page => ({ - page, - regex: getRouteRegex(page).re.source, - })), + dynamicRoutes: getSortedRoutes(dynamicRoutes).map(page => { + const routeRegex = getRouteRegex(page) + return { + page, + regex: routeRegex.re.source, + namedRegex: routeRegex.namedRegex, + routeKeys: Object.keys(routeRegex.groups), + } + }), } await mkdir(distDir, { recursive: true }) @@ -633,20 +638,37 @@ export default async function build(dir: string, conf = null): Promise { `${pagePath}.json` ) + const routeKeys = [] + let dataRouteRegex: string + let namedDataRouteRegex: string | undefined + + if (isDynamicRoute(page)) { + const routeRegex = getRouteRegex(dataRoute.replace(/\.json$/, '')) + + dataRouteRegex = routeRegex.re.source.replace( + /\(\?:\\\/\)\?\$$/, + '\\.json$' + ) + namedDataRouteRegex = routeRegex.namedRegex!.replace( + /\(\?:\/\)\?\$$/, + '\\.json$' + ) + routeKeys.push(...Object.keys(routeRegex.groups)) + } else { + dataRouteRegex = new RegExp( + `^${path.posix.join( + '/_next/data', + escapeStringRegexp(buildId), + `${pagePath}.json` + )}$` + ).source + } + return { page, - dataRouteRegex: isDynamicRoute(page) - ? getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace( - /\(\?:\\\/\)\?\$$/, - '\\.json$' - ) - : new RegExp( - `^${path.posix.join( - '/_next/data', - escapeStringRegexp(buildId), - `${pagePath}.json` - )}$` - ).source, + routeKeys, + dataRouteRegex, + namedDataRouteRegex, } }) diff --git a/packages/next/next-server/lib/router/utils/route-regex.ts b/packages/next/next-server/lib/router/utils/route-regex.ts index 7502f1e87234f..61e4896020bee 100644 --- a/packages/next/next-server/lib/router/utils/route-regex.ts +++ b/packages/next/next-server/lib/router/utils/route-regex.ts @@ -1,14 +1,18 @@ +// this isn't importing the escape-string-regex module +// to reduce bytes +function escapeRegex(str: string) { + return str.replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&') +} + export function getRouteRegex( normalizedRoute: string ): { re: RegExp + namedRegex?: string groups: { [groupName: string]: { pos: number; repeat: boolean } } } { // Escape all characters that could be considered RegEx - const escapedRoute = (normalizedRoute.replace(/\/$/, '') || '/').replace( - /[|\\{}()[\]^$+*?.-]/g, - '\\$&' - ) + const escapedRoute = escapeRegex(normalizedRoute.replace(/\/$/, '') || '/') const groups: { [groupName: string]: { pos: number; repeat: boolean } } = {} let groupIndex = 1 @@ -28,8 +32,34 @@ export function getRouteRegex( } ) + let namedParameterizedRoute: string | undefined + + // dead code eliminate for browser since it's only needed + // while generating routes-manifest + if (typeof window === 'undefined') { + namedParameterizedRoute = escapedRoute.replace( + /\/\\\[([^/]+?)\\\](?=\/|$)/g, + (_, $1) => { + const isCatchAll = /^(\\\.){3}/.test($1) + const key = $1 + // Un-escape key + .replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1') + .replace(/^\.{3}/, '') + + return isCatchAll + ? `/(?<${escapeRegex(key)}>.+?)` + : `/(?<${escapeRegex(key)}>[^/]+?)` + } + ) + } + return { re: new RegExp('^' + parameterizedRoute + '(?:/)?$', 'i'), groups, + ...(namedParameterizedRoute + ? { + namedRegex: `^${namedParameterizedRoute}(?:/)?$`, + } + : {}), } } diff --git a/test/integration/dynamic-routing/pages/index.js b/test/integration/dynamic-routing/pages/index.js index 793621998daf9..59591a000c52a 100644 --- a/test/integration/dynamic-routing/pages/index.js +++ b/test/integration/dynamic-routing/pages/index.js @@ -3,15 +3,15 @@ import Link from 'next/link' const Page = () => (

My blog

- + View post 1
- + View post 1 comments
- + View comment 1 on post 1
@@ -19,7 +19,7 @@ const Page = () => ( View comment 123 on blog post 321
- + View post 1 with query
From 16ce7133fe4b5ddf6a5696ec7c5cf910f80817be Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 27 Apr 2020 13:15:16 -0500 Subject: [PATCH 2/2] Update tests --- packages/next/build/index.ts | 4 +-- .../custom-routes/test/index.test.js | 6 ++++ .../dynamic-routing/test/index.test.js | 32 +++++++++++++++++++ .../getserversideprops/test/index.test.js | 16 ++++++++++ test/integration/prerender/test/index.test.js | 24 ++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 751160a680ef3..f25ee7a1ca90e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -638,8 +638,8 @@ export default async function build(dir: string, conf = null): Promise { `${pagePath}.json` ) - const routeKeys = [] let dataRouteRegex: string + let routeKeys: string[] | undefined let namedDataRouteRegex: string | undefined if (isDynamicRoute(page)) { @@ -653,7 +653,7 @@ export default async function build(dir: string, conf = null): Promise { /\(\?:\/\)\?\$$/, '\\.json$' ) - routeKeys.push(...Object.keys(routeRegex.groups)) + routeKeys = Object.keys(routeRegex.groups) } else { dataRouteRegex = new RegExp( `^${path.posix.join( diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 0e0f920dafbed..b2194abb3184f 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -773,16 +773,22 @@ const runTests = (isDev = false) => { ], dynamicRoutes: [ { + namedRegex: '^/another/(?[^/]+?)(?:/)?$', page: '/another/[id]', regex: normalizeRegEx('^\\/another\\/([^\\/]+?)(?:\\/)?$'), + routeKeys: ['id'], }, { + namedRegex: '^/api/dynamic/(?[^/]+?)(?:/)?$', page: '/api/dynamic/[slug]', regex: normalizeRegEx('^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$'), + routeKeys: ['slug'], }, { + namedRegex: '^/blog/(?[^/]+?)(?:/)?$', page: '/blog/[post]', regex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'), + routeKeys: ['post'], }, ], }) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index b9c25c0fe743b..c1666f5b24d52 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -532,78 +532,110 @@ function runTests(dev) { redirects: [], dataRoutes: [ { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/p1/p2/all\\-ssg/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/p1\\/p2\\/all\\-ssg\\/(.+?)\\.json$` ), page: '/p1/p2/all-ssg/[...rest]', + routeKeys: ['rest'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/p1/p2/nested\\-all\\-ssg/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/p1\\/p2\\/nested\\-all\\-ssg\\/(.+?)\\.json$` ), page: '/p1/p2/nested-all-ssg/[...rest]', + routeKeys: ['rest'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/p1/p2/predefined\\-ssg/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/p1\\/p2\\/predefined\\-ssg\\/(.+?)\\.json$` ), page: '/p1/p2/predefined-ssg/[...rest]', + routeKeys: ['rest'], }, ], dynamicRoutes: [ { + namedRegex: `^/blog/(?[^/]+?)/comment/(?[^/]+?)(?:/)?$`, page: '/blog/[name]/comment/[id]', regex: normalizeRegEx( '^\\/blog\\/([^\\/]+?)\\/comment\\/([^\\/]+?)(?:\\/)?$' ), + routeKeys: ['name', 'id'], }, { + namedRegex: `^/on\\-mount/(?[^/]+?)(?:/)?$`, page: '/on-mount/[post]', regex: normalizeRegEx('^\\/on\\-mount\\/([^\\/]+?)(?:\\/)?$'), + routeKeys: ['post'], }, { + namedRegex: `^/p1/p2/all\\-ssg/(?.+?)(?:/)?$`, page: '/p1/p2/all-ssg/[...rest]', regex: normalizeRegEx('^\\/p1\\/p2\\/all\\-ssg\\/(.+?)(?:\\/)?$'), + routeKeys: ['rest'], }, { + namedRegex: `^/p1/p2/all\\-ssr/(?.+?)(?:/)?$`, page: '/p1/p2/all-ssr/[...rest]', regex: normalizeRegEx('^\\/p1\\/p2\\/all\\-ssr\\/(.+?)(?:\\/)?$'), + routeKeys: ['rest'], }, { + namedRegex: `^/p1/p2/nested\\-all\\-ssg/(?.+?)(?:/)?$`, page: '/p1/p2/nested-all-ssg/[...rest]', regex: normalizeRegEx( '^\\/p1\\/p2\\/nested\\-all\\-ssg\\/(.+?)(?:\\/)?$' ), + routeKeys: ['rest'], }, { + namedRegex: `^/p1/p2/predefined\\-ssg/(?.+?)(?:/)?$`, page: '/p1/p2/predefined-ssg/[...rest]', regex: normalizeRegEx( '^\\/p1\\/p2\\/predefined\\-ssg\\/(.+?)(?:\\/)?$' ), + routeKeys: ['rest'], }, { + namedRegex: `^/(?[^/]+?)(?:/)?$`, page: '/[name]', regex: normalizeRegEx('^\\/([^\\/]+?)(?:\\/)?$'), + routeKeys: ['name'], }, { + namedRegex: `^/(?[^/]+?)/comments(?:/)?$`, page: '/[name]/comments', regex: normalizeRegEx('^\\/([^\\/]+?)\\/comments(?:\\/)?$'), + routeKeys: ['name'], }, { + namedRegex: `^/(?[^/]+?)/on\\-mount\\-redir(?:/)?$`, page: '/[name]/on-mount-redir', regex: normalizeRegEx( '^\\/([^\\/]+?)\\/on\\-mount\\-redir(?:\\/)?$' ), + routeKeys: ['name'], }, { + namedRegex: `^/(?[^/]+?)/(?[^/]+?)(?:/)?$`, page: '/[name]/[comment]', regex: normalizeRegEx('^\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$'), + routeKeys: ['name', 'comment'], }, ], }) diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js index 3874978278576..1c49b8783fb4d 100644 --- a/test/integration/getserversideprops/test/index.test.js +++ b/test/integration/getserversideprops/test/index.test.js @@ -47,24 +47,36 @@ const expectedManifestRoutes = () => [ page: '/blog', }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/blog/(?[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog\\/([^\\/]+?)\\.json$` ), page: '/blog/[post]', + routeKeys: ['post'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/blog/(?[^/]+?)/(?[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$` ), page: '/blog/[post]/[comment]', + routeKeys: ['post', 'comment'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/catchall/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/catchall\\/(.+?)\\.json$` ), page: '/catchall/[...path]', + routeKeys: ['path'], }, { dataRouteRegex: normalizeRegEx( @@ -103,12 +115,16 @@ const expectedManifestRoutes = () => [ page: '/something', }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/user/(?[^/]+?)/profile\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/user\\/([^\\/]+?)\\/profile\\.json$` ), page: '/user/[user]/profile', + routeKeys: ['user'], }, ] diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 923c2d65558b9..1f01d66801198 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -771,36 +771,52 @@ const runTests = (dev = false, looseMode = false) => { page: '/blog', }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/blog/(?[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/blog\\/([^\\/]+?)\\.json$` ), page: '/blog/[post]', + routeKeys: ['post'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/blog/(?[^/]+?)/(?[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$` ), page: '/blog/[post]/[comment]', + routeKeys: ['post', 'comment'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/catchall/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/catchall\\/(.+?)\\.json$` ), page: '/catchall/[...slug]', + routeKeys: ['slug'], }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/catchall\\-explicit/(?.+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/catchall\\-explicit\\/(.+?)\\.json$` ), page: '/catchall-explicit/[...slug]', + routeKeys: ['slug'], }, { dataRouteRegex: normalizeRegEx( @@ -811,12 +827,16 @@ const runTests = (dev = false, looseMode = false) => { page: '/default-revalidate', }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/non\\-json/(?

[^/]+?)\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/non\\-json\\/([^\\/]+?)\\.json$` ), page: '/non-json/[p]', + routeKeys: ['p'], }, { dataRouteRegex: normalizeRegEx( @@ -825,12 +845,16 @@ const runTests = (dev = false, looseMode = false) => { page: '/something', }, { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/user/(?[^/]+?)/profile\\.json$`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( buildId )}\\/user\\/([^\\/]+?)\\/profile\\.json$` ), page: '/user/[user]/profile', + routeKeys: ['user'], }, ]) })