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

Commit 463e252

Browse files
committed
Merge branch 'pull-15-optional-catch-all-routes'
Next.js added a new, experimental, [optional catch-all routes]( https://nextjs.org/docs/api-routes/dynamic-api-routes#optional-catch-all-api-routes ), this adds support for it. See: #15
2 parents 479b7e7 + 85ec0f2 commit 463e252

File tree

11 files changed

+358
-6
lines changed

11 files changed

+358
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
target: 'serverless',
3+
experimental: {
4+
optionalCatchAll: true
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useRouter } from 'next/router'
2+
import Link from 'next/link'
3+
4+
const CatchAll = ({ show }) => {
5+
const router = useRouter()
6+
7+
// This is never shown on Netlify. We just need it for NextJS to be happy,
8+
// because NextJS will render a fallback HTML page.
9+
if (router.isFallback) {
10+
return <div>Loading...</div>
11+
}
12+
13+
return (
14+
<div>
15+
<p>
16+
This page uses getStaticProps() to pre-fetch a TV show.
17+
</p>
18+
19+
<hr/>
20+
21+
<h1>Show #{show.id}</h1>
22+
<p>
23+
{show.name}
24+
</p>
25+
26+
<hr/>
27+
28+
<Link href="/">
29+
<a>Go back home</a>
30+
</Link>
31+
</div>
32+
)
33+
}
34+
35+
export async function getServerSideProps({ params }) {
36+
// The ID to render
37+
const { all } = params
38+
const id = all ? all[0] : 1
39+
40+
const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
41+
const data = await res.json();
42+
43+
return {
44+
props: {
45+
show: data
46+
}
47+
}
48+
}
49+
50+
export default CatchAll
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Link from 'next/link'
2+
3+
const Index = () => (
4+
<div>
5+
<ul>
6+
<li>
7+
<Link href="/catch/[[...all]]" as="/catch">
8+
<a>/catch</a>
9+
</Link>
10+
</li>
11+
<li>
12+
<Link href="/catch/[[...all]]" as="/catch/25/catch/all">
13+
<a>/catch/25/catch/all</a>
14+
</Link>
15+
</li>
16+
<li>
17+
<Link href="/catch/[[...all]]" as="/catch/75/undefined/path/test">
18+
<a>/catch/75/undefined/path/test</a>
19+
</Link>
20+
</li>
21+
</ul>
22+
</div>
23+
)
24+
25+
export default Index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const project = "optionalCatchAll"
2+
3+
before(() => {
4+
// When changing the base URL within a spec file, Cypress runs the spec twice
5+
// To avoid rebuilding and redeployment on the second run, we check if the
6+
// project has already been deployed.
7+
cy.task('isDeployed').then(isDeployed => {
8+
// Cancel setup, if already deployed
9+
if(isDeployed)
10+
return
11+
12+
// Clear project folder
13+
cy.task('clearProject', { project })
14+
15+
// Copy NextJS files
16+
cy.task('copyFixture', {
17+
project, from: 'pages-with-optionalCatchAll', to: 'pages'
18+
})
19+
cy.task('copyFixture', {
20+
project, from: 'next.config.js-with-optionalCatchAll', to: 'next.config.js'
21+
})
22+
23+
// Copy package.json file
24+
cy.task('copyFixture', {
25+
project, from: 'package.json', to: 'package.json'
26+
})
27+
28+
// Copy Netlify settings
29+
cy.task('copyFixture', {
30+
project, from: 'netlify.toml', to: 'netlify.toml'
31+
})
32+
cy.task('copyFixture', {
33+
project, from: '.netlify', to: '.netlify'
34+
})
35+
36+
// Build
37+
cy.task('buildProject', { project })
38+
39+
// Deploy
40+
cy.task('deployProject', { project }, { timeout: 180 * 1000 })
41+
})
42+
43+
// Set base URL
44+
cy.task('getBaseUrl', { project }).then((url) => {
45+
Cypress.config('baseUrl', url)
46+
})
47+
})
48+
49+
after(() => {
50+
// While the before hook runs twice (it's re-run when the base URL changes),
51+
// the after hook only runs once.
52+
cy.task('clearDeployment')
53+
})
54+
55+
describe('Page with optional catch all routing', () => {
56+
it('responds to base path', () => {
57+
cy.visit('/catch')
58+
59+
cy.get('h1').should('contain', 'Show #1')
60+
cy.get('p').should('contain', 'Under the Dome')
61+
})
62+
63+
it('responds to catch-all path', () => {
64+
cy.visit('/catch/25/catch/all')
65+
66+
cy.get('h1').should('contain', 'Show #25')
67+
cy.get('p').should('contain', 'Hellsing')
68+
})
69+
70+
it('loads page props from data .json file when navigating to it', () => {
71+
cy.visit('/')
72+
cy.window().then(w => w.noReload = true)
73+
74+
// Navigate to page and test that no reload is performed
75+
// See: https://glebbahmutov.com/blog/detect-page-reload/
76+
cy.contains('/catch').click()
77+
cy.get('h1').should('contain', 'Show #1')
78+
cy.get('p').should('contain', 'Under the Dome')
79+
80+
cy.contains('Go back home').click()
81+
cy.contains('/catch/25/catch/all').click()
82+
83+
cy.get('h1').should('contain', 'Show #25')
84+
cy.get('p').should('contain', 'Hellsing')
85+
86+
cy.contains('Go back home').click()
87+
cy.contains('/catch/75/undefined/path/test').click()
88+
89+
cy.get('h1').should('contain', 'Show #75')
90+
cy.get('p').should('contain', 'The Mindy Project')
91+
cy.window().should('have.property', 'noReload', true)
92+
})
93+
})

lib/getNetlifyRoute.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66
// The original does not work with Netlify routing.
77

88
// converts a nextjs dynamic route /[param]/ -> /:param
9-
// also handles catch all routes /[...param]/ -> /:*
9+
// also handles catch all routes /[...param]/ -> /:param/*
10+
// and optional catch all routes /[[...param]]/ -> /*
1011
module.exports = dynamicRoute => {
11-
// replace any catch all group first
12-
const expressified = dynamicRoute.replace(/\[\.\.\.(.*)](.json)?$/, ":$1/*");
12+
let expressified = dynamicRoute
13+
14+
// replace default catch-all groups, e.g. [...slug]
15+
expressified = expressified.replace(/\/\[\.{3}(.*)](.json)?$/, "/:$1/*")
16+
17+
// replace optional catch-all groups, e.g. [[...slug]]
18+
expressified = expressified.replace(/\/\[{2}\.{3}(.*)]{2}(.json)?$/, "*")
1319

1420
// now replace other dynamic route groups
15-
return expressified.replace(/\[(.*?)]/g, ":$1");
21+
expressified = expressified.replace(/\/\[(.*?)]/g, "/:$1")
22+
23+
return expressified
1624
};

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
],
2727
"scripts": {
2828
"test": "jest --config tests/jest.config.js",
29-
"cypress:local": "env CYPRESS_DEPLOY=local cypress run --config-file false",
30-
"cypress:netlify": "env CYPRESS_DEPLOY=netlify cypress run --config-file false",
29+
"cypress:local": "env CYPRESS_DEPLOY=local cypress run --config-file false --config video=false",
30+
"cypress:netlify": "env CYPRESS_DEPLOY=netlify cypress run --config-file false --config video=false",
3131
"cypress:local:testonly": "env CYPRESS_SKIP_DEPLOY=true npm run cypress:local",
3232
"cypress:netlify:testonly": "env CYPRESS_SKIP_DEPLOY=true npm run cypress:netlify"
3333
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Routing creates Netlify redirects 1`] = `
4+
"# Next-on-Netlify Redirects
5+
/index /index.html 200
6+
/ /index.html 200
7+
/404 /404.html 200
8+
/catch* /.netlify/functions/next_catch_all 200
9+
/_next/data/%BUILD_ID%/catch* /.netlify/functions/next_catch_all 200"
10+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
target: 'serverless',
3+
experimental: {
4+
optionalCatchAll: true
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useRouter } from 'next/router'
2+
import Link from 'next/link'
3+
4+
const CatchAll = ({ show }) => {
5+
const router = useRouter()
6+
7+
// This is never shown on Netlify. We just need it for NextJS to be happy,
8+
// because NextJS will render a fallback HTML page.
9+
if (router.isFallback) {
10+
return <div>Loading...</div>
11+
}
12+
13+
return (
14+
<div>
15+
<p>
16+
This page uses getStaticProps() to pre-fetch a TV show.
17+
</p>
18+
19+
<hr/>
20+
21+
<h1>Show #{show.id}</h1>
22+
<p>
23+
{show.name}
24+
</p>
25+
26+
<hr/>
27+
28+
<Link href="/">
29+
<a>Go back home</a>
30+
</Link>
31+
</div>
32+
)
33+
}
34+
35+
export async function getServerSideProps({ params }) {
36+
// The ID to render
37+
const { all } = params
38+
const id = all ? all[0] : 1
39+
40+
const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
41+
const data = await res.json();
42+
43+
return {
44+
props: {
45+
show: data
46+
}
47+
}
48+
}
49+
50+
export default CatchAll
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Link from 'next/link'
2+
3+
const Index = () => (
4+
<div>
5+
<ul>
6+
<li>
7+
<Link href="/catch/[[...all]]" as="/catch">
8+
<a>/catch</a>
9+
</Link>
10+
</li>
11+
<li>
12+
<Link href="/catch/[[...all]]" as="/catch/25/catch/all">
13+
<a>/catch/25/catch/all</a>
14+
</Link>
15+
</li>
16+
<li>
17+
<Link href="/catch/[[...all]]" as="/catch/75/undefined/path/test">
18+
<a>/catch/75/undefined/path/test</a>
19+
</Link>
20+
</li>
21+
</ul>
22+
</div>
23+
)
24+
25+
export default Index

tests/optionalCatchAll.test.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Test that next-on-netlify does not crash when pre-rendering index.js file.
2+
// See: https://github.com/FinnWoelm/next-on-netlify/issues/2#issuecomment-636415494
3+
4+
const { parse, join } = require('path')
5+
const { copySync, emptyDirSync, existsSync,
6+
readdirSync, readFileSync, readJsonSync } = require('fs-extra')
7+
const npmRunBuild = require("./helpers/npmRunBuild")
8+
9+
// The name of this test file (without extension)
10+
const FILENAME = parse(__filename).name
11+
12+
// The directory which will be used for testing.
13+
// We simulate a NextJS app within that directory, with pages, and a
14+
// package.json file.
15+
const PROJECT_PATH = join(__dirname, "builds", FILENAME)
16+
17+
// The directory that contains the fixtures, such as NextJS pages,
18+
// NextJS config, and package.json
19+
const FIXTURE_PATH = join(__dirname, "fixtures")
20+
21+
// Capture the output of `npm run build` to verify successful build
22+
let BUILD_OUTPUT
23+
24+
beforeAll(
25+
async () => {
26+
// Clear project directory
27+
emptyDirSync(PROJECT_PATH)
28+
emptyDirSync(join(PROJECT_PATH, "pages"))
29+
30+
// Copy NextJS pages and config
31+
copySync(
32+
join(FIXTURE_PATH, "pages-with-optionalCatchAll"),
33+
join(PROJECT_PATH, "pages")
34+
)
35+
copySync(
36+
join(FIXTURE_PATH, "next.config.js-with-optionalCatchAll"),
37+
join(PROJECT_PATH, "next.config.js")
38+
)
39+
40+
// Copy package.json
41+
copySync(
42+
join(FIXTURE_PATH, "package.json"),
43+
join(PROJECT_PATH, "package.json")
44+
)
45+
46+
// Invoke `npm run build`: Build Next and run next-on-netlify
47+
const { stdout } = await npmRunBuild({ directory: PROJECT_PATH })
48+
BUILD_OUTPUT = stdout
49+
},
50+
// time out after 180 seconds
51+
180 * 1000
52+
)
53+
54+
describe('Next', () => {
55+
test('builds successfully', () => {
56+
// NextJS output
57+
expect(BUILD_OUTPUT).toMatch("Creating an optimized production build...")
58+
expect(BUILD_OUTPUT).toMatch("Automatically optimizing pages...")
59+
expect(BUILD_OUTPUT).toMatch("First Load JS shared by all")
60+
61+
// Next on Netlify output
62+
expect(BUILD_OUTPUT).toMatch("Next on Netlify")
63+
expect(BUILD_OUTPUT).toMatch("Success! All done!")
64+
})
65+
})
66+
67+
describe('Routing',() => {
68+
test('creates Netlify redirects', async () => {
69+
// Read _redirects file
70+
const contents = readFileSync(join(PROJECT_PATH, "out_publish", "_redirects"))
71+
let redirects = contents.toString()
72+
73+
// Replace non-persistent build ID with placeholder
74+
redirects = redirects.replace(/\/_next\/data\/[^\/]+\//g, "/_next/data/%BUILD_ID%/")
75+
76+
// Check that redirects match
77+
expect(redirects).toMatchSnapshot()
78+
})
79+
})

0 commit comments

Comments
 (0)