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

Catch-all routes (reopening of PR#1) #5

Merged
merged 7 commits into from
Apr 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
9 changes: 7 additions & 2 deletions lib/collectNextjsPages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
})
Expand Down
15 changes: 15 additions & 0 deletions lib/getNetlifyRoute.js
Original file line number Diff line number Diff line change
@@ -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");
};
11 changes: 8 additions & 3 deletions lib/serverless-next.js/expressifyDynamicRoute.js
Original file line number Diff line number Diff line change
@@ -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(/\[(?<param>.*?)]/g, ":$<param>");
// replace any catch all group first
let expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, ":$1*");

// now replace other dynamic route groups
return expressified.replace(/\[(.*?)]/g, ":$1");
};
2 changes: 1 addition & 1 deletion lib/serverless-next.js/isDynamicRoute.js
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion lib/serverless-next.js/pathToRegexStr.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
3 changes: 1 addition & 2 deletions lib/serverless-next.js/prepareBuildManifests.js
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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] = {
Expand Down
2 changes: 1 addition & 1 deletion lib/serverless-next.js/readPagesManifest.js
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
114 changes: 85 additions & 29 deletions lib/serverless-next.js/sortedRoutes.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,142 @@
// 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);
}
}

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
Expand All @@ -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
Expand Down