Compare commits

..

25 Commits

Author SHA1 Message Date
JJ Kasper
4eb5ad625c Publish Stable
- @vercel/next@2.6.35
2020-11-02 10:26:52 -06:00
luc
7164f6e58e Publish Canary
- vercel@20.1.3-canary.2
2020-11-02 15:43:54 +01:00
Naoyuki Kanezawa
a36d084b3e [cli] Add support for plaintext env variables (#5345)
* fix plain envs can not be retrieved

* add test

* add test for `vc dev`

* use v6 to retrieve env variables

* fix v6 return type

* use v6 to display env vars

* add test for vc env ls

* expand env vars with multiple targets

* minor type fixes

* always use v6 for getEnvVariables

Co-authored-by: luc <luc.leray@gmail.com>
2020-11-02 15:42:28 +01:00
JJ Kasper
8a16447fed Publish Canary
- vercel@20.1.3-canary.1
 - @vercel/next@2.6.35-canary.1
2020-11-01 12:24:51 -06:00
Leo Lamprecht
efda4ab6b9 Updated Next.js Template to use latest version of Next.js and React (#5358) 2020-11-01 17:13:16 +01:00
JJ Kasper
16060a71a9 [next] Add additional tests for i18n revalidate params (#5356) 2020-11-01 15:15:20 +01:00
Luc Leray
b18e0a7415 [cli] Replace util/prompt-bool by util/input/confirm (#5359) 2020-10-31 22:07:35 +01:00
JJ Kasper
1251f11a97 Publish Canary
- @vercel/next@2.6.35-canary.0
2020-10-31 00:34:03 -05:00
JJ Kasper
07235e22f6 [next] Fix root-most optional-catchall with i18n (#5354)
* Fix root-most optional-catchall with i18n

* Ensure dynamic prerenders revalidation

* Add test delay
2020-10-30 17:40:39 -05:00
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
JJ Kasper
7470ff3724 Publish Stable
- @vercel/next@2.6.32
2020-10-27 09:30:43 -05:00
JJ Kasper
8340d9327c Publish Canary
- @vercel/next@2.6.32-canary.1
2020-10-27 09:11:04 -05:00
JJ Kasper
d278425810 Update tests for stabilized field (#5337) 2020-10-27 09:05:39 -05:00
JJ Kasper
8b26bbe643 [next] Add redirecting domain specific locales (#5333)
Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
2020-10-27 11:32:08 +01:00
JJ Kasper
fa8e1e73c8 Ensure index GSP data is available at correct path (#5336) 2020-10-27 01:51:58 -05:00
JJ Kasper
f8abcbcd9f Remove unstable_ prefix from unstable_blocking (#5334) 2020-10-27 00:42:11 -05:00
Andy Bitz
e18ff683b2 Publish Canary
- @vercel/frameworks@0.1.2-canary.0
 - @vercel/build-utils@2.5.5-canary.0
 - vercel@20.1.3-canary.0
 - @vercel/client@9.0.4-canary.0
 - @vercel/redwood@0.1.2-canary.0
2020-10-26 16:22:53 +01:00
Andy
f28293a5a8 [frameworks] Add recommended integrations and related dependencies (#5330)
Adds the `recommendedIntegrations` property to the frameworks list with related dependencies.

Story https://app.clubhouse.io/vercel/story/13391
2020-10-26 15:10:40 +00:00
Steven
a4963a89c7 Publish Canary
- @vercel/next@2.6.32-canary.0
2020-10-25 16:58:01 -04:00
Steven
21df39fe8c [next] Image Optimization for default loader (#5321)
We currently pass through `images` whenever its defined, but this is enabling Image Optimization in the Proxy for every Next.js project.

Instead, we should check to see if the default loader is used (the same use for `next dev`) as a signal to enable this feature in the deployment.

Related to https://github.com/vercel/next.js/issues/18122
2020-10-24 12:55:53 +00:00
48 changed files with 1368 additions and 220 deletions

View File

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

View File

@@ -56,7 +56,13 @@
"outputDirectory": {
"placeholder": "Next.js default"
}
}
},
"recommendedIntegrations": [
{
"id": "oac_5lUsiANun1DEzgLg0NZx5Es3",
"dependencies": ["next-plugin-sentry", "next-sentry-source-maps"]
}
]
},
{
"name": "Gatsby.js",

View File

@@ -31,4 +31,8 @@ export interface Framework {
devCommand: Setting;
outputDirectory: Setting;
};
recommendedIntegrations?: {
id: string;
dependencies: string[];
}[];
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.1.1",
"version": "0.1.2-canary.0",
"main": "frameworks.json",
"license": "UNLICENSED",
"scripts": {

View File

@@ -97,6 +97,25 @@ const Schema = {
outputDirectory: SchemaSettings,
},
},
recommendedIntegrations: {
type: 'array',
items: {
type: 'object',
required: ['id', 'dependencies'],
additionalProperties: false,
properties: {
id: {
type: 'string',
},
dependencies: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
},
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.5.4",
"version": "2.5.5-canary.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -29,7 +29,7 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.1.1",
"@vercel/frameworks": "0.1.2-canary.0",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "20.1.2",
"version": "20.1.3-canary.2",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -61,7 +61,7 @@
"node": ">= 10"
},
"dependencies": {
"@vercel/build-utils": "2.5.4",
"@vercel/build-utils": "2.5.5-canary.0",
"@vercel/go": "1.1.6",
"@vercel/node": "1.8.4",
"@vercel/python": "1.2.3",
@@ -100,7 +100,7 @@
"@types/universal-analytics": "0.4.2",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.1.1",
"@vercel/frameworks": "0.1.2-canary.0",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",

View File

@@ -7,7 +7,7 @@ import getScope from '../../util/get-scope.ts';
import removeAliasById from '../../util/alias/remove-alias-by-id';
import stamp from '../../util/output/stamp.ts';
import strlen from '../../util/strlen.ts';
import promptBool from '../../util/prompt-bool';
import confirm from '../../util/input/confirm';
import { isValidName } from '../../util/is-valid-name';
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
import { getCommandName } from '../../util/pkg-name.ts';
@@ -108,5 +108,5 @@ async function confirmAliasRemove(output, alias) {
output.log(`The following alias will be removed permanently`);
output.print(` ${tbl}\n`);
return promptBool(output, chalk.red('Are you sure?'));
return confirm(chalk.red('Are you sure?'), false);
}

View File

@@ -77,7 +77,7 @@ export default async function add(
}
}
const envs = await getEnvVariables(output, client, project.id, 4);
const { envs } = await getEnvVariables(output, client, project.id);
const existing = new Set(
envs.filter(r => r.key === envName).map(r => r.target)
);

View File

@@ -42,7 +42,6 @@ const help = () => {
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-N, --next Show next page of results
${chalk.dim('Examples:')}
@@ -73,12 +72,6 @@ const help = () => {
${chalk.cyan(`$ ${getPkgName()} env rm <name> ${placeholder}`)}
${chalk.cyan(`$ ${getPkgName()} env rm NPM_RC preview`)}
${chalk.gray('')} Paginate results, where ${chalk.dim(
'`1584722256178`'
)} is the time in milliseconds since the UNIX epoch.
${chalk.cyan(`$ ${getPkgName()} env ls --next 1584722256178`)}
`);
};
@@ -96,8 +89,6 @@ export default async function main(ctx: NowContext) {
argv = getArgs(ctx.argv.slice(2), {
'--yes': Boolean,
'-y': '--yes',
'--next': Number,
'-N': '--next',
});
} catch (error) {
handleError(error);

View File

@@ -1,7 +1,7 @@
import chalk from 'chalk';
import ms from 'ms';
import { Output } from '../../util/output';
import { ProjectEnvVariable, ProjectEnvTarget, Project } from '../../types';
import { ProjectEnvTarget, Project, ProjectEnvVariableV5 } from '../../types';
import Client from '../../util/client';
import formatTable from '../../util/format-table';
import getEnvVariables from '../../util/env/get-env-records';
@@ -11,12 +11,11 @@ import {
} from '../../util/env/env-target';
import stamp from '../../util/output/stamp';
import param from '../../util/output/param';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import ellipsis from '../../util/output/ellipsis';
type Options = {
'--debug': boolean;
'--next'?: number;
};
export default async function ls(
@@ -26,8 +25,6 @@ export default async function ls(
args: string[],
output: Output
) {
const { '--next': nextTimestamp } = opts;
if (args.length > 1) {
output.error(
`Invalid number of arguments. Usage: ${getCommandName(
@@ -50,40 +47,31 @@ export default async function ls(
const lsStamp = stamp();
if (typeof nextTimestamp !== 'undefined' && Number.isNaN(nextTimestamp)) {
output.error('Please provide a number for flag --next');
return 1;
const data = await getEnvVariables(output, client, project.id, envTarget);
// we expand env vars with multiple targets
const envs: ProjectEnvVariableV5[] = [];
for (let env of data.envs) {
if (Array.isArray(env.target)) {
for (let target of env.target) {
envs.push({ ...env, target });
}
} else {
envs.push({ ...env, target: env.target });
}
}
const data = await getEnvVariables(
output,
client,
project.id,
5,
envTarget,
nextTimestamp
);
const { envs: records, pagination } = data;
output.log(
`${
records.length > 0 ? 'Environment Variables' : 'No Environment Variables'
envs.length > 0 ? 'Environment Variables' : 'No Environment Variables'
} found in Project ${chalk.bold(project.name)} ${chalk.gray(lsStamp())}`
);
console.log(getTable(records));
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page run ${getCommandName(
`env ls${flags} --next ${pagination.next}`
)}`
);
}
console.log(getTable(envs));
return 0;
}
function getTable(records: ProjectEnvVariable[]) {
function getTable(records: ProjectEnvVariableV5[]) {
return formatTable(
['name', 'value', 'environment', 'created'],
['l', 'l', 'l', 'l', 'l'],
@@ -96,17 +84,25 @@ function getTable(records: ProjectEnvVariable[]) {
);
}
function getRow({
key,
system = false,
target,
createdAt = 0,
}: ProjectEnvVariable) {
function getRow(env: ProjectEnvVariableV5) {
let value: string;
if (env.type === 'plain') {
// replace space characters (line-break, etc.) with simple spaces
// to make sure the displayed value is a single line
const singleLineValue = env.value.replace(/\s/g, ' ');
value = chalk.gray(ellipsis(singleLineValue, 19));
} else if (env.type === 'system') {
value = chalk.gray.italic('Populated by System');
} else {
value = chalk.gray.italic('Encrypted');
}
const now = Date.now();
return [
chalk.bold(key),
chalk.gray(chalk.italic(system ? 'Populated by System' : 'Encrypted')),
target || '',
`${ms(now - createdAt)} ago`,
chalk.bold(env.key),
value,
env.target || '',
env.createdAt ? `${ms(now - env.createdAt)} ago` : '',
];
}

View File

@@ -1,7 +1,7 @@
import chalk from 'chalk';
import { ProjectEnvTarget, Project } from '../../types';
import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import confirm from '../../util/input/confirm';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
@@ -68,9 +68,9 @@ export default async function pull(
} else if (
exists &&
!skipConfirmation &&
!(await promptBool(
output,
`Found existing file ${param(filename)}. Do you want to overwrite?`
!(await confirm(
`Found existing file ${param(filename)}. Do you want to overwrite?`,
false
))
) {
output.log('Aborted');

View File

@@ -2,7 +2,7 @@ import chalk from 'chalk';
import inquirer from 'inquirer';
import { ProjectEnvTarget, Project } from '../../types';
import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import confirm from '../../util/input/confirm';
import removeEnvRecord from '../../util/env/remove-env-record';
import getEnvVariables from '../../util/env/get-env-records';
import {
@@ -69,7 +69,7 @@ export default async function rm(
envName = inputName;
}
const envs = await getEnvVariables(output, client, project.id, 4);
const { envs } = await getEnvVariables(output, client, project.id);
const existing = new Set(
envs.filter(r => r.key === envName).map(r => r.target)
);
@@ -104,11 +104,11 @@ export default async function rm(
const skipConfirmation = opts['--yes'];
if (
!skipConfirmation &&
!(await promptBool(
output,
!(await confirm(
`Removing Environment Variable ${param(
envName
)} from Project ${chalk.bold(project.name)}. Are you sure?`
)} from Project ${chalk.bold(project.name)}. Are you sure?`,
false
))
) {
output.log('Aborted');

View File

@@ -203,12 +203,19 @@ export enum ProjectEnvTarget {
Development = 'development',
}
export type ProjectEnvVariableType = 'system' | 'secret' | 'plain';
export interface ProjectEnvVariable {
key: string;
value: string;
type: ProjectEnvVariableType;
configurationId?: string | null;
createdAt?: number;
updatedAt?: number;
target?: ProjectEnvTarget | ProjectEnvTarget[];
}
export interface ProjectEnvVariableV5 extends ProjectEnvVariable {
target?: ProjectEnvTarget;
system?: boolean;
}

View File

@@ -1,6 +1,6 @@
import { Output } from '../output';
import Client from '../client';
import { Secret, ProjectEnvTarget, ProjectEnvVariable } from '../../types';
import { Secret, ProjectEnvTarget, ProjectEnvVariableV5 } from '../../types';
import { customAlphabet } from 'nanoid';
import slugify from '@sindresorhus/slugify';
@@ -42,7 +42,7 @@ export default async function addEnvRecord(
}));
const urlProject = `/v4/projects/${projectId}/env`;
await client.fetch<ProjectEnvVariable>(urlProject, {
await client.fetch<ProjectEnvVariableV5>(urlProject, {
method: 'POST',
body: JSON.stringify(body),
});

View File

@@ -1,69 +1,24 @@
import { Output } from '../output';
import Client from '../client';
import {
ProjectEnvVariable,
ProjectEnvTarget,
PaginationOptions,
} from '../../types';
import { ProjectEnvVariable, ProjectEnvTarget } from '../../types';
import { URLSearchParams } from 'url';
type ApiVersion = 4 | 5;
type APIV4Response = ProjectEnvVariable[];
interface APIV5Response {
pagination: PaginationOptions;
envs: ProjectEnvVariable[];
}
export default async function getEnvVariables(
output: Output,
client: Client,
projectId: string,
apiVersion: 4,
target?: ProjectEnvTarget
): Promise<APIV4Response>;
export default async function getEnvVariables(
output: Output,
client: Client,
projectId: string,
apiVersion: 5,
target?: ProjectEnvTarget,
next?: number
): Promise<APIV5Response>;
export default async function getEnvVariables<V extends ApiVersion>(
output: Output,
client: Client,
projectId: string,
apiVersion: V,
target?: ProjectEnvTarget,
next?: number
) {
output.debug(
`Fetching Environment Variables of project ${projectId} and target ${target}`
);
const query = new URLSearchParams();
if (apiVersion >= 5) {
query.set('limit', String(20));
}
if (target) {
query.set('target', target);
}
if (next) {
query.set('until', String(next));
}
const url = `/v6/projects/${projectId}/env?${query}`;
const url = `/v${apiVersion}/projects/${projectId}/env?${query}`;
if (apiVersion === 5) {
return client.fetch<APIV5Response>(url);
} else if (apiVersion === 4) {
return client.fetch<APIV4Response>(url);
} else {
throw new Error('Unknown version: ' + apiVersion);
}
return client.fetch<{ envs: ProjectEnvVariable[] }>(url);
}

View File

@@ -1,6 +1,6 @@
import { Output } from '../output';
import Client from '../client';
import { ProjectEnvTarget, Secret, ProjectEnvVariable } from '../../types';
import { ProjectEnvTarget, Secret, ProjectEnvVariableV5 } from '../../types';
export default async function removeEnvRecord(
output: Output,
@@ -18,7 +18,7 @@ export default async function removeEnvRecord(
envName
)}${qs}`;
const env = await client.fetch<ProjectEnvVariable>(urlProject, {
const env = await client.fetch<ProjectEnvVariableV5>(urlProject, {
method: 'DELETE',
});

View File

@@ -12,9 +12,15 @@ export default async function getDecryptedEnvRecords(
project: Project,
target: ProjectEnvTarget
): Promise<Env> {
const envs = await getEnvVariables(output, client, project.id, 4, target);
const { envs } = await getEnvVariables(output, client, project.id, target);
const decryptedValues = await Promise.all(
envs.map(async env => {
if (env.type === 'system') {
return { value: '', found: true };
} else if (env.type === 'plain') {
return { value: env.value, found: true };
}
try {
const value = await getDecryptedSecret(output, client, env.value);
return { value, found: true };

View File

@@ -0,0 +1,3 @@
export default function ellipsis(str: string, length: number) {
return str.length > length ? `${str.slice(0, length - 1)}` : str;
}

View File

@@ -1,21 +0,0 @@
import chalk from 'chalk';
import { Output } from './output';
async function promptBool(output: Output, message: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
output.print(`${chalk.gray('>')} ${message} ${chalk.gray('[y/N] ')}`);
process.stdin
.on('data', d => {
process.stdin.pause();
resolve(
d
.toString()
.trim()
.toLowerCase() === 'y'
);
})
.resume();
});
}
export default promptBool;

View File

@@ -422,6 +422,34 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
function withPlainTextEnv(fn) {
return async function (...args) {
const link = require(path.join(target, '.vercel/project.json'));
const postRes = await apiFetch(`/v6/projects/${link.projectId}/env`, {
method: 'POST',
body: JSON.stringify({
type: 'plain',
key: 'MY_PLAIN_VAR',
value: 'hello',
target: ['development'],
}),
});
t.is(postRes.status, 200);
try {
return await fn(...args);
} finally {
const deleteRes = await apiFetch(
`/v4/projects/${link.projectId}/env/MY_PLAIN_VAR?target=development`,
{
method: 'DELETE',
}
);
t.is(deleteRes.status, 200);
}
};
}
async function nowEnvLsIsEmpty() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
@@ -530,6 +558,11 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.regex(vercelVars.join('\n'), /development/gm);
t.regex(vercelVars.join('\n'), /preview/gm);
t.regex(vercelVars.join('\n'), /production/gm);
const myPlainVars = lines.filter(line => line.includes('MY_PLAIN_VAR'));
t.is(myPlainVars.length, 1);
t.regex(myPlainVars.join('\n'), /development/gm);
t.regex(myPlainVars.join('\n'), /hello/gm);
}
async function nowEnvPull() {
@@ -552,6 +585,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.true(lines.has('MY_ENV_VAR="MY_VALUE"'));
t.true(lines.has('MY_STDIN_VAR="{"expect":"quotes"}"'));
t.true(lines.has('VERCEL_URL=""'));
t.true(lines.has('MY_PLAIN_VAR="hello"'));
}
async function nowEnvPullOverwrite() {
@@ -681,12 +715,14 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(apiJson['MY_PLAIN_VAR'], 'hello');
const homeUrl = localhost[0];
const homeRes = await fetch(homeUrl);
const homeJson = await homeRes.json();
t.is(homeJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
t.is(homeJson['MY_PLAIN_VAR'], 'hello');
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
@@ -755,14 +791,14 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
await nowEnvAdd();
await nowEnvAddFromStdin();
await nowEnvAddSystemEnv();
await nowEnvLsIncludesVar();
await nowEnvPull();
await withPlainTextEnv(nowEnvLsIncludesVar)();
await withPlainTextEnv(nowEnvPull)();
await nowEnvPullOverwrite();
await nowEnvPullConfirm();
await nowDeployWithVar();
await nowDevWithEnv();
fs.unlinkSync(path.join(target, '.env'));
await nowDevAndFetchCloudVars();
await withPlainTextEnv(nowDevAndFetchCloudVars)();
await nowEnvRemove();
await nowEnvRemoveWithArgs();
await nowEnvRemoveWithNameOnly();

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "9.0.3",
"version": "9.0.4-canary.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -37,7 +37,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "2.5.4",
"@vercel/build-utils": "2.5.5-canary.0",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

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

View File

@@ -511,6 +511,7 @@ export const build = async ({
const { i18n } = routesManifest;
if (i18n) {
const origSrc = route.src;
route.src = route.src.replace(
// we need to double escape the build ID here
// to replace it properly
@@ -522,6 +523,21 @@ export const build = async ({
.join('|')})/`
);
// optional-catchall routes don't have slash between
// build-id and the regex
if (route.src === origSrc) {
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(
@@ -655,12 +671,13 @@ export const build = async ({
return {
output,
images: imagesManifest?.images
? {
domains: imagesManifest.images.domains,
sizes: imagesManifest.images.sizes,
}
: undefined,
images:
imagesManifest?.images?.loader === 'default'
? {
domains: imagesManifest.images.domains,
sizes: imagesManifest.images.sizes,
}
: undefined,
routes: [
// User headers
...headers,
@@ -879,7 +896,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;
}
@@ -1390,17 +1409,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(
@@ -1456,7 +1477,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
@@ -1492,7 +1513,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
@@ -1511,7 +1532,7 @@ export const build = async ({
.replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
.replace(/\\.json$/, '')
toRender = stripLocalePath(toRender)
toRender = stripLocalePath(toRender) || '/index'
currentPage = pages[toRender]
}
@@ -1539,8 +1560,9 @@ 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 }, "in lambda"
)
console.error('pages in lambda', Object.keys(pages))
res.statusCode = 500
return res.end('internal server error')
}
@@ -1723,7 +1745,12 @@ export const build = async ({
if (nonDynamicSsg || isFallback) {
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${routeFileNoExt}.json`
`${routeFileNoExt}${
routeFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
}
@@ -1788,6 +1815,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) {
@@ -1952,12 +2017,13 @@ export const build = async ({
};
})
: undefined,
images: imagesManifest?.images
? {
domains: imagesManifest.images.domains,
sizes: imagesManifest.images.sizes,
}
: undefined,
images:
imagesManifest?.images?.loader === 'default'
? {
domains: imagesManifest.images.domains,
sizes: imagesManifest.images.sizes,
}
: undefined,
/*
Desired routes order
- Runtime headers
@@ -2019,13 +2085,26 @@ export const build = async ({
{
// TODO: enable redirecting between domains, will require
// updating the src with the desired locales to redirect
src: '/',
src: `^${path.join(
'/',
entryDirectory
)}/?(?:${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})?/?$`,
locale: {
redirect: i18n.domains.reduce(
(prev: Record<string, string>, item) => {
prev[item.defaultLocale] = `http${
item.http ? '' : 's'
}://${item.domain}/`;
if (item.locales) {
item.locales.map(locale => {
prev[locale] = `http${item.http ? '' : 's'}://${
item.domain
}/${locale}`;
});
}
return prev;
},
{}
@@ -2066,6 +2145,10 @@ export const build = async ({
},
// Auto-prefix non-locale path with default locale
// note for prerendered pages this will cause
// x-now-route-matches to contain the path minus the locale
// e.g. for /de/posts/[slug] x-now-route-matches would have
// 1=posts%2Fpost-1
{
src: `^${path.join(
'/',
@@ -2108,10 +2191,22 @@ export const build = async ({
{ handle: 'filesystem' },
// map pages to their lambda
...pageLambdaRoutes,
...pageLambdaRoutes.filter(route => {
// filter out any SSG pages as they are already present in output
if ('headers' in route) {
let page = route.headers?.['x-nextjs-page']!;
page = page === '/index' ? '/' : page;
// map /blog/[post] to correct lambda for iSSG
...dynamicPageLambdaRoutes,
if (
prerenderManifest.staticRoutes[page] ||
prerenderManifest.fallbackRoutes[page] ||
prerenderManifest.blockingFallbackRoutes[page]
) {
return false;
}
}
return true;
}),
// These need to come before handle: miss or else they are grouped
// with that routing section

View File

@@ -332,6 +332,7 @@ export type RoutesManifest = {
domains?: Array<{
http?: boolean;
domain: string;
locales?: string[];
defaultLocale: string;
}>;
};
@@ -511,9 +512,12 @@ export async function getDynamicRoutes(
return routes;
}
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default';
type ImagesManifest = {
version: number;
images: {
loader: LoaderKey;
sizes: number[];
domains: string[];
};

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

@@ -26,7 +26,7 @@ export default function Page(props) {
export const getStaticProps = ({ params, locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
unstable_notFound: true,
notFound: true,
};
}

View File

@@ -24,7 +24,7 @@ export default function Page(props) {
export const getStaticProps = ({ locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
unstable_notFound: true,
notFound: true,
};
}

View File

@@ -0,0 +1,182 @@
/* eslint-env jest */
const fetch = require('node-fetch');
const cheerio = require('cheerio');
module.exports = function (ctx) {
it('should revalidate content properly from /', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
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 () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
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 () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
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 /second', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/second`);
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}/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('en-US');
});
it('should revalidate content properly from /fr/second', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr/second`);
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/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('fr');
});
it('should revalidate content properly from /nl-NL/second', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/second`);
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/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

@@ -0,0 +1,21 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
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,192 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"probes": [
{
"path": "/hello.txt",
"status": 200,
"mustContain": "hello world!"
},
{
"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": "catchall page"
},
{
"path": "/en-US",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/en",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/fr",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": ">nl-NL<"
},
{
"path": "/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/first",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/en/first",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/fr/first",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl/first",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl-NL/first",
"status": 200,
"mustContain": ">nl-NL<"
}
]
}

View File

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

View File

@@ -0,0 +1,69 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
const Slug = props => {
const router = useRouter();
// invariant ensuring fallback is never accidentally flipped
if (router.isFallback) {
return 'Loading...';
}
return (
<div>
<p>catchall 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="/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>
<br />
<Link href="/">
<a>/</a>
</Link>
</div>
);
};
export const getStaticProps = ({ params }) => {
return {
props: {
params,
random: Math.random(),
catchall: 'yes',
},
revalidate: 1,
};
};
export const getStaticPaths = ({ locales }) => {
const paths = [];
for (const locale of locales) {
paths.push({ params: { slug: ['first'] }, locale });
paths.push({ params: { slug: ['first'] }, locale });
}
return {
paths,
fallback: 'blocking',
};
};
export default Slug;

View File

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

View File

@@ -0,0 +1,412 @@
/* eslint-env jest */
const fetch = require('node-fetch');
const cheerio = require('cheerio');
module.exports = function (ctx) {
it('should revalidate content properly from /', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
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');
expect(JSON.parse($('#router-query').text())).toEqual({});
// 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');
expect(JSON.parse($('#router-query').text())).toEqual({});
});
it('should revalidate content properly from /fr', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
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');
expect(JSON.parse($('#router-query').text())).toEqual({});
// 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');
expect(JSON.parse($('#router-query').text())).toEqual({});
});
it('should revalidate content properly from /nl-NL', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
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');
expect(JSON.parse($('#router-query').text())).toEqual({});
// 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');
expect(JSON.parse($('#router-query').text())).toEqual({});
});
it('should revalidate content properly from /gsp/fallback/first', async () => {
// check the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/gsp/fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/gsp/fallback/first`);
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');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/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');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /fr/gsp/fallback/first', async () => {
// check the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/gsp/fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/first`);
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');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/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');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /nl-NL/gsp/fallback/first', async () => {
// check the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/gsp/fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/first`);
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');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/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('nl-NL');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
//
it('should revalidate content properly from /gsp/fallback/new-page', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/gsp/fallback/new-page.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
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');
expect(props.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
// 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');
expect(props2.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
});
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');
expect(props.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
// 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');
expect(props.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
// 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');
expect(props2.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
});
it('should revalidate content properly from /gsp/no-fallback/first', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/gsp/no-fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
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');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// 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');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /fr/gsp/no-fallback/first', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/gsp/no-fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
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');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// 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');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /nl-NL/gsp/no-fallback/second', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/gsp/no-fallback/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
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');
expect(props.params).toEqual({ slug: 'second' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'second' });
// 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');
expect(props2.params).toEqual({ slug: 'second' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'second' });
});
};

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

@@ -348,6 +348,9 @@
"status": 200,
"mustContain": "lang=\"en\""
},
{
"delay": 2000
},
{
"path": "/en/not-found/fallback/first",
"status": 200,
@@ -381,10 +384,101 @@
"status": 200,
"mustContain": "lang=\"fr\""
},
{
"delay": 2000
},
{
"path": "/fr/not-found/fallback/first",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/_next/data/testing-build-id/en-US/index.json",
"status": 200,
"mustContain": "\"locale\":\"en-US\""
},
{
"path": "/_next/data/testing-build-id/en/index.json",
"status": 200,
"mustContain": "\"locale\":\"en\""
},
{
"path": "/_next/data/testing-build-id/fr/index.json",
"status": 200,
"mustContain": "\"locale\":\"fr\""
},
{
"path": "/_next/data/testing-build-id/nl/index.json",
"status": 200,
"mustContain": "\"locale\":\"nl\""
},
{
"path": "/_next/data/testing-build-id/en-US/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"en-US\""
},
{
"path": "/_next/data/testing-build-id/en/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"en\""
},
{
"path": "/_next/data/testing-build-id/fr/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"fr\""
},
{
"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,19 +26,26 @@ export default function Page(props) {
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
random: Math.random(),
params,
locale,
locales,
},
revalidate: 1,
};
};
export const getStaticPaths = () => {
export const getStaticPaths = ({ locales }) => {
const paths = [];
for (const locale of locales) {
paths.push({ params: { slug: 'first' }, locale });
paths.push({ params: { slug: 'second' }, locale });
}
return {
// the default locale will be used since one isn't defined here
paths: ['first', 'second'].map(slug => ({
params: { slug },
})),
paths,
fallback: true,
};
};

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

@@ -26,7 +26,7 @@ export default function Page(props) {
export const getStaticProps = ({ params, locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
unstable_notFound: true,
notFound: true,
};
}

View File

@@ -24,7 +24,7 @@ export default function Page(props) {
export const getStaticProps = ({ locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
unstable_notFound: true,
notFound: true,
};
}

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',
},
};
};

View File

@@ -19,5 +19,5 @@ export function getStaticProps({ params }) {
}
export function getStaticPaths() {
return { paths: [], fallback: 'unstable_blocking' };
return { paths: [], fallback: 'blocking' };
}

View File

@@ -19,5 +19,5 @@ export function getStaticProps({ params }) {
}
export function getStaticPaths() {
return { paths: [], fallback: 'unstable_blocking' };
return { paths: [], fallback: 'blocking' };
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/redwood",
"version": "0.1.1",
"version": "0.1.2-canary.0",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs",
@@ -19,7 +19,7 @@
},
"dependencies": {
"@netlify/zip-it-and-ship-it": "1.2.0",
"@vercel/frameworks": "0.1.1"
"@vercel/frameworks": "0.1.2-canary.0"
},
"devDependencies": {
"@types/aws-lambda": "8.10.19",