Compare commits

..

17 Commits

Author SHA1 Message Date
JJ Kasper
fd5d3b2921 Publish Stable
- @vercel/next@2.6.30
2020-10-20 13:11:20 -05:00
JJ Kasper
d22bdeb8d0 Publish Canary
- @vercel/next@2.6.30-canary.0
2020-10-20 12:59:12 -05:00
JJ Kasper
c120fd82f9 [next] Ensure root-most index GSP page is located correctly (#5309) 2020-10-20 19:58:18 +02:00
JJ Kasper
2474a80ff1 Correct i18n trailing slash redirect priority (#5306) 2020-10-20 11:01:31 -05:00
JJ Kasper
cc1cdbe610 Publish Stable
- @vercel/next@2.6.29
2020-10-20 06:15:03 -05:00
JJ Kasper
0b92f8ceee Publish Canary
- @vercel/next@2.6.29-canary.0
 - @vercel/routing-utils@1.9.1-canary.1
2020-10-20 05:08:26 -05:00
JJ Kasper
be82a88d1a [next] Add i18n support handling (#5298)
* Add initial locale output mapping

* Add additional probes

* Add locale specific 404 handling

* Add locale specific 404s, locale redirects, and more tests

* Update wildcard value

* Fix wildcard handling and update route type

* Update addLocale util

* Apply suggestions from code review

Co-authored-by: Steven <steven@ceriously.com>

* Remove redundant console.error

Co-authored-by: Steven <steven@ceriously.com>
Co-authored-by: Leo Lamprecht <leo@zeit.co>
2020-10-20 12:05:06 +02:00
JJ Kasper
64da08f0f2 Publish Stable
- @vercel/next@2.6.28
2020-10-20 05:00:10 -05:00
Steven
2e957fce55 Publish Canary
- @vercel/next@2.6.28-canary.0
2020-10-16 18:19:07 -04:00
Steven
c7ead151f5 [next] Add support for Image Optimization (#5296)
This PR sends the results of the image optimization config from next.config.js to the build output.

x-ref: https://github.com/vercel/next.js/pull/17749
2020-10-16 17:56:14 -04:00
dav-is
e2baf9b00f Publish Canary
- @vercel/routing-utils@1.9.1-canary.0
2020-10-14 16:48:04 -04:00
Connor Davis
ba8ef7bc98 Add custom locale cookie (#5293) 2020-10-14 16:46:40 -04:00
JJ Kasper
c5d04e0d4f Publish Stable
- @vercel/next@2.6.27
2020-10-13 14:40:53 -05:00
JJ Kasper
b4f849418d Publish Canary
- @vercel/next@2.6.27-canary.0
2020-10-13 00:17:38 -05:00
JJ Kasper
b37c4f211e [next] Update next.config backup name to be deterministic (#5288) 2020-10-13 01:13:40 -04:00
Daniel Roe
f06efe167c [docs] Update contributing example of @vercel/nft (#5284) 2020-10-12 15:01:27 -04:00
dependabot[bot]
296d8da676 Bump next from 9.5.1 to 9.5.4 in /examples/nextjs (#5278)
Bumps [next](https://github.com/vercel/next.js) from 9.5.1 to 9.5.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/vercel/next.js/releases">next's releases</a>.</em></p>
<blockquote>
<h2>v9.5.4</h2>
<p><strong>This upgrade is <em>completely backwards compatible and recommended for all users on versions below 9.5.4.</em> For future security related communications of our OSS projects, please <a href="https://vercel.com/security">join this mailing list</a>.</strong></p>
<p>A security team from one of our partners noticed an issue in Next.js that allowed for open redirects to occur.</p>
<p>Specially encoded paths could be used with the trailing slash redirect to allow an open redirect to occur to an external site.</p>
<p>In general, this redirect does not directly harm users although can allow for phishing attacks by redirecting to an attackers domain from a trusted domain.</p>
<p>We recommend upgrading to the latest version of Next.js to improve the overall security of your application.</p>
<h2>How to Upgrade</h2>
<ul>
<li>We have released patch versions for both the stable and canary channels of Next.js.</li>
<li>To upgrade run <code>npm install next@latest --save</code></li>
</ul>
<h2>Impact</h2>
<ul>
<li><strong>Affected</strong>: Users of Next.js between 9.5.0 and 9.5.3</li>
<li><strong>Not affected</strong>: Deployments on Vercel (<a href="https://vercel.com">https://vercel.com</a>) are not affected</li>
<li><strong>Not affected</strong>: Deployments using <code>next export</code></li>
</ul>
<p>We recommend everyone to upgrade regardless of whether you can reproduce the issue or not.</p>
<h3>How to Assess Impact</h3>
<p>If you think users could have been affected, you can filter logs of affected sites by <code>%2F</code> with a 308 response.</p>
<h2>What is Being Done</h2>
<p>As Next.js has grown in popularity, it has received the attention of security teams and auditors. We are thankful to those that reached out for their investigation and discovery of the original bug and subsequent responsible disclosure.</p>
<p>We've landed a patch that ensures encoding is handled properly for these types of redirects so the open redirect can no longer occur.</p>
<p>Regression tests for this attack were added to the <a href="https://github.com/vercel/next.js/blob/canary/test/integration/production/test/security.js">security</a> integration test suite.</p>
<ul>
<li>We have notified known Next.js users in advance of this publication.</li>
<li>A public CVE was released.</li>
<li>If you want to stay on top of our security related news impacting Next.js or other Vercel projects, please <a href="https://zeit.co/security">join this mailing list</a>.</li>
<li>We encourage responsible disclosure of future issues. Please email us at <strong><a href="https://github.com/vercel/next.js/blob/HEAD/mailto:security@zeit.co">security@vercel.com</a>.</strong> We are actively monitoring this mailbox.</li>
</ul>
<hr />
<h3>Core Changes</h3>
<ul>
<li>Make the image post-processor ignore SVG images: <a href="https://github-redirect.dependabot.com/vercel/next.js/issues/16732">#16732</a></li>
<li>Only update lookups for dev overlay if mounted: <a href="https://github-redirect.dependabot.com/vercel/next.js/issues/16776">#16776</a></li>
<li>Ensure interpolating dynamic href values works correctly: <a href="https://github-redirect.dependabot.com/vercel/next.js/issues/16774">#16774</a></li>
<li>Add automatic reloading when editing GS(S)P methods: <a href="https://github-redirect.dependabot.com/vercel/next.js/issues/16744">#16744</a></li>
<li>Update to show build indicator while re-fetching GS(S)P data in dev: <a href="https://github-redirect.dependabot.com/vercel/next.js/issues/16789">#16789</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="6588108150"><code>6588108</code></a> v9.5.4</li>
<li><a href="7108567b06"><code>7108567</code></a> v9.5.4-canary.25</li>
<li><a href="5d79a8c0c4"><code>5d79a8c</code></a> Update workflow step to restore cache (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17656">#17656</a>)</li>
<li><a href="4c38e3ed8e"><code>4c38e3e</code></a> fix typo (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17653">#17653</a>)</li>
<li><a href="241f38eaa8"><code>241f38e</code></a> v9.5.4-canary.24</li>
<li><a href="7dec91175c"><code>7dec911</code></a> change anonymous functions to named in docs examples (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17510">#17510</a>)</li>
<li><a href="1659e4da61"><code>1659e4d</code></a> Update migrating from Gatsby docs. (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17636">#17636</a>)</li>
<li><a href="06a8b1ad67"><code>06a8b1a</code></a> Add docs on how to migrate from Gatsby. (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17491">#17491</a>)</li>
<li><a href="04234cc312"><code>04234cc</code></a> Update to use hasNextSupport for custom-routes in next export check (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17630">#17630</a>)</li>
<li><a href="742f5d9a46"><code>742f5d9</code></a> test(create-next-app): increase coverage (<a href="https://github-redirect.dependabot.com/vercel/next.js/issues/17507">#17507</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/vercel/next.js/compare/v9.5.1...v9.5.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=9.5.1&new-version=9.5.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/configuring-github-dependabot-security-updates)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/vercel/vercel/network/alerts).

</details>
2020-10-09 14:32:15 +00:00
38 changed files with 1975 additions and 114 deletions

View File

@@ -73,8 +73,8 @@ The logs of this deployment will contain the actual error which may help you to
Some of the Builders use `@vercel/nft` to tree-shake files before deployment. If you suspect an error with this tree-shaking mechanism, you can create the following script in your project:
```js
const trace = require('@vercel/nft');
trace(['path/to/entrypoint.js'], {
const { nodeFileTrace } = require('@vercel/nft');
nodeFileTrace(['path/to/entrypoint.js'], {
ts: true,
mixedModules: true,
})

View File

@@ -8,7 +8,7 @@
"start": "next start"
},
"dependencies": {
"next": "9.5.1",
"next": "9.5.4",
"react": "16.13.1",
"react-dom": "16.13.1"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "2.6.26",
"version": "2.6.30",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -29,7 +29,7 @@
"@vercel/nft": "0.9.2",
"async-sema": "3.0.1",
"buffer-crc32": "0.2.13",
"escape-string-regexp": "3.0.0",
"escape-string-regexp": "2.0.0",
"execa": "2.0.4",
"find-up": "4.1.0",
"fs-extra": "7.0.0",

View File

@@ -59,7 +59,7 @@ export default async function createServerlessConfig(
const primaryConfigPath = path.join(entryPath, 'next.config.js');
const secondaryConfigPath = path.join(workPath, 'next.config.js');
const backupConfigName = `next.config.original.${Date.now()}.js`;
const backupConfigName = `next.config.__vercel_builder_backup__.js`;
const hasPrimaryConfig = fs.existsSync(primaryConfigPath);
const hasSecondaryConfig = fs.existsSync(secondaryConfigPath);

View File

@@ -10,7 +10,7 @@ import {
PrepareCacheOptions,
Prerender,
} from '@vercel/build-utils';
import { Handler, Route } from '@vercel/routing-utils';
import { Handler, Route, Source } from '@vercel/routing-utils';
import {
convertHeaders,
convertRedirects,
@@ -19,6 +19,7 @@ import {
import { nodeFileTrace, NodeFileTraceReasons } from '@vercel/nft';
import { Sema } from 'async-sema';
import { ChildProcess, fork } from 'child_process';
// escape-string-regexp version must match Next.js version
import escapeStringRegexp from 'escape-string-regexp';
import findUp from 'find-up';
import { lstat, pathExists, readFile, remove, writeFile } from 'fs-extra';
@@ -31,6 +32,7 @@ import buildUtils from './build-utils';
import createServerlessConfig from './create-serverless-config';
import nextLegacyVersions from './legacy-versions';
import {
addLocaleOrDefault,
createLambdaFromPseudoLayers,
createPseudoLayer,
EnvConfig,
@@ -39,6 +41,7 @@ import {
getDynamicRoutes,
getExportIntent,
getExportStatus,
getImagesManifest,
getNextConfig,
getPathsInside,
getPrerenderManifest,
@@ -46,6 +49,7 @@ import {
getRoutesManifest,
getSourceFilePathFromPage,
isDynamicRoute,
normalizeLocalePath,
normalizePackageJson,
normalizePage,
PseudoLayer,
@@ -219,7 +223,12 @@ export const build = async ({
meta = {} as BuildParamsMeta,
}: BuildParamsType): Promise<{
routes: Route[];
images?: { domains: string[]; sizes: number[] };
output: Files;
wildcard?: Array<{
domain: string;
value: string;
}>;
watch?: string[];
childProcesses: ChildProcess[];
}> => {
@@ -302,6 +311,7 @@ export const build = async ({
return {
output: {},
images: undefined,
routes: await getRoutes(
entryPath,
entryDirectory,
@@ -367,7 +377,7 @@ export const build = async ({
}
console.log('Installing dependencies...');
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts, meta);
await runNpmInstall(entryPath, [], spawnOpts, meta);
// Refetch Next version now that dependencies are installed.
// This will now resolve the actual installed Next version,
@@ -420,6 +430,7 @@ export const build = async ({
outputDirectory,
nextVersion
);
const imagesManifest = await getImagesManifest(entryPath, outputDirectory);
const prerenderManifest = await getPrerenderManifest(entryPath);
const headers: Route[] = [];
const rewrites: Route[] = [];
@@ -428,6 +439,24 @@ export const build = async ({
let dynamicRoutes: Route[] = [];
// whether they have enabled pages/404.js as the custom 404 page
let hasPages404 = false;
let buildId = '';
let escapedBuildId = '';
if (isLegacy || isSharedLambdas) {
try {
buildId = await readFile(
path.join(entryPath, outputDirectory, 'BUILD_ID'),
'utf8'
);
escapedBuildId = escapeStringRegexp(buildId);
} catch (err) {
throw new NowBuildError({
code: 'NOW_NEXT_NO_BUILD_ID',
message:
'The BUILD_ID file was not found in the Output Directory. Did you forget to run "next build" in your Build Command?',
});
}
}
if (routesManifest) {
switch (routesManifest.version) {
@@ -458,7 +487,7 @@ export const build = async ({
continue;
}
dataRoutes.push({
const route = {
src: (
dataRoute.namedDataRouteRegex || dataRoute.dataRouteRegex
).replace(/^\^/, `^${appMountPrefixNoTrailingSlash}`),
@@ -477,7 +506,31 @@ export const build = async ({
}`
),
check: true,
});
};
const { i18n } = routesManifest;
if (i18n) {
route.src = route.src.replace(
// we need to double escape the build ID here
// to replace it properly
`/${escapedBuildId}/`,
`/${escapedBuildId}/(?${
ssgDataRoute ? '<nextLocale>' : ':'
}${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})/`
);
// make sure to route to the correct prerender output
if (ssgDataRoute) {
route.dest = route.dest.replace(
`/${buildId}/`,
`/${buildId}/$nextLocale/`
);
}
}
dataRoutes.push(route);
}
}
@@ -519,6 +572,44 @@ export const build = async ({
}
}
if (imagesManifest) {
switch (imagesManifest.version) {
case 1: {
if (!imagesManifest.images) {
throw new NowBuildError({
code: 'NEXT_IMAGES_MISSING',
message:
'image-manifest.json "images" is required. Contact support if this continues to happen.',
});
}
const { images } = imagesManifest;
if (!Array.isArray(images.domains)) {
throw new NowBuildError({
code: 'NEXT_IMAGES_DOMAINS',
message:
'image-manifest.json "images.domains" must be an array. Contact support if this continues to happen.',
});
}
if (!Array.isArray(images.sizes)) {
throw new NowBuildError({
code: 'NEXT_IMAGES_DOMAINS',
message:
'image-manifest.json "images.sizes" must be an array. Contact support if this continues to happen.',
});
}
break;
}
default: {
throw new NowBuildError({
code: 'NEXT_IMAGES_VERSION_UNKNOWN',
message:
'This version of `@vercel/next` does not support the version of Next.js you are trying to deploy.\n' +
'Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
}
}
const userExport = await getExportStatus(entryPath);
if (userExport) {
@@ -564,6 +655,12 @@ export const build = async ({
return {
output,
images: imagesManifest?.images
? {
domains: imagesManifest.images.domains,
sizes: imagesManifest.images.sizes,
}
: undefined,
routes: [
// User headers
...headers,
@@ -645,12 +742,7 @@ export const build = async ({
if (isLegacy) {
debug('Running npm install --production...');
await runNpmInstall(
entryPath,
['--prefer-offline', '--production'],
spawnOpts,
meta
);
await runNpmInstall(entryPath, ['--production'], spawnOpts, meta);
}
if (process.env.NPM_AUTH_TOKEN) {
@@ -667,27 +759,7 @@ export const build = async ({
const staticPages: { [key: string]: FileFsRef } = {};
const dynamicPages: string[] = [];
let static404Page: string | undefined;
let buildId = '';
let page404Path = '';
let escapedBuildId = '';
if (isLegacy || isSharedLambdas) {
try {
buildId = await readFile(
path.join(entryPath, outputDirectory, 'BUILD_ID'),
'utf8'
);
escapedBuildId = escapeStringRegexp(buildId);
} catch (err) {
console.error(
'BUILD_ID not found in ".next". The "package.json" "build" script did not run "next build"'
);
throw new NowBuildError({
code: 'NOW_NEXT_NO_BUILD_ID',
message: 'Missing BUILD_ID',
});
}
}
if (isLegacy) {
const filesAfterBuild = await glob('**', entryPath);
@@ -796,7 +868,10 @@ export const build = async ({
Object.keys(staticPageFiles).forEach((page: string) => {
const pathname = page.replace(/\.html$/, '');
const routeName = normalizePage(pathname);
const routeName = normalizeLocalePath(
normalizePage(pathname),
routesManifest?.i18n?.locales
).pathname;
// Prerendered routes emit a `.html` file but should not be treated as a
// static page.
@@ -829,6 +904,17 @@ export const build = async ({
? path.join(entryDirectory, '_errors/404')
: undefined;
// TODO: locale specific 404s
const { i18n } = routesManifest || {};
if (!static404Page && i18n) {
static404Page = staticPages[
path.join(entryDirectory, i18n.defaultLocale, '404')
]
? path.join(entryDirectory, i18n.defaultLocale, '404')
: undefined;
}
// > 1 because _error is a lambda but isn't used if a static 404 is available
const pageKeys = Object.keys(pages);
let hasLambdas = !static404Page || pageKeys.length > 1;
@@ -1141,25 +1227,36 @@ export const build = async ({
};
}
const pageLambdaRoute: Route = {
src: `^${escapeStringRegexp(outputName).replace(
/\/index$/,
'(/|/index|)'
)}/?$`,
dest: `${path.join('/', currentLambdaGroup.lambdaIdentifier)}`,
headers: {
'x-nextjs-page': outputName,
},
check: true,
const addPageLambdaRoute = (escapedOutputPath: string) => {
const pageLambdaRoute: Route = {
src: `^${escapedOutputPath.replace(/\/index$/, '(/|/index|)')}/?$`,
dest: `${path.join('/', currentLambdaGroup.lambdaIdentifier)}`,
headers: {
'x-nextjs-page': outputName,
},
check: true,
};
// we only need to add the additional routes if shared lambdas
// is enabled
if (routeIsDynamic) {
dynamicPageLambdaRoutes.push(pageLambdaRoute);
dynamicPageLambdaRoutesMap[outputName] = pageLambdaRoute;
} else {
pageLambdaRoutes.push(pageLambdaRoute);
}
};
// we only need to add the additional routes if shared lambdas
// is enabled
if (routeIsDynamic) {
dynamicPageLambdaRoutes.push(pageLambdaRoute);
dynamicPageLambdaRoutesMap[outputName] = pageLambdaRoute;
const { i18n } = routesManifest || {};
if (i18n) {
addPageLambdaRoute(
`[/]?(?:${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})?${escapeStringRegexp(outputName)}`
);
} else {
pageLambdaRoutes.push(pageLambdaRoute);
addPageLambdaRoute(escapeStringRegexp(outputName));
}
if (page === '_error.js' || (hasPages404 && page === '404.js')) {
@@ -1288,7 +1385,32 @@ export const build = async ({
new Set(prerenderManifest.omittedRoutes)
).then(arr =>
arr.map(route => {
route.src = route.src.replace('^', `^${dynamicPrefix}`);
const { i18n } = routesManifest || {};
if (i18n) {
const { pathname } = url.parse(route.dest!);
const isFallback = prerenderManifest.fallbackRoutes[pathname!];
route.src = route.src.replace(
'^',
`^${dynamicPrefix ? `${dynamicPrefix}[/]?` : '[/]?'}(?${
isFallback ? '<nextLocale>' : ':'
}${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})?`
);
if (isFallback) {
// ensure destination has locale prefix to match prerender output
// path so that the prerender object is used
route.dest = route.dest!.replace(
`${path.join('/', entryDirectory, '/')}`,
`${path.join('/', entryDirectory, '$nextLocale', '/')}`
);
}
} else {
route.src = route.src.replace('^', `^${dynamicPrefix}`);
}
return route;
})
);
@@ -1321,6 +1443,31 @@ export const build = async ({
/\/\/ __LAUNCHER_PAGE_HANDLER__/g,
`
const url = require('url');
${
routesManifest?.i18n
? `
function stripLocalePath(pathname) {
// first item will be empty string from splitting at first char
const pathnameParts = pathname.split('/')
;(${JSON.stringify(
routesManifest.i18n.locales
)}).some((locale) => {
if (pathnameParts[1].toLowerCase() === locale.toLowerCase()) {
pathnameParts.splice(1, 1)
pathname = pathnameParts.join('/') || '/'
return true
}
return false
})
return pathname
}
`
: `function stripLocalePath(pathname) { return pathname }`
}
page = function(req, res) {
try {
const pages = {
@@ -1345,7 +1492,7 @@ export const build = async ({
if (!toRender) {
try {
const { pathname } = url.parse(req.url)
toRender = pathname.replace(/\\/$/, '')
toRender = stripLocalePath(pathname).replace(/\\/$/, '')
} catch (_) {
// handle failing to parse url
res.statusCode = 400
@@ -1364,6 +1511,7 @@ export const build = async ({
.replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
.replace(/\\.json$/, '')
toRender = stripLocalePath(toRender)
currentPage = pages[toRender]
}
@@ -1474,7 +1622,15 @@ export const build = async ({
let prerenderGroup = 1;
const onPrerenderRoute = (
routeKey: string,
{ isBlocking, isFallback }: { isBlocking: boolean; isFallback: boolean }
{
isBlocking,
isFallback,
locale,
}: {
isBlocking: boolean;
isFallback: boolean;
locale?: string;
}
) => {
if (isBlocking && isFallback) {
throw new NowBuildError({
@@ -1484,7 +1640,23 @@ export const build = async ({
}
// Get the route file as it'd be mounted in the builder output
const routeFileNoExt = routeKey === '/' ? '/index' : routeKey;
let routeFileNoExt = routeKey === '/' ? '/index' : routeKey;
const origRouteFileNoExt = routeFileNoExt;
const nonDynamicSsg =
!isFallback &&
!isBlocking &&
!prerenderManifest.staticRoutes[routeKey].srcRoute;
// if there isn't a srcRoute then it's a non-dynamic SSG page and
if (nonDynamicSsg || isFallback) {
routeFileNoExt = addLocaleOrDefault(
// root index files are located without folder/index.html
routeFileNoExt,
routesManifest,
locale
);
}
const htmlFsRef = isBlocking
? // Blocking pages do not have an HTML fallback
@@ -1494,7 +1666,11 @@ export const build = async ({
pagesDir,
isFallback
? // Fallback pages have a special file.
prerenderManifest.fallbackRoutes[routeKey].fallback
addLocaleOrDefault(
prerenderManifest.fallbackRoutes[routeKey].fallback,
routesManifest,
locale
)
: // Otherwise, the route itself should exist as a static HTML
// file.
`${routeFileNoExt}.html`
@@ -1533,14 +1709,25 @@ export const build = async ({
}
const outputPathPage = path.posix.join(entryDirectory, routeFileNoExt);
const outputPathPageOrig = path.posix.join(
entryDirectory,
origRouteFileNoExt
);
let lambda: undefined | Lambda;
const outputPathData = path.posix.join(entryDirectory, dataRoute);
let outputPathData = path.posix.join(entryDirectory, dataRoute);
if (nonDynamicSsg || isFallback) {
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${routeFileNoExt}.json`
);
}
if (isSharedLambdas) {
const outputSrcPathPage = path.join(
'/',
srcRoute == null
? outputPathPage
? outputPathPageOrig
: path.join(entryDirectory, srcRoute === '/' ? '/index' : srcRoute)
);
@@ -1549,7 +1736,7 @@ export const build = async ({
} else {
const outputSrcPathPage =
srcRoute == null
? outputPathPage
? outputPathPageOrig
: path.posix.join(
entryDirectory,
srcRoute === '/' ? '/index' : srcRoute
@@ -1598,6 +1785,18 @@ export const build = async ({
++prerenderGroup;
}
if ((nonDynamicSsg || isFallback) && routesManifest?.i18n && !locale) {
// load each locale
for (const locale of routesManifest.i18n.locales) {
if (locale === routesManifest.i18n.defaultLocale) continue;
onPrerenderRoute(routeKey, {
isBlocking,
isFallback,
locale,
});
}
}
};
Object.keys(prerenderManifest.staticRoutes).forEach(route =>
@@ -1726,6 +1925,8 @@ export const build = async ({
}
}
const { i18n } = routesManifest || {};
return {
output: {
...publicDirectoryFiles,
@@ -1736,6 +1937,23 @@ export const build = async ({
...staticFiles,
...staticDirectoryFiles,
},
wildcard: i18n?.domains
? i18n?.domains.map(item => {
return {
domain: item.domain,
value:
item.defaultLocale === i18n.defaultLocale
? ''
: `/${item.defaultLocale}`,
};
})
: undefined,
images: imagesManifest?.images
? {
domains: imagesManifest.images.domains,
sizes: imagesManifest.images.sizes,
}
: undefined,
/*
Desired routes order
- Runtime headers
@@ -1751,14 +1969,135 @@ export const build = async ({
...headers,
// redirects
...redirects,
...redirects.map(_redir => {
if (i18n) {
const redir = _redir as Source;
// detect the trailing slash redirect and make sure it's
// kept above the wildcard mapping to prevent erroneous redirects
// since non-continue routes come after continue the $wildcard
// route will come before the redirect otherwise and if the
// redirect is triggered it breaks locale mapping
const location =
redir.headers && (redir.headers.location || redir.headers.Location);
if (
redir.status === 308 &&
(location === '/$1' || location === '/$1/')
) {
// we set continue true
redir.continue = true;
}
}
return _redir;
}),
...(i18n
? [
// Handle auto-adding current default locale to path based on $wildcard
{
src: `^${path.join(
'/',
entryDirectory,
'/'
)}(?!(?:_next/.*|${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})(?:/.*|$))(.*)$`,
// TODO: this needs to contain or not contain a trailing slash
// to prevent the trailing slash redirect from being triggered
dest: '$wildcard/$1',
continue: true,
},
// Handle redirecting to locale specific domains
...(i18n.domains
? [
{
// TODO: enable redirecting between domains, will require
// updating the src with the desired locales to redirect
src: '/',
locale: {
redirect: i18n.domains.reduce(
(prev: Record<string, string>, item) => {
prev[item.defaultLocale] = `http${
item.http ? '' : 's'
}://${item.domain}/`;
return prev;
},
{}
),
cookie: 'NEXT_LOCALE',
},
continue: true,
},
]
: []),
// Handle redirecting to locale paths
{
// TODO: enable redirecting between paths, will require
// updating the src with the desired locales to redirect.
// if default locale is included in this src it won't be visitable
// by users who prefer another language since the cookie isn't set
// on redirect currently like in `next start`
src: '/',
locale: {
redirect: i18n.locales.reduce(
(prev: Record<string, string>, locale) => {
prev[locale] =
locale === i18n.defaultLocale ? `/` : `/${locale}`;
return prev;
},
{}
),
cookie: 'NEXT_LOCALE',
},
continue: true,
},
{
src: `^${path.join('/', entryDirectory)}$`,
dest: `/${i18n.defaultLocale}`,
continue: true,
},
// Auto-prefix non-locale path with default locale
{
src: `^${path.join(
'/',
entryDirectory,
'/'
)}(?!(?:_next/.*|${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})(?:/.*|$))(.*)$`,
dest: `/${i18n.defaultLocale}/$1`,
continue: true,
},
]
: []),
// Make sure to 404 for the /404 path itself
{
src: path.join('/', entryDirectory, '404'),
status: 404,
continue: true,
},
...(i18n
? [
{
src: `${path.join(
'/',
entryDirectory,
'/'
)}(?:${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})?[/]?404`,
status: 404,
continue: true,
},
]
: [
{
src: path.join('/', entryDirectory, '404'),
status: 404,
continue: true,
},
]),
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
// folder
@@ -1788,6 +2127,40 @@ export const build = async ({
dest: '$0',
},
// remove default locale prefix to check public files
...(i18n
? [
{
src: `${path.join(
'/',
entryDirectory,
i18n.defaultLocale,
'/'
)}(.*)`,
dest: `${path.join('/', entryDirectory, '/')}$1`,
check: true,
},
]
: []),
// for non-shared lambdas remove locale prefix if present
// to allow checking for lambda
...(isSharedLambdas || !i18n
? []
: [
{
src: `${path.join(
'/',
entryDirectory,
'/'
)}(?:${i18n?.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})/(.*)`,
dest: '/$1',
check: true,
},
]),
// routes that are called after each rewrite or after routes
// if there no rewrites
{ handle: 'rewrite' },
@@ -1828,39 +2201,60 @@ export const build = async ({
// Custom Next.js 404 page
{ handle: 'error' } as Handler,
isSharedLambdas
? {
src: path.join('/', entryDirectory, '.*'),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: path.join(
'/',
(static404Page
? static404Page
: pageLambdaMap[page404Path]) as string
),
status: 404,
headers: {
'x-nextjs-page': page404Path,
...(i18n && static404Page
? [
{
src: `${path.join(
'/',
entryDirectory,
'/'
)}(?<nextLocale>${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})(/.*|$)`,
dest: '/$nextLocale/404',
status: 404,
},
}
: {
src: path.join('/', entryDirectory, '.*'),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: static404Page
? path.join('/', static404Page)
: path.join(
'/',
entryDirectory,
hasPages404 &&
lambdas[path.join('./', entryDirectory, '404')]
? '404'
: '_error'
),
status: 404,
},
{
src: path.join('/', entryDirectory, '.*'),
dest: `/${i18n.defaultLocale}/404`,
status: 404,
},
]
: [
isSharedLambdas
? {
src: path.join('/', entryDirectory, '.*'),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: path.join(
'/',
(static404Page
? static404Page
: pageLambdaMap[page404Path]) as string
),
status: 404,
headers: {
'x-nextjs-page': page404Path,
},
}
: {
src: path.join('/', entryDirectory, '.*'),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: static404Page
? path.join('/', static404Page)
: path.join(
'/',
entryDirectory,
hasPages404 &&
lambdas[path.join('./', entryDirectory, '404')]
? '404'
: '_error'
),
status: 404,
},
]),
]),
],
watch: [],

View File

@@ -326,6 +326,15 @@ export type RoutesManifest = {
namedDataRouteRegex?: string;
routeKeys?: { [named: string]: string };
}>;
i18n?: {
defaultLocale: string;
locales: string[];
domains?: Array<{
http?: boolean;
domain: string;
defaultLocale: string;
}>;
};
};
export async function getRoutesManifest(
@@ -502,6 +511,38 @@ export async function getDynamicRoutes(
return routes;
}
type ImagesManifest = {
version: number;
images: {
sizes: number[];
domains: string[];
};
};
export async function getImagesManifest(
entryPath: string,
outputDirectory: string
): Promise<ImagesManifest | undefined> {
const pathImagesManifest = path.join(
entryPath,
outputDirectory,
'images-manifest.json'
);
const hasImagesManifest = await fs
.access(pathImagesManifest)
.then(() => true)
.catch(() => false);
if (!hasImagesManifest) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const imagesManifest: ImagesManifest = require(pathImagesManifest);
return imagesManifest;
}
function syncEnvVars(base: EnvConfig, removeEnv: EnvConfig, addEnv: EnvConfig) {
// Remove any env vars from `removeEnv`
// that are not present in the `addEnv`
@@ -1034,6 +1075,46 @@ function isDirectory(path: string) {
return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
}
export function normalizeLocalePath(
pathname: string,
locales?: string[]
): {
detectedLocale?: string;
pathname: string;
} {
let detectedLocale: string | undefined;
// first item will be empty string from splitting at first char
const pathnameParts = pathname.split('/');
(locales || []).some(locale => {
if (pathnameParts[1].toLowerCase() === locale.toLowerCase()) {
detectedLocale = locale;
pathnameParts.splice(1, 1);
pathname = pathnameParts.join('/') || '/';
return true;
}
return false;
});
return {
pathname,
detectedLocale,
};
}
export function addLocaleOrDefault(
pathname: string,
routesManifest?: RoutesManifest,
locale?: string
) {
if (!routesManifest?.i18n) return pathname;
if (!locale) locale = routesManifest.i18n.defaultLocale;
return locale
? `/${locale}${pathname === '/index' ? '' : pathname}`
: pathname;
}
export {
excludeFiles,
validateEntrypoint,

View File

@@ -0,0 +1,20 @@
module.exports = {
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',
},
],
},
},
};

View File

@@ -0,0 +1,318 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next",
"config": {
"sharedLambdas": false
}
}
],
"probes": [
{
"path": "/",
"headers": {
"accept-language": "en;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//en/"
}
},
{
"path": "/",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//nl/"
}
},
{
"path": "/",
"headers": {
"accept-language": "nl-NL;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//nl-NL/"
}
},
{
"path": "/",
"headers": {
"accept-language": "fr;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//fr/"
}
},
{
"path": "/",
"headers": {
"accept-language": "en-US;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "index page"
},
{
"path": "/en-US",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "index page"
},
{
"path": "/",
"status": 200,
"mustContain": "index page"
},
{
"path": "/",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en",
"status": 200,
"mustContain": "index page"
},
{
"path": "/en",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr",
"status": 200,
"mustContain": "index page"
},
{
"path": "/fr",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl",
"status": 200,
"mustContain": "index page"
},
{
"path": "/nl",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": "index page"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": ">nl-NL<"
},
{
"path": "/non-existent",
"status": 404
},
{
"path": "/fr/non-existent",
"status": 404,
"mustContain": "lang=\"fr\""
},
{
"path": "/en/non-existent",
"status": 404,
"mustContain": "lang=\"en\""
},
{
"path": "/en-US/non-existent",
"status": 404,
"mustContain": "lang=\"en-US\""
},
{
"path": "/nl/non-existent",
"status": 404,
"mustContain": "lang=\"nl\""
},
{
"path": "/nl-NL/non-existent",
"status": 404,
"mustContain": "lang=\"nl-NL\""
},
{
"path": "/hello.txt",
"status": 200,
"mustContain": "hello world!"
},
{
"path": "/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/gsp",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/en/gsp",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/nl/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/nl/gsp",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/fr/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/fr/gsp",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/gssp",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/en/gssp",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/nl/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/nl/gssp",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/fr/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/fr/gssp",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="another">another page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,21 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="auto-export">auto-export page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
</>
);
}

View File

@@ -0,0 +1,44 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};
export const getStaticPaths = () => {
return {
// the default locale will be used since one isn't defined here
paths: ['first', 'second'].map(slug => ({
params: { slug },
})),
fallback: true,
};
};

View File

@@ -0,0 +1,32 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
// TODO: should non-dynamic GSP pages pre-render for each locale?
export const getStaticProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,46 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};
export const getStaticPaths = () => {
return {
paths: [
{ params: { slug: 'first' } },
'/gsp/no-fallback/second',
{ params: { slug: 'first' }, locale: 'en-US' },
'/nl-NL/gsp/no-fallback/second',
],
fallback: false,
};
};

View File

@@ -0,0 +1,32 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gssp">gssp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};

View File

@@ -0,0 +1,31 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gssp">gssp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,46 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="index">index page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another">
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp">
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first">
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello">
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first">
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp">
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first">
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
);
}

View File

@@ -0,0 +1,54 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
const { nextLocale } = router.query;
return (
<>
<p id="links">links page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another" locale={nextLocale}>
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp" locale={nextLocale}>
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first" locale={nextLocale}>
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello" locale={nextLocale}>
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first" locale={nextLocale}>
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp" locale={nextLocale}>
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first" locale={nextLocale}>
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
);
}
// make SSR page so we have query values immediately
export const getServerSideProps = () => {
return {
props: {},
};
};

View File

@@ -0,0 +1 @@
hello world!

View File

@@ -0,0 +1,20 @@
module.exports = {
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',
},
],
},
},
};

View File

@@ -0,0 +1,315 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"probes": [
{
"path": "/",
"headers": {
"accept-language": "en;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//en/"
}
},
{
"path": "/",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//nl/"
}
},
{
"path": "/",
"headers": {
"accept-language": "nl-NL;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//nl-NL/"
}
},
{
"path": "/",
"headers": {
"accept-language": "fr;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//fr/"
}
},
{
"path": "/",
"headers": {
"accept-language": "en-US;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "index page"
},
{
"path": "/en-US",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "index page"
},
{
"path": "/",
"status": 200,
"mustContain": "index page"
},
{
"path": "/",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en",
"status": 200,
"mustContain": "index page"
},
{
"path": "/en",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr",
"status": 200,
"mustContain": "index page"
},
{
"path": "/fr",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl",
"status": 200,
"mustContain": "index page"
},
{
"path": "/nl",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": "index page"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": ">nl-NL<"
},
{
"path": "/non-existent",
"status": 404
},
{
"path": "/fr/non-existent",
"status": 404,
"mustContain": "lang=\"fr\""
},
{
"path": "/en/non-existent",
"status": 404,
"mustContain": "lang=\"en\""
},
{
"path": "/en-US/non-existent",
"status": 404,
"mustContain": "lang=\"en-US\""
},
{
"path": "/nl/non-existent",
"status": 404,
"mustContain": "lang=\"nl\""
},
{
"path": "/nl-NL/non-existent",
"status": 404,
"mustContain": "lang=\"nl-NL\""
},
{
"path": "/hello.txt",
"status": 200,
"mustContain": "hello world!"
},
{
"path": "/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/gsp",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/en/gsp",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/nl/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/nl/gsp",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/fr/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/fr/gsp",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/gssp",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/en/gssp",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/nl/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/nl/gssp",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/fr/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/fr/gssp",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="another">another page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,21 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="auto-export">auto-export page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
</>
);
}

View File

@@ -0,0 +1,44 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};
export const getStaticPaths = () => {
return {
// the default locale will be used since one isn't defined here
paths: ['first', 'second'].map(slug => ({
params: { slug },
})),
fallback: true,
};
};

View File

@@ -0,0 +1,32 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
// TODO: should non-dynamic GSP pages pre-render for each locale?
export const getStaticProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,46 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};
export const getStaticPaths = () => {
return {
paths: [
{ params: { slug: 'first' } },
'/gsp/no-fallback/second',
{ params: { slug: 'first' }, locale: 'en-US' },
'/nl-NL/gsp/no-fallback/second',
],
fallback: false,
};
};

View File

@@ -0,0 +1,32 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gssp">gssp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};

View File

@@ -0,0 +1,31 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gssp">gssp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,55 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="index">index page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another">
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp">
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first">
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello">
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first">
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp">
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first">
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,54 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
const { nextLocale } = router.query;
return (
<>
<p id="links">links page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another" locale={nextLocale}>
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp" locale={nextLocale}>
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first" locale={nextLocale}>
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello" locale={nextLocale}>
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first" locale={nextLocale}>
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp" locale={nextLocale}>
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first" locale={nextLocale}>
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
);
}
// make SSR page so we have query values immediately
export const getServerSideProps = () => {
return {
props: {},
};
};

View File

@@ -0,0 +1 @@
hello world!

View File

@@ -207,7 +207,9 @@ it(
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
contents.some(name =>
name.includes('next.config.__vercel_builder_backup__')
)
).toBeTruthy();
},
FOUR_MINUTES
@@ -278,7 +280,9 @@ it(
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
contents.some(name =>
name.includes('next.config.__vercel_builder_backup__')
)
).toBeTruthy();
},
FOUR_MINUTES
@@ -345,7 +349,9 @@ it(
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
contents.some(name =>
name.includes('next.config.__vercel_builder_backup__')
)
).toBeTruthy();
},
FOUR_MINUTES
@@ -380,7 +386,9 @@ it(
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
contents.some(name =>
name.includes('next.config.__vercel_builder_backup__')
)
).toBeFalsy();
},
FOUR_MINUTES
@@ -422,7 +430,9 @@ it(
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
contents.some(name =>
name.includes('next.config.__vercel_builder_backup__')
)
).toBeFalsy();
},
FOUR_MINUTES

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/routing-utils",
"version": "1.9.0",
"version": "1.9.1-canary.1",
"description": "Vercel routing utilities",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@@ -60,6 +60,10 @@ export const routesSchema = {
type: 'string',
maxLength: 4096,
},
cookie: {
type: 'string',
maxLength: 4096,
},
default: {
type: 'string',
maxLength: 4096,

View File

@@ -17,6 +17,10 @@ export type Source = {
continue?: boolean;
check?: boolean;
status?: number;
locale?: {
redirect: Record<string, string>;
cookie: string;
};
};
export type Handler = {

View File

@@ -42,6 +42,7 @@ describe('normalizeRoutes', () => {
value: '$value',
path: '$path',
default: 'en',
cookie: 'NEXT_LOCALE',
},
},
{

View File

@@ -4793,21 +4793,16 @@ escape-html@1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
escape-string-regexp@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-3.0.0.tgz#1dad9cc28aed682be0de197280f79911a5fccd61"
integrity sha512-11dXIUC3umvzEViLP117d0KN6LJzZxh5+9F4E/7WLAAw7GrHk8NpUR+g9iJi/pe9C0py4F8rs0hreyRCwlAuZg==
escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
escape-string-regexp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escodegen@^1.8.0, escodegen@^1.9.1:
version "1.14.2"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.2.tgz#14ab71bf5026c2aa08173afba22c6f3173284a84"