Compare commits

..

6 Commits

Author SHA1 Message Date
JJ Kasper
81011df816 Publish Stable
- @vercel/next@2.6.34
2020-10-30 10:50:03 -05:00
JJ Kasper
c8d31bdcf7 Publish Canary
- @vercel/next@2.6.34-canary.0
2020-10-30 10:16:20 -05:00
JJ Kasper
5e7f1158ad [next] Ensure revalidation works with i18n (#5350)
This corrects some i18n pages being considered staticPages instead of prerender items which was breaking revalidate behavior. This also adds more verbose tests for i18n ensuring revalidation is working correctly

x-ref: https://github.com/vercel/next.js/discussions/18443
Fixes: https://github.com/vercel/next.js/issues/18503
2020-10-30 14:59:47 +00:00
JJ Kasper
df5aa1f10d Publish Stable
- @vercel/next@2.6.33
2020-10-27 13:41:00 -05:00
JJ Kasper
eb1ba97309 Publish Canary
- @vercel/next@2.6.33-canary.0
2020-10-27 13:33:33 -05:00
JJ Kasper
8047d6de49 [next] Fix index data path for non-locale path (#5339)
* Fix index data path for non-locale path

* Add index SSG test

* Update tests for canary

* Correct logic check to handle default locale
2020-10-27 13:33:02 -05:00
12 changed files with 422 additions and 43 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "2.6.32",
"version": "2.6.34",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",

View File

@@ -880,7 +880,9 @@ export const build = async ({
// Next.js versions so we need to also not treat it as a static page here.
if (
prerenderManifest.staticRoutes[routeName] ||
prerenderManifest.fallbackRoutes[routeName]
prerenderManifest.fallbackRoutes[routeName] ||
prerenderManifest.staticRoutes[normalizePage(pathname)] ||
prerenderManifest.fallbackRoutes[normalizePage(pathname)]
) {
return;
}
@@ -1391,17 +1393,19 @@ export const build = async ({
if (i18n) {
const { pathname } = url.parse(route.dest!);
const isFallback = prerenderManifest.fallbackRoutes[pathname!];
const isBlocking =
prerenderManifest.blockingFallbackRoutes[pathname!];
route.src = route.src.replace(
'^',
`^${dynamicPrefix ? `${dynamicPrefix}[/]?` : '[/]?'}(?${
isFallback ? '<nextLocale>' : ':'
isFallback || isBlocking ? '<nextLocale>' : ':'
}${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})?`
);
if (isFallback) {
if (isFallback || isBlocking) {
// ensure destination has locale prefix to match prerender output
// path so that the prerender object is used
route.dest = route.dest!.replace(
@@ -1457,7 +1461,7 @@ export const build = async ({
)}).some((locale) => {
if (pathnameParts[1].toLowerCase() === locale.toLowerCase()) {
pathnameParts.splice(1, 1)
pathname = pathnameParts.join('/') || '/'
pathname = pathnameParts.join('/') || '/index'
return true
}
return false
@@ -1493,7 +1497,7 @@ export const build = async ({
if (!toRender) {
try {
const { pathname } = url.parse(req.url)
toRender = stripLocalePath(pathname).replace(/\\/$/, '')
toRender = stripLocalePath(pathname).replace(/\\/$/, '') || '/index'
} catch (_) {
// handle failing to parse url
res.statusCode = 400
@@ -1512,7 +1516,7 @@ export const build = async ({
.replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
.replace(/\\.json$/, '')
toRender = stripLocalePath(toRender)
toRender = stripLocalePath(toRender) || '/index'
currentPage = pages[toRender]
}
@@ -1540,7 +1544,7 @@ export const build = async ({
if (!currentPage) {
console.error(
"Failed to find matching page for", toRender, "in lambda"
"Failed to find matching page for", {toRender, header: req.headers['x-nextjs-page'], url: req.url, pages: Object.keys(pages) }, "in lambda"
)
res.statusCode = 500
return res.end('internal server error')
@@ -1725,7 +1729,10 @@ export const build = async ({
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${routeFileNoExt}${
origRouteFileNoExt === '/index' ? '/index' : ''
routeFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
}
@@ -1791,6 +1798,44 @@ export const build = async ({
});
++prerenderGroup;
if (routesManifest?.i18n && isBlocking) {
for (const locale of routesManifest.i18n.locales) {
const localeRouteFileNoExt = addLocaleOrDefault(
routeFileNoExt,
routesManifest,
locale
);
const localeOutputPathPage = path.posix.join(
entryDirectory,
localeRouteFileNoExt
);
const localeOutputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${localeRouteFileNoExt}${
localeRouteFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
const origPrerenderPage = prerenders[outputPathPage];
const origPrerenderData = prerenders[outputPathData];
prerenders[localeOutputPathPage] = {
...origPrerenderPage,
group: prerenderGroup,
} as Prerender;
prerenders[localeOutputPathData] = {
...origPrerenderData,
group: prerenderGroup,
} as Prerender;
++prerenderGroup;
}
}
}
if ((nonDynamicSsg || isFallback) && routesManifest?.i18n && !locale) {

View File

@@ -2,22 +2,20 @@ module.exports = {
generateBuildId() {
return 'testing-build-id';
},
experimental: {
i18n: {
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
// TODO: testing locale domains support, will require custom
// testing set-up as test accounts are used currently
domains: [
{
domain: 'example.be',
defaultLocale: 'nl-BE',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
i18n: {
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
// TODO: testing locale domains support, will require custom
// testing set-up as test accounts are used currently
domains: [
{
domain: 'example.be',
defaultLocale: 'nl-BE',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
};

View File

@@ -0,0 +1,224 @@
/* eslint-env jest */
const fetch = require('node-fetch');
const cheerio = require('cheerio');
module.exports = function (ctx) {
it('should revalidate content properly from /', async () => {
const res = await fetch(`${ctx.deploymentUrl}/`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
});
it('should revalidate content properly from /fr', async () => {
const res = await fetch(`${ctx.deploymentUrl}/fr`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
});
it('should revalidate content properly from /nl-NL', async () => {
const res = await fetch(`${ctx.deploymentUrl}/nl-NL`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
});
it('should revalidate content properly from /gsp/fallback/new-page', async () => {
const initRes = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
expect(initRes.status).toBe(200);
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
});
it('should revalidate content properly from /fr/gsp/fallback/new-page', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/gsp/fallback/new-page.json`
);
expect(dataRes.status).toBe(200);
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/new-page`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/new-page`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
});
it('should revalidate content properly from /nl-NL/gsp/fallback/new-page', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/gsp/fallback/new-page.json`
);
expect(dataRes.status).toBe(200);
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/new-page`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(
`${ctx.deploymentUrl}/nl-NL/gsp/fallback/new-page`
);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
});
it('should revalidate content properly from /gsp/no-fallback/first', async () => {
const res = await fetch(`${ctx.deploymentUrl}/gsp/no-fallback/first`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/no-fallback/first`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
});
it('should revalidate content properly from /fr/gsp/no-fallback/first', async () => {
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/no-fallback/first`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/no-fallback/first`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
});
it('should revalidate content properly from /nl-NL/gsp/no-fallback/second', async () => {
const res = await fetch(
`${ctx.deploymentUrl}/nl-NL/gsp/no-fallback/second`
);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(
`${ctx.deploymentUrl}/nl-NL/gsp/no-fallback/second`
);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
});
};

View File

@@ -2,22 +2,20 @@ module.exports = {
generateBuildId() {
return 'testing-build-id';
},
experimental: {
i18n: {
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
// TODO: testing locale domains support, will require custom
// testing set-up as test accounts are used currently
domains: [
{
domain: 'example.be',
defaultLocale: 'nl-BE',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
i18n: {
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
// TODO: testing locale domains support, will require custom
// testing set-up as test accounts are used currently
domains: [
{
domain: 'example.be',
defaultLocale: 'nl-BE',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
};

View File

@@ -427,6 +427,52 @@
"path": "/_next/data/testing-build-id/nl/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"nl\""
},
{
"path": "/gsp/blocking/first",
"status": 200,
"mustContain": "catchall"
},
{
"path": "/gsp/blocking/first",
"status": 200,
"mustContain": "lang=\"en-US\""
},
{
"path": "/_next/data/testing-build-id/en-US/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
},
{
"path": "/nl-NL/gsp/blocking/first",
"status": 200,
"mustContain": "catchall"
},
{
"path": "/nl-NL/gsp/blocking/first",
"status": 200,
"mustContain": "lang=\"nl-NL\""
},
{
"path": "/_next/data/testing-build-id/nl-NL/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
},
{
"path": "/fr/gsp/blocking/first",
"status": 200,
"mustContain": "catchall"
},
{
"path": "/fr/gsp/blocking/first",
"status": 200,
"mustContain": "lang=\"fr\""
},
{
"path": "/_next/data/testing-build-id/fr/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
}
]
}

View File

@@ -0,0 +1,43 @@
import Link from 'next/link';
const Slug = props => {
return (
<div>
<p id="props">{JSON.stringify(props)}</p>
<Link href="/gsp/blocking/hallo-wereld" locale={'nl-NL'}>
<a>/nl-NL/gsp/blocking/hallo-wereld</a>
</Link>
<br />
<Link href="/gsp/blocking/42" locale={'nl-NL'}>
<a>/nl-NL/gsp/blocking/42</a>
</Link>
<br />
<Link href="/gsp/blocking/hallo-welt" locale={'fr'}>
<a>/fr/gsp/blocking/hallo-welt</a>
</Link>
<br />
<Link href="/gsp/blocking/42" locale={'fr'}>
<a>/fr/gsp/blocking/42</a>
</Link>
</div>
);
};
export const getStaticProps = () => {
return {
props: {
random: Math.random(),
catchall: 'yes',
},
revalidate: 1,
};
};
export const getStaticPaths = () => {
return {
paths: [],
fallback: 'blocking',
};
};
export default Slug;

View File

@@ -26,10 +26,12 @@ export default function Page(props) {
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
random: Math.random(),
params,
locale,
locales,
},
revalidate: 1,
};
};

View File

@@ -26,10 +26,12 @@ export default function Page(props) {
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
random: Math.random(),
params,
locale,
locales,
},
revalidate: 1,
};
};
@@ -40,6 +42,7 @@ export const getStaticPaths = () => {
'/gsp/no-fallback/second',
{ params: { slug: 'first' }, locale: 'en-US' },
'/nl-NL/gsp/no-fallback/second',
'/fr/gsp/no-fallback/first',
],
fallback: false,
};

View File

@@ -48,8 +48,10 @@ export default function Page(props) {
export const getStaticProps = ({ locale, locales }) => {
return {
props: {
random: Math.random(),
locale,
locales,
},
revalidate: 1,
};
};

View File

@@ -152,6 +152,16 @@
"x-vercel-cache": "/HIT|STALE|PRERENDER/"
}
},
{
"path": "/",
"status": 200,
"mustContain": "Hi"
},
{
"path": "/_next/data/testing-build-id/index.json",
"status": 200,
"mustContain": "\"hello\":\"index\""
},
{
"path": "/_next/data/testing-build-id/api-docs/second.json",
"status": 200

View File

@@ -1 +1,9 @@
export default () => 'Hi';
export const getStaticProps = () => {
return {
props: {
hello: 'index',
},
};
};