-
Notifications
You must be signed in to change notification settings - Fork 87
feat: refresh hooks api implementation #1950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
55d6022
c3d5607
3030689
699ad3a
8033d60
3c805d4
796e458
bf3f8d9
2a2da7e
cae0a91
b04cb8e
de649e9
98407ea
f691362
443beca
e043e9d
95f7c94
238596e
cf0fc72
0f7d1db
8cc7abe
fef98a1
bf97d73
fef1c59
99ed48d
3ec181f
6b4a2ce
021f825
a40925f
6aa81df
8f60f43
9fc4123
0e61a26
a8ea2bd
36acbdb
d53a11d
4e8daa5
8bcdb1c
ce62c92
3da9e46
228ed12
f85d2f5
a197783
5da076d
d029976
6ef27a5
1445c26
fd32f38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
describe('On-demand revalidation', () => { | ||
it('revalidates static ISR route with default locale', () => { | ||
cy.request({ url: '/api/revalidate/?select=0' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates static ISR route with non-default locale', () => { | ||
cy.request({ url: '/api/revalidate/?select=1' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates root static ISR route with default locale', () => { | ||
cy.request({ url: '/api/revalidate/?select=2' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates root static ISR route with non-default locale', () => { | ||
cy.request({ url: '/api/revalidate/?select=3' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates dynamic prerendered ISR route with default locale', () => { | ||
cy.request({ url: '/api/revalidate/?select=4' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('fails to revalidate dynamic non-prerendered ISR route with fallback false', () => { | ||
cy.request({ url: '/api/revalidate/?select=5', failOnStatusCode: false }).then((res) => { | ||
expect(res.status).to.eq(500) | ||
expect(res.body).to.have.property('message') | ||
expect(res.body.message).to.include('Invalid response 404') | ||
}) | ||
}) | ||
it('revalidates dynamic non-prerendered ISR route with fallback blocking', () => { | ||
cy.request({ url: '/api/revalidate/?select=6' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates dynamic non-prerendered ISR route with fallback blocking and non-default locale', () => { | ||
cy.request({ url: '/api/revalidate/?select=7' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates dynamic prerendered appDir route', () => { | ||
cy.request({ url: '/api/revalidate/?select=8' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('fails to revalidate dynamic non-prerendered appDir route', () => { | ||
cy.request({ url: '/api/revalidate/?select=9' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
it('revalidates dynamic prerendered appDir route with catch-all params', () => { | ||
cy.request({ url: '/api/revalidate/?select=10' }).then((res) => { | ||
expect(res.status).to.eq(200) | ||
expect(res.body).to.have.property('message', 'success') | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export default async function handler(req, res) { | ||
const query = req.query | ||
const select = Number(query.select) || 0 | ||
|
||
// these paths are used for e2e testing res.revalidate() | ||
const paths = [ | ||
'/getStaticProps/with-revalidate/', // valid path | ||
'/fr/getStaticProps/with-revalidate/', // valid path (with locale) | ||
'/', // valid path (index) | ||
'/fr/', // valid path (index with locale) | ||
'/getStaticProps/withRevalidate/2/', // valid path (with dynamic route) | ||
'/getStaticProps/withRevalidate/3/', // invalid path (fallback false with dynamic route) | ||
'/getStaticProps/withRevalidate/withFallbackBlocking/3/', // valid path (fallback blocking with dynamic route) | ||
'/fr/getStaticProps/withRevalidate/withFallbackBlocking/3/', // valid path (fallback blocking with dynamic route and locale) | ||
'/blog/nick/', // valid path (with prerendered appDir dynamic route) | ||
'/blog/greg/', // invalid path (with non-prerendered appDir dynamic route) | ||
'/blog/rob/hello/', // valid path (with appDir dynamic route catch-all) | ||
] | ||
|
||
try { | ||
await res.revalidate(paths[select]) | ||
return res.json({ code: 200, message: 'success' }) | ||
} catch (err) { | ||
return res.status(500).send({ code: 500, message: err.message }) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,8 +45,16 @@ export const generateFunctions = async ( | |
}) | ||
const functionName = getFunctionNameForPage(route, config.type === ApiRouteType.BACKGROUND) | ||
await ensureDir(join(functionsDir, functionName)) | ||
|
||
// write main API handler file | ||
await writeFile(join(functionsDir, functionName, `${functionName}.js`), apiHandlerSource) | ||
|
||
// copy handler dependencies (VercelNodeBridge, NetlifyNextServer, etc.) | ||
await copyFile(bridgeFile, join(functionsDir, functionName, 'bridge.js')) | ||
await copyFile( | ||
join(__dirname, '..', '..', 'lib', 'templates', 'server.js'), | ||
join(functionsDir, functionName, 'server.js'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get some comments on why we're copying this file? (This whole functions.ts file could use those tbh hah, but I won't put that on you) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep, i totally agree! i don't know enough about some of the functions in there to comment it all, but i've added comments to |
||
) | ||
await copyFile( | ||
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), | ||
join(functionsDir, functionName, 'handlerUtils.js'), | ||
|
@@ -65,8 +73,16 @@ export const generateFunctions = async ( | |
const writeHandler = async (functionName: string, isODB: boolean) => { | ||
const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) }) | ||
await ensureDir(join(functionsDir, functionName)) | ||
|
||
// write main handler file (standard or ODB) | ||
await writeFile(join(functionsDir, functionName, `${functionName}.js`), handlerSource) | ||
|
||
// copy handler dependencies (VercelNodeBridge, NetlifyNextServer, etc.) | ||
await copyFile(bridgeFile, join(functionsDir, functionName, 'bridge.js')) | ||
await copyFile( | ||
join(__dirname, '..', '..', 'lib', 'templates', 'server.js'), | ||
join(functionsDir, functionName, 'server.js'), | ||
) | ||
await copyFile( | ||
join(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), | ||
join(functionsDir, functionName, 'handlerUtils.js'), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import fs, { createWriteStream, existsSync } from 'fs' | ||
import { ServerResponse } from 'http' | ||
import { tmpdir } from 'os' | ||
import path from 'path' | ||
import { pipeline } from 'stream' | ||
|
@@ -222,3 +223,71 @@ export const normalizePath = (event: HandlerEvent) => { | |
// Ensure that paths are encoded - but don't double-encode them | ||
return new URL(event.rawUrl).pathname | ||
} | ||
|
||
// Simple Netlify API client | ||
export const netlifyApiFetch = <T>({ | ||
endpoint, | ||
payload, | ||
token, | ||
method = 'GET', | ||
}: { | ||
endpoint: string | ||
payload: unknown | ||
token: string | ||
method: 'GET' | 'POST' | ||
}): Promise<T> => | ||
new Promise((resolve, reject) => { | ||
const body = JSON.stringify(payload) | ||
|
||
const req = https.request( | ||
{ | ||
hostname: 'api.netlify.com', | ||
port: 443, | ||
path: `/api/v1/${endpoint}`, | ||
method, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Content-Length': body.length, | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}, | ||
(res: ServerResponse) => { | ||
let data = '' | ||
res.on('data', (chunk) => { | ||
data += chunk | ||
}) | ||
res.on('end', () => { | ||
resolve(JSON.parse(data)) | ||
}) | ||
}, | ||
) | ||
|
||
req.on('error', reject) | ||
|
||
req.write(body) | ||
req.end() | ||
}) | ||
|
||
// Remove trailing slash from a route (except for the root route) | ||
export const normalizeRoute = (route: string): string => (route.endsWith('/') ? route.slice(0, -1) || '/' : route) | ||
|
||
// Check if a route has a locale prefix (including the root route) | ||
const isLocalized = (route: string, i18n: { defaultLocale: string; locales: string[] }): boolean => | ||
i18n.locales.some((locale) => route === `/${locale}` || route.startsWith(`/${locale}/`)) | ||
|
||
// Remove the locale prefix from a route (if any) | ||
export const unlocalizeRoute = (route: string, i18n: { defaultLocale: string; locales: string[] }): string => | ||
isLocalized(route, i18n) ? `/${route.split('/').slice(2).join('/')}` : route | ||
|
||
// Add the default locale prefix to a route (if necessary) | ||
export const localizeRoute = (route: string, i18n: { defaultLocale: string; locales: string[] }): string => | ||
isLocalized(route, i18n) ? route : normalizeRoute(`/${i18n.defaultLocale}${route}`) | ||
|
||
// Normalize a data route to include the locale prefix and remove the index suffix | ||
export const localizeDataRoute = (dataRoute: string, localizedRoute: string): string => { | ||
if (dataRoute.endsWith('.rsc')) return dataRoute | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not certain about this - there's a bunch of strings in that function that we could make into constants - but they are unlikely to change and i think it helps readability to know what they are in that context, especially since specifics like whether they have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair! |
||
const locale = localizedRoute.split('/').find(Boolean) | ||
return dataRoute | ||
.replace(new RegExp(`/_next/data/(.+?)/(${locale}/)?`), `/_next/data/$1/${locale}/`) | ||
.replace(/\/index\.json$/, '.json') | ||
} |
Uh oh!
There was an error while loading. Please reload this page.