diff --git a/README.md b/README.md
index 9db8192..eb0626d 100644
--- a/README.md
+++ b/README.md
@@ -120,8 +120,10 @@ Or you can define custom redirects in the `netlify.toml` file.
next-on-netlify has only been tested on NextJS version 9 and above.
-next-on-netlify does not yet support [catch-all routes](https://nextjs.org/blog/next-9-2#catch-all-dynamic-routes) (yet).
-
## Credits
📣 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.
+
+🙌 Big "thank you" to the following people for their contributions:
+- [@spencewood](https://github.com/spencewood)
+- [@alxhghs](https://github.com/alxhghs)
diff --git a/lib/collectNextjsPages.js b/lib/collectNextjsPages.js
index f2aa192..8d7a427 100644
--- a/lib/collectNextjsPages.js
+++ b/lib/collectNextjsPages.js
@@ -8,6 +8,7 @@
// - Structure is an array of objects (was an object of objects)
const readPagesManifest = require('./readPagesManifest')
+const getNetlifyRoute = require('./getNetlifyRoute')
const isDynamicRoute = require("./serverless-next.js/isDynamicRoute")
const expressifyDynamicRoute = require("./serverless-next.js/expressifyDynamicRoute")
const pathToRegexStr = require("./serverless-next.js/pathToRegexStr")
@@ -33,10 +34,14 @@ function collectNextjsPages() {
// Route to the page (/about)
// If the page is dynamic, use url segments: /posts/[id] --> /posts/:id
- page.route = page.isDynamic ? expressifyDynamicRoute(route) : route
+ page.route = page.isDynamic ? getNetlifyRoute(route) :route
// Regex for matching the page (/^\/about$/)
- page.regex = pathToRegexStr(page.route)
+ // NOTE: This route is different than the Netlify route set above!
+ // They are very similar, but not the same. See getNetlifyRoute.js for
+ // more information.
+ const _route = page.isDynamic ? expressifyDynamicRoute(route) : route
+ page.regex = pathToRegexStr(_route)
return page
})
diff --git a/lib/getNetlifyRoute.js b/lib/getNetlifyRoute.js
new file mode 100644
index 0000000..b63a902
--- /dev/null
+++ b/lib/getNetlifyRoute.js
@@ -0,0 +1,15 @@
+// Adapted from serverless-next.js (v1.9.10)
+// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/expressifyDynamicRoute.js
+// The original turns catch-all routes from /[...params] into /:params*
+// This adaptation turns catch-all routes from /[...params] into /:*
+// This is necessary for it to work with Netlify routing.
+
+// converts a nextjs dynamic route /[param]/ -> /:param
+// also handles catch all routes /[...param]/ -> /:*
+module.exports = dynamicRoute => {
+ // replace any catch all group first
+ let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, "*");
+
+ // now replace other dynamic route groups
+ return expressified.replace(/\[(.*?)]/g, ":$1");
+};
diff --git a/lib/serverless-next.js/expressifyDynamicRoute.js b/lib/serverless-next.js/expressifyDynamicRoute.js
index 8e8ac22..4eeb64b 100644
--- a/lib/serverless-next.js/expressifyDynamicRoute.js
+++ b/lib/serverless-next.js/expressifyDynamicRoute.js
@@ -1,7 +1,12 @@
-// Copied from serverless-next.js (v1.8.0)
+// Copied from serverless-next.js (v1.9.10)
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/expressifyDynamicRoute.js
-// converts a nextjs dynamic route /[param]/ to express style /:param/
+// converts a nextjs dynamic route /[param]/ -> /:param
+// also handles catch all routes /[...param]/ -> /:param*
module.exports = dynamicRoute => {
- return dynamicRoute.replace(/\[(?.*?)]/g, ":$");
+ // replace any catch all group first
+ let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, ":$1*");
+
+ // now replace other dynamic route groups
+ return expressified.replace(/\[(.*?)]/g, ":$1");
};
diff --git a/lib/serverless-next.js/isDynamicRoute.js b/lib/serverless-next.js/isDynamicRoute.js
index 1d3579d..bef5e9d 100644
--- a/lib/serverless-next.js/isDynamicRoute.js
+++ b/lib/serverless-next.js/isDynamicRoute.js
@@ -1,4 +1,4 @@
-// Copied from serverless-next.js (v1.8.0)
+// Copied from serverless-next.js (v1.9.10)
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/isDynamicRoute.js
module.exports = route => {
diff --git a/lib/serverless-next.js/pathToRegexStr.js b/lib/serverless-next.js/pathToRegexStr.js
index 327610a..d94f565 100644
--- a/lib/serverless-next.js/pathToRegexStr.js
+++ b/lib/serverless-next.js/pathToRegexStr.js
@@ -1,4 +1,4 @@
-// Copied from serverless-next.js (v1.8.0)
+// Copied from serverless-next.js (v1.9.10)
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/pathToRegexStr.js
const pathToRegexp = require("path-to-regexp");
diff --git a/lib/serverless-next.js/prepareBuildManifests.js b/lib/serverless-next.js/prepareBuildManifests.js
index d7f56c1..4bf1792 100644
--- a/lib/serverless-next.js/prepareBuildManifests.js
+++ b/lib/serverless-next.js/prepareBuildManifests.js
@@ -1,4 +1,4 @@
-// Copied from serverless-next.js (v1.8.0)
+// Copied from serverless-next.js (v1.9.10)
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/serverless.js
// This file is for reference purposes only.
@@ -37,7 +37,6 @@ async prepareBuildManifests(nextConfigPath) {
Object.entries(pagesManifest).forEach(([route, pageFile]) => {
const dynamicRoute = isDynamicRoute(route);
const expressRoute = dynamicRoute ? expressifyDynamicRoute(route) : null;
-
if (isHtmlPage(pageFile)) {
if (dynamicRoute) {
htmlPages.dynamic[expressRoute] = {
diff --git a/lib/serverless-next.js/readPagesManifest.js b/lib/serverless-next.js/readPagesManifest.js
index 3419794..2603f11 100644
--- a/lib/serverless-next.js/readPagesManifest.js
+++ b/lib/serverless-next.js/readPagesManifest.js
@@ -1,4 +1,4 @@
-// Original function from serverless-next.js (v1.8.0)
+// Original function from serverless-next.js (v1.9.10)
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/serverless.js
// This file is for reference purposes only.
diff --git a/lib/serverless-next.js/sortedRoutes.js b/lib/serverless-next.js/sortedRoutes.js
index 0189a03..6bea410 100644
--- a/lib/serverless-next.js/sortedRoutes.js
+++ b/lib/serverless-next.js/sortedRoutes.js
@@ -1,80 +1,134 @@
-// Copied from serverless-next.js (v1.8.0)
+// Copied from serverless-next.js (v1.9.10)
// https://github.com/danielcondemarin/serverless-next.js/blob/master/packages/serverless-nextjs-component/lib/sortedRoutes.js
-// This file Taken was from next.js repo
-// https://github.com/zeit/next.js/blob/820a9790baafd36f14a79cf416162e3263cb00c4/packages/next/next-server/lib/router/utils/sorted-routes.ts#L89
+/// This file taken was from next.js repo and converted to JS.
+// https://github.com/zeit/next.js/blob/canary/packages/next/next-server/lib/router/utils/sorted-routes.ts
class UrlNode {
constructor() {
this.placeholder = true;
this.children = new Map();
this.slugName = null;
+ this.restSlugName = null;
}
- hasSlug() {
- return this.slugName != null;
- }
+
insert(urlPath) {
- this._insert(urlPath.split("/").filter(Boolean));
+ this._insert(urlPath.split("/").filter(Boolean), [], false);
}
+
smoosh() {
return this._smoosh();
}
+
_smoosh(prefix = "/") {
const childrenPaths = [...this.children.keys()].sort();
- if (this.hasSlug()) {
+ if (this.slugName !== null) {
childrenPaths.splice(childrenPaths.indexOf("[]"), 1);
}
+ if (this.restSlugName !== null) {
+ childrenPaths.splice(childrenPaths.indexOf("[...]"), 1);
+ }
+
const routes = childrenPaths
.map(c => this.children.get(c)._smoosh(`${prefix}${c}/`))
.reduce((prev, curr) => [...prev, ...curr], []);
- if (this.hasSlug()) {
+
+ if (this.slugName !== null) {
routes.push(
...this.children.get("[]")._smoosh(`${prefix}[${this.slugName}]/`)
);
}
+
if (!this.placeholder) {
routes.unshift(prefix === "/" ? "/" : prefix.slice(0, -1));
}
+
+ if (this.restSlugName !== null) {
+ routes.push(
+ ...this.children
+ .get("[...]")
+ ._smoosh(`${prefix}[...${this.restSlugName}]/`)
+ );
+ }
+
return routes;
}
- _insert(urlPaths, slugNames = []) {
+
+ _insert(urlPaths, slugNames, isCatchAll) {
if (urlPaths.length === 0) {
this.placeholder = false;
return;
}
+
+ if (isCatchAll) {
+ throw new Error(`Catch-all must be the last part of the URL.`);
+ }
+
// The next segment in the urlPaths list
let nextSegment = urlPaths[0];
+
// Check if the segment matches `[something]`
if (nextSegment.startsWith("[") && nextSegment.endsWith("]")) {
// Strip `[` and `]`, leaving only `something`
- const slugName = nextSegment.slice(1, -1);
- // If the specific segment already has a slug but the slug is not `something`
- // This prevents collisions like:
- // pages/[post]/index.js
- // pages/[id]/index.js
- // Because currently multiple dynamic params on the same segment level are not supported
- if (this.hasSlug() && slugName !== this.slugName) {
- // TODO: This error seems to be confusing for users, needs an err.sh link, the description can be based on above comment.
- throw new Error(
- "You cannot use different slug names for the same dynamic path."
- );
+ let segmentName = nextSegment.slice(1, -1);
+ if (segmentName.startsWith("...")) {
+ segmentName = segmentName.substring(3);
+ isCatchAll = true;
}
- if (slugNames.indexOf(slugName) !== -1) {
+
+ if (segmentName.startsWith(".")) {
throw new Error(
- `You cannot have the same slug name "${slugName}" repeat within a single dynamic path`
+ `Segment names may not start with erroneous periods ('${segmentName}').`
);
}
- slugNames.push(slugName);
- // slugName is kept as it can only be one particular slugName
- this.slugName = slugName;
- // nextSegment is overwritten to [] so that it can later be sorted specifically
- nextSegment = "[]";
+
+ function handleSlug(previousSlug, nextSlug) {
+ if (previousSlug !== null) {
+ // If the specific segment already has a slug but the slug is not `something`
+ // This prevents collisions like:
+ // pages/[post]/index.js
+ // pages/[id]/index.js
+ // Because currently multiple dynamic params on the same segment level are not supported
+ if (previousSlug !== nextSlug) {
+ // TODO: This error seems to be confusing for users, needs an err.sh link, the description can be based on above comment.
+ throw new Error(
+ `You cannot use different slug names for the same dynamic path ('${previousSlug}' !== '${nextSlug}').`
+ );
+ }
+ }
+
+ if (slugNames.indexOf(nextSlug) !== -1) {
+ throw new Error(
+ `You cannot have the same slug name "${nextSlug}" repeat within a single dynamic path`
+ );
+ }
+
+ slugNames.push(nextSlug);
+ }
+
+ if (isCatchAll) {
+ handleSlug(this.restSlugName, segmentName);
+ // slugName is kept as it can only be one particular slugName
+ this.restSlugName = segmentName;
+ // nextSegment is overwritten to [] so that it can later be sorted specifically
+ nextSegment = "[...]";
+ } else {
+ handleSlug(this.slugName, segmentName);
+ // slugName is kept as it can only be one particular slugName
+ this.slugName = segmentName;
+ // nextSegment is overwritten to [] so that it can later be sorted specifically
+ nextSegment = "[]";
+ }
}
+
// If this UrlNode doesn't have the nextSegment yet we create a new child UrlNode
if (!this.children.has(nextSegment)) {
this.children.set(nextSegment, new UrlNode());
}
- this.children.get(nextSegment)._insert(urlPaths.slice(1), slugNames);
+
+ this.children
+ .get(nextSegment)
+ ._insert(urlPaths.slice(1), slugNames, isCatchAll);
}
}
@@ -82,6 +136,7 @@ module.exports = function getSortedRoutes(normalizedPages) {
// First the UrlNode is created, and every UrlNode can have only 1 dynamic segment
// Eg you can't have pages/[post]/abc.js and pages/[hello]/something-else.js
// Only 1 dynamic segment per nesting level
+
// So in the case that is test/integration/dynamic-routing it'll be this:
// pages/[post]/comments.js
// pages/blog/[post]/comment/[id].js
@@ -90,6 +145,7 @@ module.exports = function getSortedRoutes(normalizedPages) {
// And since your PR passed through `slugName` as an array basically it'd including it in too many possibilities
// Instead what has to be passed through is the upwards path's dynamic names
const root = new UrlNode();
+
// Here the `root` gets injected multiple paths, and insert will break them up into sublevels
normalizedPages.forEach(pagePath => root.insert(pagePath));
// Smoosh will then sort those sublevels up to the point where you get the correct route definition priority