From fc738b4c78fb795d71869508ab661777bd0a7e07 Mon Sep 17 00:00:00 2001 From: Tyler Spencewood Date: Sat, 7 Mar 2020 15:11:21 -0600 Subject: [PATCH 1/7] Update dynamic route regex for catch all --- lib/serverless-next.js/expressifyDynamicRoute.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/serverless-next.js/expressifyDynamicRoute.js b/lib/serverless-next.js/expressifyDynamicRoute.js index 8e8ac22..79ff685 100644 --- a/lib/serverless-next.js/expressifyDynamicRoute.js +++ b/lib/serverless-next.js/expressifyDynamicRoute.js @@ -1,7 +1,10 @@ // Copied from serverless-next.js (v1.8.0) // 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/ 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"); }; From 604ec77ee5a63eb02dbb163c2817977b26b9b019 Mon Sep 17 00:00:00 2001 From: Tyler Spencewood Date: Tue, 10 Mar 2020 13:22:41 -0500 Subject: [PATCH 2/7] Restore comment --- lib/serverless-next.js/expressifyDynamicRoute.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/serverless-next.js/expressifyDynamicRoute.js b/lib/serverless-next.js/expressifyDynamicRoute.js index 79ff685..6b006e7 100644 --- a/lib/serverless-next.js/expressifyDynamicRoute.js +++ b/lib/serverless-next.js/expressifyDynamicRoute.js @@ -1,6 +1,7 @@ // Copied from serverless-next.js (v1.8.0) // 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/ module.exports = dynamicRoute => { // replace any catch all group first let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, ":$1*"); From ea6db0141ef9c9a7e10b35b65afd9ebb7e58ed34 Mon Sep 17 00:00:00 2001 From: Tyler Spencewood Date: Tue, 10 Mar 2020 13:25:56 -0500 Subject: [PATCH 3/7] Remove catch-all limitation from readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9db8192..ebfe503 100644 --- a/README.md +++ b/README.md @@ -120,8 +120,6 @@ 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. From 156d214d2ee02f1d9291312997c95667c1c3ab8a Mon Sep 17 00:00:00 2001 From: Tyler Spencewood Date: Tue, 10 Mar 2020 13:28:36 -0500 Subject: [PATCH 4/7] Restore original comments --- lib/serverless-next.js/expressifyDynamicRoute.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/serverless-next.js/expressifyDynamicRoute.js b/lib/serverless-next.js/expressifyDynamicRoute.js index 6b006e7..60b3f46 100644 --- a/lib/serverless-next.js/expressifyDynamicRoute.js +++ b/lib/serverless-next.js/expressifyDynamicRoute.js @@ -1,7 +1,8 @@ // Copied from serverless-next.js (v1.8.0) // 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 => { // replace any catch all group first let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, ":$1*"); From 927eb67be78a0ae6baa5dab9bdfd2291fb92e6c9 Mon Sep 17 00:00:00 2001 From: Finn Woelm Date: Sun, 19 Apr 2020 15:17:27 +0200 Subject: [PATCH 5/7] serverless-next.js: Update files to v1.9.10 A new version of serverless-nextjs-component has been released. It introduces support for dynamic catch-all routes. Integrating these changes means that we can also support dynamic catch-all routes. --- .../expressifyDynamicRoute.js | 2 +- lib/serverless-next.js/isDynamicRoute.js | 2 +- lib/serverless-next.js/pathToRegexStr.js | 2 +- .../prepareBuildManifests.js | 3 +- lib/serverless-next.js/readPagesManifest.js | 2 +- lib/serverless-next.js/sortedRoutes.js | 114 +++++++++++++----- 6 files changed, 90 insertions(+), 35 deletions(-) diff --git a/lib/serverless-next.js/expressifyDynamicRoute.js b/lib/serverless-next.js/expressifyDynamicRoute.js index 60b3f46..4eeb64b 100644 --- a/lib/serverless-next.js/expressifyDynamicRoute.js +++ b/lib/serverless-next.js/expressifyDynamicRoute.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/expressifyDynamicRoute.js // converts a nextjs dynamic route /[param]/ -> /:param 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 From ca339e46be027f6a82d05fc0023770d80f35ba40 Mon Sep 17 00:00:00 2001 From: Finn Woelm Date: Sun, 19 Apr 2020 15:25:23 +0200 Subject: [PATCH 6/7] Add routing support for catch-all routes Create an adaptation of serverless-next.js' expressifyDynamicRoute function: getNetlifyRoute. Our getNetlifyRoute function differs from the expressifyDynamicRoute in that it converts catch-all routes from [...params] to /:* rather than to /:params*. The latter does not work with Netlify routing, but our version does. --- lib/collectNextjsPages.js | 9 +++++++-- lib/getNetlifyRoute.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 lib/getNetlifyRoute.js 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"); +}; From 1d7d3e3646e74984ae05494e625817f39c718c4c Mon Sep 17 00:00:00 2001 From: Finn Woelm Date: Sun, 19 Apr 2020 16:20:20 +0200 Subject: [PATCH 7/7] README: Add @spencewood and @alxhghs to contributor list Acknowledge their help with adding support for catch-all routes. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ebfe503..eb0626d 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,7 @@ next-on-netlify has only been tested on NextJS version 9 and above. ## 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)