Skip to content
This repository was archived by the owner on May 10, 2021. It is now read-only.

Commit 4aad3df

Browse files
committed
Merge branch 'catch-all-routes'
Add support for catch all routes (introduced in NextJS v9.2): https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes Thanks to @spencewood and @alxhghs for their contributions!
2 parents e8adac6 + 1d7d3e3 commit 4aad3df

9 files changed

+123
-41
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ Or you can define custom redirects in the `netlify.toml` file.
120120

121121
next-on-netlify has only been tested on NextJS version 9 and above.
122122

123-
next-on-netlify does not yet support [catch-all routes](https://nextjs.org/blog/next-9-2#catch-all-dynamic-routes) (yet).
124-
125123
## Credits
126124

127125
📣 Shoutout to [@mottox2](https://github.com/mottox2) (a pioneer of hosting NextJS on Netlify) and [@danielcondemarin](https://github.com/danielcondemarin) (author of serverless-next.js for AWS). The two were big inspirations for this package.
126+
127+
🙌 Big "thank you" to the following people for their contributions:
128+
- [@spencewood](https://github.com/spencewood)
129+
- [@alxhghs](https://github.com/alxhghs)

lib/collectNextjsPages.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// - Structure is an array of objects (was an object of objects)
99

1010
const readPagesManifest = require('./readPagesManifest')
11+
const getNetlifyRoute = require('./getNetlifyRoute')
1112
const isDynamicRoute = require("./serverless-next.js/isDynamicRoute")
1213
const expressifyDynamicRoute = require("./serverless-next.js/expressifyDynamicRoute")
1314
const pathToRegexStr = require("./serverless-next.js/pathToRegexStr")
@@ -33,10 +34,14 @@ function collectNextjsPages() {
3334

3435
// Route to the page (/about)
3536
// If the page is dynamic, use url segments: /posts/[id] --> /posts/:id
36-
page.route = page.isDynamic ? expressifyDynamicRoute(route) : route
37+
page.route = page.isDynamic ? getNetlifyRoute(route) :route
3738

3839
// Regex for matching the page (/^\/about$/)
39-
page.regex = pathToRegexStr(page.route)
40+
// NOTE: This route is different than the Netlify route set above!
41+
// They are very similar, but not the same. See getNetlifyRoute.js for
42+
// more information.
43+
const _route = page.isDynamic ? expressifyDynamicRoute(route) : route
44+
page.regex = pathToRegexStr(_route)
4045

4146
return page
4247
})

lib/getNetlifyRoute.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Adapted from serverless-next.js (v1.9.10)
2+
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/expressifyDynamicRoute.js
3+
// The original turns catch-all routes from /[...params] into /:params*
4+
// This adaptation turns catch-all routes from /[...params] into /:*
5+
// This is necessary for it to work with Netlify routing.
6+
7+
// converts a nextjs dynamic route /[param]/ -> /:param
8+
// also handles catch all routes /[...param]/ -> /:*
9+
module.exports = dynamicRoute => {
10+
// replace any catch all group first
11+
let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, "*");
12+
13+
// now replace other dynamic route groups
14+
return expressified.replace(/\[(.*?)]/g, ":$1");
15+
};
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
// Copied from serverless-next.js (v1.8.0)
1+
// Copied from serverless-next.js (v1.9.10)
22
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/expressifyDynamicRoute.js
33

4-
// converts a nextjs dynamic route /[param]/ to express style /:param/
4+
// converts a nextjs dynamic route /[param]/ -> /:param
5+
// also handles catch all routes /[...param]/ -> /:param*
56
module.exports = dynamicRoute => {
6-
return dynamicRoute.replace(/\[(?<param>.*?)]/g, ":$<param>");
7+
// replace any catch all group first
8+
let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, ":$1*");
9+
10+
// now replace other dynamic route groups
11+
return expressified.replace(/\[(.*?)]/g, ":$1");
712
};

lib/serverless-next.js/isDynamicRoute.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copied from serverless-next.js (v1.8.0)
1+
// Copied from serverless-next.js (v1.9.10)
22
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/isDynamicRoute.js
33

44
module.exports = route => {

lib/serverless-next.js/pathToRegexStr.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copied from serverless-next.js (v1.8.0)
1+
// Copied from serverless-next.js (v1.9.10)
22
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/pathToRegexStr.js
33

44
const pathToRegexp = require("path-to-regexp");

lib/serverless-next.js/prepareBuildManifests.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copied from serverless-next.js (v1.8.0)
1+
// Copied from serverless-next.js (v1.9.10)
22
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/serverless.js
33
// This file is for reference purposes only.
44

@@ -37,7 +37,6 @@ async prepareBuildManifests(nextConfigPath) {
3737
Object.entries(pagesManifest).forEach(([route, pageFile]) => {
3838
const dynamicRoute = isDynamicRoute(route);
3939
const expressRoute = dynamicRoute ? expressifyDynamicRoute(route) : null;
40-
4140
if (isHtmlPage(pageFile)) {
4241
if (dynamicRoute) {
4342
htmlPages.dynamic[expressRoute] = {

lib/serverless-next.js/readPagesManifest.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Original function from serverless-next.js (v1.8.0)
1+
// Original function from serverless-next.js (v1.9.10)
22
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/serverless.js
33
// This file is for reference purposes only.
44

lib/serverless-next.js/sortedRoutes.js

+85-29
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,142 @@
1-
// Copied from serverless-next.js (v1.8.0)
1+
// Copied from serverless-next.js (v1.9.10)
22
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/sortedRoutes.js
33

4-
// This file Taken was from next.js repo
5-
// https://github.com/zeit/next.js/blob/820a9790baafd36f14a79cf416162e3263cb00c4/packages/next/next-server/lib/router/utils/sorted-routes.ts#L89
4+
/// This file taken was from next.js repo and converted to JS.
5+
// https://github.com/zeit/next.js/blob/canary/packages/next/next-server/lib/router/utils/sorted-routes.ts
66

77
class UrlNode {
88
constructor() {
99
this.placeholder = true;
1010
this.children = new Map();
1111
this.slugName = null;
12+
this.restSlugName = null;
1213
}
13-
hasSlug() {
14-
return this.slugName != null;
15-
}
14+
1615
insert(urlPath) {
17-
this._insert(urlPath.split("/").filter(Boolean));
16+
this._insert(urlPath.split("/").filter(Boolean), [], false);
1817
}
18+
1919
smoosh() {
2020
return this._smoosh();
2121
}
22+
2223
_smoosh(prefix = "/") {
2324
const childrenPaths = [...this.children.keys()].sort();
24-
if (this.hasSlug()) {
25+
if (this.slugName !== null) {
2526
childrenPaths.splice(childrenPaths.indexOf("[]"), 1);
2627
}
28+
if (this.restSlugName !== null) {
29+
childrenPaths.splice(childrenPaths.indexOf("[...]"), 1);
30+
}
31+
2732
const routes = childrenPaths
2833
.map(c => this.children.get(c)._smoosh(`${prefix}${c}/`))
2934
.reduce((prev, curr) => [...prev, ...curr], []);
30-
if (this.hasSlug()) {
35+
36+
if (this.slugName !== null) {
3137
routes.push(
3238
...this.children.get("[]")._smoosh(`${prefix}[${this.slugName}]/`)
3339
);
3440
}
41+
3542
if (!this.placeholder) {
3643
routes.unshift(prefix === "/" ? "/" : prefix.slice(0, -1));
3744
}
45+
46+
if (this.restSlugName !== null) {
47+
routes.push(
48+
...this.children
49+
.get("[...]")
50+
._smoosh(`${prefix}[...${this.restSlugName}]/`)
51+
);
52+
}
53+
3854
return routes;
3955
}
40-
_insert(urlPaths, slugNames = []) {
56+
57+
_insert(urlPaths, slugNames, isCatchAll) {
4158
if (urlPaths.length === 0) {
4259
this.placeholder = false;
4360
return;
4461
}
62+
63+
if (isCatchAll) {
64+
throw new Error(`Catch-all must be the last part of the URL.`);
65+
}
66+
4567
// The next segment in the urlPaths list
4668
let nextSegment = urlPaths[0];
69+
4770
// Check if the segment matches `[something]`
4871
if (nextSegment.startsWith("[") && nextSegment.endsWith("]")) {
4972
// Strip `[` and `]`, leaving only `something`
50-
const slugName = nextSegment.slice(1, -1);
51-
// If the specific segment already has a slug but the slug is not `something`
52-
// This prevents collisions like:
53-
// pages/[post]/index.js
54-
// pages/[id]/index.js
55-
// Because currently multiple dynamic params on the same segment level are not supported
56-
if (this.hasSlug() && slugName !== this.slugName) {
57-
// TODO: This error seems to be confusing for users, needs an err.sh link, the description can be based on above comment.
58-
throw new Error(
59-
"You cannot use different slug names for the same dynamic path."
60-
);
73+
let segmentName = nextSegment.slice(1, -1);
74+
if (segmentName.startsWith("...")) {
75+
segmentName = segmentName.substring(3);
76+
isCatchAll = true;
6177
}
62-
if (slugNames.indexOf(slugName) !== -1) {
78+
79+
if (segmentName.startsWith(".")) {
6380
throw new Error(
64-
`You cannot have the same slug name "${slugName}" repeat within a single dynamic path`
81+
`Segment names may not start with erroneous periods ('${segmentName}').`
6582
);
6683
}
67-
slugNames.push(slugName);
68-
// slugName is kept as it can only be one particular slugName
69-
this.slugName = slugName;
70-
// nextSegment is overwritten to [] so that it can later be sorted specifically
71-
nextSegment = "[]";
84+
85+
function handleSlug(previousSlug, nextSlug) {
86+
if (previousSlug !== null) {
87+
// If the specific segment already has a slug but the slug is not `something`
88+
// This prevents collisions like:
89+
// pages/[post]/index.js
90+
// pages/[id]/index.js
91+
// Because currently multiple dynamic params on the same segment level are not supported
92+
if (previousSlug !== nextSlug) {
93+
// TODO: This error seems to be confusing for users, needs an err.sh link, the description can be based on above comment.
94+
throw new Error(
95+
`You cannot use different slug names for the same dynamic path ('${previousSlug}' !== '${nextSlug}').`
96+
);
97+
}
98+
}
99+
100+
if (slugNames.indexOf(nextSlug) !== -1) {
101+
throw new Error(
102+
`You cannot have the same slug name "${nextSlug}" repeat within a single dynamic path`
103+
);
104+
}
105+
106+
slugNames.push(nextSlug);
107+
}
108+
109+
if (isCatchAll) {
110+
handleSlug(this.restSlugName, segmentName);
111+
// slugName is kept as it can only be one particular slugName
112+
this.restSlugName = segmentName;
113+
// nextSegment is overwritten to [] so that it can later be sorted specifically
114+
nextSegment = "[...]";
115+
} else {
116+
handleSlug(this.slugName, segmentName);
117+
// slugName is kept as it can only be one particular slugName
118+
this.slugName = segmentName;
119+
// nextSegment is overwritten to [] so that it can later be sorted specifically
120+
nextSegment = "[]";
121+
}
72122
}
123+
73124
// If this UrlNode doesn't have the nextSegment yet we create a new child UrlNode
74125
if (!this.children.has(nextSegment)) {
75126
this.children.set(nextSegment, new UrlNode());
76127
}
77-
this.children.get(nextSegment)._insert(urlPaths.slice(1), slugNames);
128+
129+
this.children
130+
.get(nextSegment)
131+
._insert(urlPaths.slice(1), slugNames, isCatchAll);
78132
}
79133
}
80134

81135
module.exports = function getSortedRoutes(normalizedPages) {
82136
// First the UrlNode is created, and every UrlNode can have only 1 dynamic segment
83137
// Eg you can't have pages/[post]/abc.js and pages/[hello]/something-else.js
84138
// Only 1 dynamic segment per nesting level
139+
85140
// So in the case that is test/integration/dynamic-routing it'll be this:
86141
// pages/[post]/comments.js
87142
// pages/blog/[post]/comment/[id].js
@@ -90,6 +145,7 @@ module.exports = function getSortedRoutes(normalizedPages) {
90145
// And since your PR passed through `slugName` as an array basically it'd including it in too many possibilities
91146
// Instead what has to be passed through is the upwards path's dynamic names
92147
const root = new UrlNode();
148+
93149
// Here the `root` gets injected multiple paths, and insert will break them up into sublevels
94150
normalizedPages.forEach(pagePath => root.insert(pagePath));
95151
// Smoosh will then sort those sublevels up to the point where you get the correct route definition priority

0 commit comments

Comments
 (0)