Skip to content

Commit e64797d

Browse files
committed
Merge branch 'main' into fix-ampersand
2 parents 6f9bd41 + e5ddcd2 commit e64797d

File tree

16 files changed

+787
-23
lines changed

16 files changed

+787
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
describe('On-demand revalidation', () => {
2+
it('revalidates static ISR route with default locale', () => {
3+
cy.request({ url: '/api/revalidate/?select=0' }).then((res) => {
4+
expect(res.status).to.eq(200)
5+
expect(res.body).to.have.property('message', 'success')
6+
})
7+
})
8+
it('revalidates static ISR route with non-default locale', () => {
9+
cy.request({ url: '/api/revalidate/?select=1' }).then((res) => {
10+
expect(res.status).to.eq(200)
11+
expect(res.body).to.have.property('message', 'success')
12+
})
13+
})
14+
it('revalidates root static ISR route with default locale', () => {
15+
cy.request({ url: '/api/revalidate/?select=2' }).then((res) => {
16+
expect(res.status).to.eq(200)
17+
expect(res.body).to.have.property('message', 'success')
18+
})
19+
})
20+
it('revalidates root static ISR route with non-default locale', () => {
21+
cy.request({ url: '/api/revalidate/?select=3' }).then((res) => {
22+
expect(res.status).to.eq(200)
23+
expect(res.body).to.have.property('message', 'success')
24+
})
25+
})
26+
it('revalidates dynamic prerendered ISR route with default locale', () => {
27+
cy.request({ url: '/api/revalidate/?select=4' }).then((res) => {
28+
expect(res.status).to.eq(200)
29+
expect(res.body).to.have.property('message', 'success')
30+
})
31+
})
32+
it('fails to revalidate dynamic non-prerendered ISR route with fallback false', () => {
33+
cy.request({ url: '/api/revalidate/?select=5', failOnStatusCode: false }).then((res) => {
34+
expect(res.status).to.eq(500)
35+
expect(res.body).to.have.property('message')
36+
expect(res.body.message).to.include('Invalid response 404')
37+
})
38+
})
39+
it('revalidates dynamic non-prerendered ISR route with fallback blocking', () => {
40+
cy.request({ url: '/api/revalidate/?select=6' }).then((res) => {
41+
expect(res.status).to.eq(200)
42+
expect(res.body).to.have.property('message', 'success')
43+
})
44+
})
45+
it('revalidates dynamic non-prerendered ISR route with fallback blocking and non-default locale', () => {
46+
cy.request({ url: '/api/revalidate/?select=7' }).then((res) => {
47+
expect(res.status).to.eq(200)
48+
expect(res.body).to.have.property('message', 'success')
49+
})
50+
})
51+
it('revalidates dynamic prerendered appDir route', () => {
52+
cy.request({ url: '/api/revalidate/?select=8' }).then((res) => {
53+
expect(res.status).to.eq(200)
54+
expect(res.body).to.have.property('message', 'success')
55+
})
56+
})
57+
it('fails to revalidate dynamic non-prerendered appDir route', () => {
58+
cy.request({ url: '/api/revalidate/?select=9' }).then((res) => {
59+
expect(res.status).to.eq(200)
60+
expect(res.body).to.have.property('message', 'success')
61+
})
62+
})
63+
it('revalidates dynamic prerendered appDir route with catch-all params', () => {
64+
cy.request({ url: '/api/revalidate/?select=10' }).then((res) => {
65+
expect(res.status).to.eq(200)
66+
expect(res.body).to.have.property('message', 'success')
67+
})
68+
})
69+
})

demos/default/pages/api/revalidate.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export default async function handler(req, res) {
2+
const query = req.query
3+
const select = Number(query.select) || 0
4+
5+
// these paths are used for e2e testing res.revalidate()
6+
const paths = [
7+
'/getStaticProps/with-revalidate/', // valid path
8+
'/fr/getStaticProps/with-revalidate/', // valid path (with locale)
9+
'/', // valid path (index)
10+
'/fr/', // valid path (index with locale)
11+
'/getStaticProps/withRevalidate/2/', // valid path (with dynamic route)
12+
'/getStaticProps/withRevalidate/3/', // invalid path (fallback false with dynamic route)
13+
'/getStaticProps/withRevalidate/withFallbackBlocking/3/', // valid path (fallback blocking with dynamic route)
14+
'/fr/getStaticProps/withRevalidate/withFallbackBlocking/3/', // valid path (fallback blocking with dynamic route and locale)
15+
'/blog/nick/', // valid path (with prerendered appDir dynamic route)
16+
'/blog/greg/', // invalid path (with non-prerendered appDir dynamic route)
17+
'/blog/rob/hello/', // valid path (with appDir dynamic route catch-all)
18+
]
19+
20+
try {
21+
await res.revalidate(paths[select])
22+
return res.json({ code: 200, message: 'success' })
23+
} catch (err) {
24+
return res.status(500).send({ code: 500, message: err.message })
25+
}
26+
}

demos/default/pages/getStaticProps/with-revalidate.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Link from 'next/link'
22

3-
const Show = ({ show }) => (
3+
const Show = ({ show, time }) => (
44
<div>
5-
<p>This page uses getStaticProps() to pre-fetch a TV show.</p>
5+
<p>This page uses getStaticProps() to pre-fetch a TV show at {time}</p>
66

77
<hr />
88

@@ -22,6 +22,7 @@ export async function getStaticProps(context) {
2222
return {
2323
props: {
2424
show: data,
25+
time: new Date().toISOString(),
2526
},
2627
// ODB handler will use the minimum TTL=60s
2728
revalidate: 1,

package-lock.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"jest-extended": "^3.2.0",
7676
"jest-fetch-mock": "^3.0.3",
7777
"jest-junit": "^14.0.1",
78+
"mock-fs": "^5.2.0",
7879
"netlify-plugin-cypress": "^2.2.1",
7980
"npm-run-all": "^4.1.5",
8081
"playwright-chromium": "^1.26.1",

packages/runtime/src/constants.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
export const HANDLER_FUNCTION_NAME = '___netlify-handler'
22
export const ODB_FUNCTION_NAME = '___netlify-odb-handler'
33
export const IMAGE_FUNCTION_NAME = '_ipx'
4-
4+
export const NEXT_PLUGIN_NAME = '@netlify/next-runtime'
5+
export const NEXT_PLUGIN = '@netlify/plugin-nextjs'
6+
export const HANDLER_FUNCTION_TITLE = 'Next.js SSR handler'
7+
export const ODB_FUNCTION_TITLE = 'Next.js ISR handler'
8+
export const IMAGE_FUNCTION_TITLE = 'next/image handler'
59
// These are paths in .next that shouldn't be publicly accessible
610
export const HIDDEN_PATHS = [
711
'/cache/*',

packages/runtime/src/helpers/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const updateRequiredServerFiles = async (publish: string, modifiedConfig:
7171
await writeJSON(configFile, modifiedConfig)
7272
}
7373

74-
const resolveModuleRoot = (moduleName) => {
74+
export const resolveModuleRoot = (moduleName) => {
7575
try {
7676
return dirname(relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })))
7777
} catch {

packages/runtime/src/helpers/functions.ts

+35-4
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@ import type { ImageConfigComplete, RemotePattern } from 'next/dist/shared/lib/im
77
import { outdent } from 'outdent'
88
import { join, relative, resolve } from 'pathe'
99

10-
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME, DEFAULT_FUNCTIONS_SRC } from '../constants'
10+
import {
11+
HANDLER_FUNCTION_NAME,
12+
ODB_FUNCTION_NAME,
13+
IMAGE_FUNCTION_NAME,
14+
DEFAULT_FUNCTIONS_SRC,
15+
HANDLER_FUNCTION_TITLE,
16+
ODB_FUNCTION_TITLE,
17+
IMAGE_FUNCTION_TITLE,
18+
} from '../constants'
1119
import { getApiHandler } from '../templates/getApiHandler'
1220
import { getHandler } from '../templates/getHandler'
1321
import { getResolverForPages, getResolverForSourceFiles } from '../templates/getPageResolver'
1422

1523
import { ApiConfig, ApiRouteType, extractConfigFromFile } from './analysis'
1624
import { getSourceFileForPage } from './files'
25+
import { writeFunctionConfiguration } from './functionsMetaData'
1726
import { getFunctionNameForPage } from './utils'
1827

1928
export interface ApiRouteConfig {
@@ -45,8 +54,16 @@ export const generateFunctions = async (
4554
})
4655
const functionName = getFunctionNameForPage(route, config.type === ApiRouteType.BACKGROUND)
4756
await ensureDir(join(functionsDir, functionName))
57+
58+
// write main API handler file
4859
await writeFile(join(functionsDir, functionName, `${functionName}.js`), apiHandlerSource)
60+
61+
// copy handler dependencies (VercelNodeBridge, NetlifyNextServer, etc.)
4962
await copyFile(bridgeFile, join(functionsDir, functionName, 'bridge.js'))
63+
await copyFile(
64+
join(__dirname, '..', '..', 'lib', 'templates', 'server.js'),
65+
join(functionsDir, functionName, 'server.js'),
66+
)
5067
await copyFile(
5168
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'),
5269
join(functionsDir, functionName, 'handlerUtils.js'),
@@ -62,19 +79,28 @@ export const generateFunctions = async (
6279
await writeFile(join(functionsDir, functionName, 'pages.js'), resolverSource)
6380
}
6481

65-
const writeHandler = async (functionName: string, isODB: boolean) => {
82+
const writeHandler = async (functionName: string, functionTitle: string, isODB: boolean) => {
6683
const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) })
6784
await ensureDir(join(functionsDir, functionName))
85+
86+
// write main handler file (standard or ODB)
6887
await writeFile(join(functionsDir, functionName, `${functionName}.js`), handlerSource)
88+
89+
// copy handler dependencies (VercelNodeBridge, NetlifyNextServer, etc.)
6990
await copyFile(bridgeFile, join(functionsDir, functionName, 'bridge.js'))
91+
await copyFile(
92+
join(__dirname, '..', '..', 'lib', 'templates', 'server.js'),
93+
join(functionsDir, functionName, 'server.js'),
94+
)
7095
await copyFile(
7196
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'),
7297
join(functionsDir, functionName, 'handlerUtils.js'),
7398
)
99+
writeFunctionConfiguration({ functionName, functionTitle, functionsDir })
74100
}
75101

76-
await writeHandler(HANDLER_FUNCTION_NAME, false)
77-
await writeHandler(ODB_FUNCTION_NAME, true)
102+
await writeHandler(HANDLER_FUNCTION_NAME, HANDLER_FUNCTION_TITLE, false)
103+
await writeHandler(ODB_FUNCTION_NAME, ODB_FUNCTION_TITLE, true)
78104
}
79105

80106
/**
@@ -138,6 +164,11 @@ export const setupImageFunction = async ({
138164
})
139165

140166
await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName))
167+
writeFunctionConfiguration({
168+
functionName: IMAGE_FUNCTION_NAME,
169+
functionTitle: IMAGE_FUNCTION_TITLE,
170+
functionsDir: functionsPath,
171+
})
141172

142173
// If we have edge functions then the request will have already been rewritten
143174
// so this won't match. This is matched if edge is disabled or unavailable.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { existsSync, readJSON, writeFile } from 'fs-extra'
2+
import { join } from 'pathe'
3+
4+
import { NEXT_PLUGIN, NEXT_PLUGIN_NAME } from '../constants'
5+
6+
import { resolveModuleRoot } from './config'
7+
8+
const getNextRuntimeVersion = async (packageJsonPath: string, useNodeModulesPath: boolean) => {
9+
if (!existsSync(packageJsonPath)) {
10+
return
11+
}
12+
13+
const packagePlugin = await readJSON(packageJsonPath)
14+
15+
return useNodeModulesPath ? packagePlugin.version : packagePlugin.dependencies[NEXT_PLUGIN]
16+
}
17+
18+
// The information needed to create a function configuration file
19+
export interface FunctionInfo {
20+
// The name of the function, e.g. `___netlify-handler`
21+
functionName: string
22+
23+
// The name of the function that will be displayed in logs, e.g. `Next.js SSR handler`
24+
functionTitle: string
25+
26+
// The directory where the function is located, e.g. `.netlify/functions`
27+
functionsDir: string
28+
}
29+
30+
/**
31+
* Creates a function configuration file for the given function.
32+
*
33+
* @param functionInfo The information needed to create a function configuration file
34+
*/
35+
export const writeFunctionConfiguration = async (functionInfo: FunctionInfo) => {
36+
const { functionName, functionTitle, functionsDir } = functionInfo
37+
const pluginPackagePath = '.netlify/plugins/package.json'
38+
const moduleRoot = resolveModuleRoot(NEXT_PLUGIN)
39+
const nodeModulesPath = moduleRoot ? join(moduleRoot, 'package.json') : null
40+
41+
const nextPluginVersion =
42+
(await getNextRuntimeVersion(nodeModulesPath, true)) ||
43+
(await getNextRuntimeVersion(pluginPackagePath, false)) ||
44+
// The runtime version should always be available, but if it's not, return 'unknown'
45+
'unknown'
46+
47+
const metadata = {
48+
config: {
49+
name: functionTitle,
50+
generator: `${NEXT_PLUGIN_NAME}@${nextPluginVersion}`,
51+
},
52+
version: 1,
53+
}
54+
55+
await writeFile(join(functionsDir, functionName, `${functionName}.json`), JSON.stringify(metadata))
56+
}

0 commit comments

Comments
 (0)