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