Compare commits
44 Commits
@vercel/no
...
@vercel/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25bea3f83e | ||
|
|
b3e1828ebe | ||
|
|
e0e2a8e87e | ||
|
|
37ec89796d | ||
|
|
d3e2a4c4db | ||
|
|
ece3645914 | ||
|
|
fee386493b | ||
|
|
d95ed184ab | ||
|
|
aef8e6388e | ||
|
|
ba43e88603 | ||
|
|
e61f9740c4 | ||
|
|
c4b010fe8b | ||
|
|
db18eb091f | ||
|
|
360e62d172 | ||
|
|
8c3cd0332d | ||
|
|
f5f276021e | ||
|
|
9fbec823f3 | ||
|
|
18c3dd3a63 | ||
|
|
5a4a20b33f | ||
|
|
4489ed0c85 | ||
|
|
359f23daf1 | ||
|
|
4ef92e85db | ||
|
|
659c4d6ccd | ||
|
|
e93d477df8 | ||
|
|
f64625655b | ||
|
|
25a8189997 | ||
|
|
25c3e627cf | ||
|
|
1d6d8b530f | ||
|
|
e821cc0ae7 | ||
|
|
8ecbdc5d03 | ||
|
|
895224985b | ||
|
|
0f42a63c03 | ||
|
|
81e4c9e6fe | ||
|
|
a0a29dc836 | ||
|
|
c1f9d51d7a | ||
|
|
422f0558c1 | ||
|
|
f064ae2908 | ||
|
|
58c3e636f0 | ||
|
|
d5081367f3 | ||
|
|
0ee88366ff | ||
|
|
9ae42c9e92 | ||
|
|
62b8df4a8d | ||
|
|
73ec7f3018 | ||
|
|
2d24a75ca6 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -989,7 +989,6 @@ function getRouteResult(
|
||||
rewriteRoutes.push({
|
||||
src: '^/api(/.*)?$',
|
||||
status: 404,
|
||||
continue: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -2393,7 +2393,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
expect(errorRoutes).toStrictEqual([
|
||||
@@ -2495,7 +2494,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2533,7 +2531,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2571,7 +2568,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2604,7 +2600,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2632,7 +2627,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2663,7 +2657,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2690,7 +2683,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2725,7 +2717,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
expect(errorRoutes).toStrictEqual([
|
||||
@@ -2820,7 +2811,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2853,7 +2843,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2887,7 +2876,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2913,7 +2901,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2937,7 +2924,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2962,7 +2948,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -2983,7 +2968,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3018,7 +3002,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -3076,7 +3059,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3109,7 +3091,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3143,7 +3124,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3162,7 +3142,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3186,7 +3165,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3211,7 +3189,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -3232,7 +3209,6 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
status: 404,
|
||||
src: '^/api(/.*)?$',
|
||||
continue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "20.1.3",
|
||||
"version": "21.0.2-canary.1",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -61,9 +61,9 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.6.0",
|
||||
"@vercel/build-utils": "2.6.1-canary.0",
|
||||
"@vercel/go": "1.1.6",
|
||||
"@vercel/node": "1.8.5",
|
||||
"@vercel/node": "1.8.6-canary.0",
|
||||
"@vercel/python": "1.2.3",
|
||||
"@vercel/ruby": "1.2.4",
|
||||
"update-notifier": "4.1.0"
|
||||
|
||||
@@ -3,15 +3,15 @@ import { resolve, join } from 'path';
|
||||
import DevServer from '../../util/dev/server';
|
||||
import parseListen from '../../util/dev/parse-listen';
|
||||
import { Output } from '../../util/output';
|
||||
import { NowContext } from '../../types';
|
||||
import { NowContext, ProjectEnvVariable } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { getLinkedProject } from '../../util/projects/link';
|
||||
import { getFrameworks } from '../../util/get-frameworks';
|
||||
import { isSettingValue } from '../../util/is-setting-value';
|
||||
import { ProjectSettings, ProjectEnvTarget } from '../../types';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
|
||||
import { Env } from '@vercel/build-utils';
|
||||
import setupAndLink from '../../util/link/setup-and-link';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
|
||||
type Options = {
|
||||
'--debug'?: boolean;
|
||||
@@ -70,7 +70,8 @@ export default async function dev(
|
||||
let devCommand: string | undefined;
|
||||
let frameworkSlug: string | undefined;
|
||||
let projectSettings: ProjectSettings | undefined;
|
||||
let environmentVars: Env | undefined;
|
||||
let projectEnvs: ProjectEnvVariable[] = [];
|
||||
let systemEnvValues: string[] = [];
|
||||
if (link.status === 'linked') {
|
||||
const { project, org } = link;
|
||||
client.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
@@ -98,12 +99,12 @@ export default async function dev(
|
||||
cwd = join(cwd, project.rootDirectory);
|
||||
}
|
||||
|
||||
environmentVars = await getDecryptedEnvRecords(
|
||||
output,
|
||||
client,
|
||||
project,
|
||||
ProjectEnvTarget.Development
|
||||
);
|
||||
[{ envs: projectEnvs }, { systemEnvValues }] = await Promise.all([
|
||||
getDecryptedEnvRecords(output, client, project.id),
|
||||
project.autoExposeSystemEnvs
|
||||
? getSystemEnvValues(output, client, project.id)
|
||||
: { systemEnvValues: [] },
|
||||
]);
|
||||
}
|
||||
|
||||
const devServer = new DevServer(cwd, {
|
||||
@@ -112,7 +113,8 @@ export default async function dev(
|
||||
devCommand,
|
||||
frameworkSlug,
|
||||
projectSettings,
|
||||
environmentVars,
|
||||
projectEnvs,
|
||||
systemEnvValues,
|
||||
});
|
||||
|
||||
process.once('SIGINT', () => devServer.stop());
|
||||
|
||||
14
packages/now-cli/src/commands/env/add.ts
vendored
@@ -18,7 +18,7 @@ import withSpinner from '../../util/with-spinner';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isKnownError } from '../../util/env/known-error';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { SYSTEM_ENV_VALUES } from '../../util/env/system-env';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -91,7 +91,10 @@ export default async function add(
|
||||
name: `Secret (can be created using ${getCommandName('secret add')})`,
|
||||
value: ProjectEnvType.Secret,
|
||||
},
|
||||
{ name: 'Provided by System', value: ProjectEnvType.System },
|
||||
{
|
||||
name: 'Reference to System Environment Variable',
|
||||
value: ProjectEnvType.System,
|
||||
},
|
||||
],
|
||||
})) as { inputEnvType: ProjectEnvType };
|
||||
|
||||
@@ -112,7 +115,10 @@ export default async function add(
|
||||
}
|
||||
}
|
||||
|
||||
const { envs } = await getEnvVariables(output, client, project.id);
|
||||
const [{ envs }, { systemEnvValues }] = await Promise.all([
|
||||
getEnvVariables(output, client, project.id),
|
||||
getSystemEnvValues(output, client, project.id),
|
||||
]);
|
||||
const existing = new Set(
|
||||
envs.filter(r => r.key === envName).map(r => r.target)
|
||||
);
|
||||
@@ -182,7 +188,7 @@ export default async function add(
|
||||
name: 'systemEnvValue',
|
||||
type: 'list',
|
||||
message: `What’s the value of ${envName}?`,
|
||||
choices: SYSTEM_ENV_VALUES.map(value => ({ name: value, value })),
|
||||
choices: systemEnvValues.map(value => ({ name: value, value })),
|
||||
});
|
||||
|
||||
envValue = systemEnvValue;
|
||||
|
||||
30
packages/now-cli/src/commands/env/pull.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import { ProjectEnvTarget, Project } from '../../types';
|
||||
import { Project } from '../../types';
|
||||
import { Output } from '../../util/output';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import Client from '../../util/client';
|
||||
@@ -12,7 +12,8 @@ import { promises, openSync, closeSync, readSync } from 'fs';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
const { writeFile } = promises;
|
||||
import { Env } from '@vercel/build-utils';
|
||||
import exposeSystemEnvs from '../../util/dev/expose-system-envs';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
@@ -84,15 +85,22 @@ export default async function pull(
|
||||
);
|
||||
const pullStamp = stamp();
|
||||
|
||||
const records: Env = await withSpinner(
|
||||
'Downloading',
|
||||
async () =>
|
||||
await getDecryptedEnvRecords(
|
||||
output,
|
||||
client,
|
||||
project,
|
||||
ProjectEnvTarget.Development
|
||||
)
|
||||
const [
|
||||
{ envs: projectEnvs },
|
||||
{ systemEnvValues },
|
||||
] = await withSpinner('Downloading', () =>
|
||||
Promise.all([
|
||||
getDecryptedEnvRecords(output, client, project.id),
|
||||
project.autoExposeSystemEnvs
|
||||
? getSystemEnvValues(output, client, project.id)
|
||||
: { systemEnvValues: [] },
|
||||
])
|
||||
);
|
||||
|
||||
const records = exposeSystemEnvs(
|
||||
projectEnvs,
|
||||
systemEnvValues,
|
||||
project.autoExposeSystemEnvs
|
||||
);
|
||||
|
||||
const contents =
|
||||
|
||||
@@ -230,6 +230,7 @@ export interface ProjectSettings {
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
}
|
||||
|
||||
export interface Project extends ProjectSettings {
|
||||
@@ -243,6 +244,7 @@ export interface Project extends ProjectSettings {
|
||||
framework?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
latestDeployments?: Partial<Deployment>[];
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
}
|
||||
|
||||
export interface Org {
|
||||
|
||||
@@ -149,8 +149,8 @@ export async function executeBuild(
|
||||
filesRemoved,
|
||||
// This env distiniction is only necessary to maintain
|
||||
// backwards compatibility with the `@vercel/next` builder.
|
||||
env: envConfigs.runEnv,
|
||||
buildEnv: envConfigs.buildEnv,
|
||||
env: { ...envConfigs.runEnv },
|
||||
buildEnv: { ...envConfigs.buildEnv },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
41
packages/now-cli/src/util/dev/expose-system-envs.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ProjectEnvType, ProjectEnvVariable } from '../../types';
|
||||
import { Env } from '@vercel/build-utils';
|
||||
|
||||
function getSystemEnvValue(
|
||||
systemEnvRef: string,
|
||||
{ vercelUrl }: { vercelUrl?: string }
|
||||
) {
|
||||
if (systemEnvRef === 'VERCEL_URL') {
|
||||
return vercelUrl || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function exposeSystemEnvs(
|
||||
projectEnvs: ProjectEnvVariable[],
|
||||
systemEnvValues: string[],
|
||||
autoExposeSystemEnvs: boolean | undefined,
|
||||
vercelUrl?: string
|
||||
) {
|
||||
const envs: Env = {};
|
||||
|
||||
if (autoExposeSystemEnvs) {
|
||||
envs['VERCEL'] = '1';
|
||||
envs['VERCEL_ENV'] = 'development';
|
||||
|
||||
for (const key of systemEnvValues) {
|
||||
envs[key] = getSystemEnvValue(key, { vercelUrl });
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of projectEnvs) {
|
||||
if (env.type === ProjectEnvType.System) {
|
||||
envs[env.key] = getSystemEnvValue(env.value, { vercelUrl });
|
||||
} else {
|
||||
envs[env.key] = env.value;
|
||||
}
|
||||
}
|
||||
|
||||
return envs;
|
||||
}
|
||||
@@ -85,7 +85,8 @@ import {
|
||||
HttpHeadersConfig,
|
||||
EnvConfigs,
|
||||
} from './types';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import exposeSystemEnvs from './expose-system-envs';
|
||||
|
||||
const frameworkList = _frameworks as Framework[];
|
||||
const frontendRuntimeSet = new Set(
|
||||
@@ -149,14 +150,16 @@ export default class DevServer {
|
||||
private updateBuildersTimeout: NodeJS.Timeout | undefined;
|
||||
private startPromise: Promise<void> | null;
|
||||
|
||||
private environmentVars: Env | undefined;
|
||||
private systemEnvValues: string[];
|
||||
private projectEnvs: ProjectEnvVariable[];
|
||||
|
||||
constructor(cwd: string, options: DevServerOptions) {
|
||||
this.cwd = cwd;
|
||||
this.debug = options.debug;
|
||||
this.output = options.output;
|
||||
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
|
||||
this.environmentVars = options.environmentVars;
|
||||
this.systemEnvValues = options.systemEnvValues || [];
|
||||
this.projectEnvs = options.projectEnvs || [];
|
||||
this.files = {};
|
||||
this.address = '';
|
||||
this.devCommand = options.devCommand;
|
||||
@@ -491,7 +494,7 @@ export default class DevServer {
|
||||
const dotenv = await fs.readFile(filePath, 'utf8');
|
||||
this.output.debug(`Using local env: ${filePath}`);
|
||||
env = parseDotenv(dotenv);
|
||||
env = this.populateVercelEnvVars(env);
|
||||
env = this.injectSystemValuesInDotenv(env);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
@@ -642,10 +645,30 @@ export default class DevServer {
|
||||
|
||||
let allEnv = { ...buildEnv, ...runEnv };
|
||||
|
||||
// If no .env/.build.env is present, fetch and use cloud environment variables
|
||||
// If no .env/.build.env is present, use cloud environment variables
|
||||
if (Object.keys(allEnv).length === 0) {
|
||||
const cloudEnv = this.populateVercelEnvVars(this.environmentVars);
|
||||
allEnv = runEnv = buildEnv = cloudEnv;
|
||||
const cloudEnv = exposeSystemEnvs(
|
||||
this.projectEnvs || [],
|
||||
this.systemEnvValues || [],
|
||||
this.projectSettings && this.projectSettings.autoExposeSystemEnvs,
|
||||
new URL(this.address).host
|
||||
);
|
||||
|
||||
allEnv = { ...cloudEnv };
|
||||
runEnv = { ...cloudEnv };
|
||||
buildEnv = { ...cloudEnv };
|
||||
}
|
||||
|
||||
// legacy NOW_REGION env variable
|
||||
runEnv['NOW_REGION'] = 'dev1';
|
||||
buildEnv['NOW_REGION'] = 'dev1';
|
||||
allEnv['NOW_REGION'] = 'dev1';
|
||||
|
||||
// mirror how VERCEL_REGION is injected in prod/preview
|
||||
// only inject in `runEnvs`, because `allEnvs` is exposed to dev command
|
||||
// and should not contain VERCEL_REGION
|
||||
if (this.projectSettings && this.projectSettings.autoExposeSystemEnvs) {
|
||||
runEnv['VERCEL_REGION'] = 'dev1';
|
||||
}
|
||||
|
||||
this.envConfigs = { buildEnv, runEnv, allEnv };
|
||||
@@ -753,23 +776,15 @@ export default class DevServer {
|
||||
return merged;
|
||||
}
|
||||
|
||||
populateVercelEnvVars(env: Env | undefined): Env {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
injectSystemValuesInDotenv(env: Env): Env {
|
||||
for (const name of Object.keys(env)) {
|
||||
if (name === 'VERCEL_URL') {
|
||||
const host = new URL(this.address).host;
|
||||
env['VERCEL_URL'] = host;
|
||||
env['VERCEL_URL'] = new URL(this.address).host;
|
||||
} else if (name === 'VERCEL_REGION') {
|
||||
env['VERCEL_REGION'] = 'dev1';
|
||||
}
|
||||
}
|
||||
|
||||
// Always set NOW_REGION to match production
|
||||
env['NOW_REGION'] = 'dev1';
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
@@ -1658,8 +1673,8 @@ export default class DevServer {
|
||||
isDev: true,
|
||||
requestPath,
|
||||
devCacheDir,
|
||||
env: envConfigs.runEnv,
|
||||
buildEnv: envConfigs.buildEnv,
|
||||
env: { ...envConfigs.runEnv },
|
||||
buildEnv: { ...envConfigs.buildEnv },
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { NowConfig } from '@vercel/client';
|
||||
import { HandleValue, Route } from '@vercel/routing-utils';
|
||||
import { Output } from '../output';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
|
||||
export { NowConfig };
|
||||
|
||||
@@ -27,7 +27,8 @@ export interface DevServerOptions {
|
||||
devCommand?: string;
|
||||
frameworkSlug?: string;
|
||||
projectSettings?: ProjectSettings;
|
||||
environmentVars?: Env;
|
||||
systemEnvValues?: string[];
|
||||
projectEnvs?: ProjectEnvVariable[];
|
||||
}
|
||||
|
||||
export interface EnvConfigs {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
import { Secret } from '../../types';
|
||||
|
||||
export default async function getDecryptedSecret(
|
||||
output: Output,
|
||||
client: Client,
|
||||
secretId: string
|
||||
): Promise<string> {
|
||||
if (!secretId) {
|
||||
return '';
|
||||
}
|
||||
output.debug(`Fetching decrypted secret ${secretId}`);
|
||||
const url = `/v2/now/secrets/${secretId}?decrypt=true`;
|
||||
const secret = await client.fetch<Secret>(url);
|
||||
return secret.value;
|
||||
}
|
||||
12
packages/now-cli/src/util/env/get-system-env-values.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
|
||||
export default async function getSystemEnvValues(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string
|
||||
) {
|
||||
output.debug(`Fetching System Environment Values of project ${projectId}`);
|
||||
const url = `/v6/projects/${projectId}/system-env-values`;
|
||||
return client.fetch<{ systemEnvValues: string[] }>(url);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
import { ProjectEnvTarget, Secret, ProjectEnvVariableV5 } from '../../types';
|
||||
import { ProjectEnvTarget, ProjectEnvVariableV5 } from '../../types';
|
||||
|
||||
export default async function removeEnvRecord(
|
||||
output: Output,
|
||||
@@ -18,32 +18,7 @@ export default async function removeEnvRecord(
|
||||
envName
|
||||
)}${qs}`;
|
||||
|
||||
const env = await client.fetch<ProjectEnvVariableV5>(urlProject, {
|
||||
await client.fetch<ProjectEnvVariableV5>(urlProject, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (env && env.value) {
|
||||
const idOrName = env.value.startsWith('@') ? env.value.slice(1) : env.value;
|
||||
const urlSecret = `/v2/now/secrets/${idOrName}`;
|
||||
let secret: Secret | undefined;
|
||||
|
||||
try {
|
||||
secret = await client.fetch<Secret>(urlSecret);
|
||||
} catch (error) {
|
||||
if (error && error.status === 404) {
|
||||
// User likely deleted the secret before the env var, so we can still report success
|
||||
output.debug(
|
||||
`Skipped ${env.key} because secret ${idOrName} was already deleted`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Since integrations add global secrets, we must only delete if the secret was
|
||||
// specifically added to this project
|
||||
if (secret && secret.projectId === projectId) {
|
||||
await client.fetch<Secret>(urlSecret, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
packages/now-cli/src/util/env/system-env.ts
vendored
@@ -1,32 +0,0 @@
|
||||
export const SYSTEM_ENV_VALUES = [
|
||||
'VERCEL_URL',
|
||||
'VERCEL_GITHUB_COMMIT_ORG',
|
||||
'VERCEL_GITHUB_COMMIT_REF',
|
||||
'VERCEL_GITHUB_ORG',
|
||||
'VERCEL_GITHUB_DEPLOYMENT',
|
||||
'VERCEL_GITHUB_COMMIT_REPO',
|
||||
'VERCEL_GITHUB_REPO',
|
||||
'VERCEL_GITHUB_COMMIT_AUTHOR_LOGIN',
|
||||
'VERCEL_GITHUB_COMMIT_AUTHOR_NAME',
|
||||
'VERCEL_GITHUB_COMMIT_SHA',
|
||||
'VERCEL_GITLAB_DEPLOYMENT',
|
||||
'VERCEL_GITLAB_PROJECT_NAMESPACE',
|
||||
'VERCEL_GITLAB_PROJECT_NAME',
|
||||
'VERCEL_GITLAB_PROJECT_ID',
|
||||
'VERCEL_GITLAB_PROJECT_PATH',
|
||||
'VERCEL_GITLAB_COMMIT_REF',
|
||||
'VERCEL_GITLAB_COMMIT_SHA',
|
||||
'VERCEL_GITLAB_COMMIT_MESSAGE',
|
||||
'VERCEL_GITLAB_COMMIT_AUTHOR_LOGIN',
|
||||
'VERCEL_GITLAB_COMMIT_AUTHOR_NAME',
|
||||
'VERCEL_BITBUCKET_DEPLOYMENT',
|
||||
'VERCEL_BITBUCKET_REPO_OWNER',
|
||||
'VERCEL_BITBUCKET_REPO_SLUG',
|
||||
'VERCEL_BITBUCKET_REPO_NAME',
|
||||
'VERCEL_BITBUCKET_COMMIT_REF',
|
||||
'VERCEL_BITBUCKET_COMMIT_SHA',
|
||||
'VERCEL_BITBUCKET_COMMIT_MESSAGE',
|
||||
'VERCEL_BITBUCKET_COMMIT_AUTHOR_NAME',
|
||||
'VERCEL_BITBUCKET_COMMIT_AUTHOR_URL',
|
||||
'VERCEL_BITBUCKET_COMMIT_AUTHOR_AVATAR',
|
||||
];
|
||||
@@ -1,52 +1,65 @@
|
||||
import getEnvVariables from './env/get-env-records';
|
||||
import getDecryptedSecret from './env/get-decrypted-secret';
|
||||
import Client from './client';
|
||||
import { Output } from './output/create-output';
|
||||
import { ProjectEnvTarget, Project, ProjectEnvType } from '../types';
|
||||
|
||||
import { Env } from '@vercel/build-utils';
|
||||
import {
|
||||
ProjectEnvTarget,
|
||||
ProjectEnvType,
|
||||
ProjectEnvVariable,
|
||||
Secret,
|
||||
} from '../types';
|
||||
import getEnvRecords from './env/get-env-records';
|
||||
|
||||
export default async function getDecryptedEnvRecords(
|
||||
output: Output,
|
||||
client: Client,
|
||||
project: Project,
|
||||
target: ProjectEnvTarget
|
||||
): Promise<Env> {
|
||||
const { envs } = await getEnvVariables(output, client, project.id, target);
|
||||
const decryptedValues = await Promise.all(
|
||||
envs.map(async env => {
|
||||
if (env.type === ProjectEnvType.System) {
|
||||
return { value: '', found: true };
|
||||
} else if (env.type === ProjectEnvType.Plaintext) {
|
||||
return { value: env.value, found: true };
|
||||
projectId: string
|
||||
): Promise<{ envs: ProjectEnvVariable[] }> {
|
||||
const { envs } = await getEnvRecords(
|
||||
output,
|
||||
client,
|
||||
projectId,
|
||||
ProjectEnvTarget.Development
|
||||
);
|
||||
|
||||
const envsWithDecryptedSecrets = await Promise.all(
|
||||
envs.map(async ({ type, key, value }) => {
|
||||
// it's not possible to create secret env variables for development
|
||||
// anymore but we keep this because legacy env variables with "decryptable"
|
||||
// secret values still exit in our system
|
||||
if (type === ProjectEnvType.Secret) {
|
||||
try {
|
||||
const secretIdOrName = value;
|
||||
|
||||
if (!secretIdOrName) {
|
||||
return { type, key, value: '', found: true };
|
||||
}
|
||||
|
||||
output.debug(`Fetching decrypted secret ${secretIdOrName}`);
|
||||
const secret = await client.fetch<Secret>(
|
||||
`/v2/now/secrets/${secretIdOrName}?decrypt=true`
|
||||
);
|
||||
|
||||
return { type, key, value: secret.value, found: true };
|
||||
} catch (error) {
|
||||
if (error && error.status === 404) {
|
||||
return { type, key, value: '', found: false };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await getDecryptedSecret(output, client, env.value);
|
||||
return { value, found: true };
|
||||
} catch (error) {
|
||||
if (error && error.status === 404) {
|
||||
return { value: '', found: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return { type, key, value, found: true };
|
||||
})
|
||||
);
|
||||
|
||||
const results: Env = {};
|
||||
for (let i = 0; i < decryptedValues.length; i++) {
|
||||
const { key } = envs[i];
|
||||
const { value, found } = decryptedValues[i];
|
||||
|
||||
if (!found) {
|
||||
for (let env of envsWithDecryptedSecrets) {
|
||||
if (!env.found) {
|
||||
output.print('');
|
||||
output.warn(
|
||||
`Unable to download variable ${key} because associated secret was deleted`
|
||||
`Unable to download variable ${env.key} because associated secret was deleted`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
results[key] = value ? value : '';
|
||||
}
|
||||
return results;
|
||||
|
||||
return { envs: envsWithDecryptedSecrets };
|
||||
}
|
||||
|
||||
4
packages/now-cli/test/dev/fixtures/30-next-image-optimization/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.next
|
||||
!public
|
||||
yarn.lock
|
||||
.vercel
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<h1>Home Page</h1>
|
||||
<hr />
|
||||
<h2>Optimized</h2>
|
||||
<Image src="/test.png" width="400" height="400" />
|
||||
<hr />
|
||||
<h2>Original</h2>
|
||||
<img src="/test.png" width="400" height="400" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="400.000000pt" height="400.000000pt" viewBox="0 0 400.000000 400.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<g transform="translate(0.000000,400.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M0 2000 l0 -2000 2000 0 2000 0 0 2000 0 2000 -2000 0 -2000 0 0
|
||||
-2000z m2401 118 l396 -693 -398 -3 c-220 -1 -578 -1 -798 0 l-398 3 396 693
|
||||
c217 380 398 692 401 692 3 0 184 -312 401 -692z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 635 B |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"build": {
|
||||
"env": {
|
||||
"FORCE_BUILDER_TAG": "canary"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,14 +128,13 @@ async function testPath(
|
||||
status,
|
||||
path,
|
||||
expectedText,
|
||||
headers = {},
|
||||
method = 'GET',
|
||||
body = undefined
|
||||
expectedHeaders = {},
|
||||
fetchOpts = {}
|
||||
) {
|
||||
const opts = { redirect: 'manual-dont-change', method, body };
|
||||
const opts = { ...fetchOpts, redirect: 'manual-dont-change' };
|
||||
const url = `${origin}${path}`;
|
||||
const res = await fetch(url, opts);
|
||||
const msg = `Testing response from ${method} ${url}`;
|
||||
const msg = `Testing response from ${fetchOpts.method || 'GET'} ${url}`;
|
||||
console.log(msg);
|
||||
t.is(res.status, status, msg);
|
||||
validateResponseHeaders(t, res);
|
||||
@@ -150,8 +149,8 @@ async function testPath(
|
||||
expectedText.lastIndex = 0; // reset since we test twice
|
||||
t.regex(actualText, expectedText);
|
||||
}
|
||||
if (headers) {
|
||||
Object.entries(headers).forEach(([key, expectedValue]) => {
|
||||
if (expectedHeaders) {
|
||||
Object.entries(expectedHeaders).forEach(([key, expectedValue]) => {
|
||||
let actualValue = res.headers.get(key);
|
||||
if (key.toLowerCase() === 'location' && actualValue === '//') {
|
||||
// HACK: `node-fetch` has strange behavior for location header so fix it
|
||||
@@ -387,9 +386,12 @@ test(
|
||||
async testPath => {
|
||||
await testPath(200, '/', /<div id="redwood-app">/m);
|
||||
await testPath(200, '/about', /<div id="redwood-app">/m);
|
||||
const reqBody = '{"query":"{redwood{version}}"}';
|
||||
const fetchOpts = {
|
||||
method: 'POST',
|
||||
body: '{"query":"{redwood{version}}"}',
|
||||
};
|
||||
const resBody = '{"data":{"redwood":{"version":"0.15.0"}}}';
|
||||
await testPath(200, '/api/graphql', resBody, {}, 'POST', reqBody);
|
||||
await testPath(200, '/api/graphql', resBody, {}, fetchOpts);
|
||||
},
|
||||
{ isExample: true }
|
||||
)
|
||||
@@ -945,12 +947,16 @@ test(
|
||||
'Access-Control-Allow-Methods':
|
||||
'GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE',
|
||||
};
|
||||
await testPath(200, '/', 'status api', headers, 'GET');
|
||||
await testPath(200, '/', 'status api', headers, 'POST');
|
||||
await testPath(200, '/api/status.js', 'status api', headers, 'GET');
|
||||
await testPath(200, '/api/status.js', 'status api', headers, 'POST');
|
||||
await testPath(204, '/', '', headers, 'OPTIONS');
|
||||
await testPath(204, '/api/status.js', '', headers, 'OPTIONS');
|
||||
await testPath(200, '/', 'status api', headers, { method: 'GET' });
|
||||
await testPath(200, '/', 'status api', headers, { method: 'POST' });
|
||||
await testPath(200, '/api/status.js', 'status api', headers, {
|
||||
method: 'GET',
|
||||
});
|
||||
await testPath(200, '/api/status.js', 'status api', headers, {
|
||||
method: 'POST',
|
||||
});
|
||||
await testPath(204, '/', '', headers, { method: 'OPTIONS' });
|
||||
await testPath(204, '/api/status.js', '', headers, { method: 'OPTIONS' });
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1595,6 +1601,61 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] 30-next-image-optimization',
|
||||
testFixtureStdio('30-next-image-optimization', async testPath => {
|
||||
const toUrl = (url, w, q) => {
|
||||
const query = new URLSearchParams();
|
||||
query.append('url', url);
|
||||
query.append('w', w);
|
||||
query.append('q', q);
|
||||
return `/_next/image?${query}`;
|
||||
};
|
||||
|
||||
const expectHeader = accept => ({
|
||||
'content-type': accept,
|
||||
'cache-control': 'public, max-age=0, must-revalidate',
|
||||
});
|
||||
const fetchOpts = accept => ({ method: 'GET', headers: { accept } });
|
||||
await testPath(200, '/', /Home Page/m);
|
||||
await testPath(
|
||||
200,
|
||||
toUrl('/test.jpg', 64, 100),
|
||||
null,
|
||||
expectHeader('image/webp'),
|
||||
fetchOpts('image/webp')
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
toUrl('/test.png', 64, 90),
|
||||
null,
|
||||
expectHeader('image/webp'),
|
||||
fetchOpts('image/webp')
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
toUrl('/test.gif', 64, 80),
|
||||
null,
|
||||
expectHeader('image/webp'),
|
||||
fetchOpts('image/webp')
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
toUrl('/test.svg', 64, 70),
|
||||
null,
|
||||
expectHeader('image/svg+xml'),
|
||||
fetchOpts('image/webp')
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
toUrl('/animated.gif', 64, 60),
|
||||
null,
|
||||
expectHeader('image/gif'),
|
||||
fetchOpts('image/gif')
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] Use `@vercel/python` with Flask requirements.txt',
|
||||
testFixtureStdio('python-flask', async testPath => {
|
||||
|
||||
177
packages/now-cli/test/integration.js
vendored
@@ -539,7 +539,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
async function nowEnvAddSystemEnv() {
|
||||
const now = execa(
|
||||
binaryPath,
|
||||
['env', 'add', 'system', 'VERCEL_URL', ...defaultArgs],
|
||||
['env', 'add', 'system', 'NEXT_PUBLIC_VERCEL_URL', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
cwd: target,
|
||||
@@ -602,6 +602,41 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
t.regex(systemEnvs[0], /Production, Preview, Development/gm);
|
||||
}
|
||||
|
||||
// we create a "legacy" env variable that contains a decryptable secret
|
||||
// to check that vc env pull and vc dev work correctly with decryptable secrets
|
||||
async function createEnvWithDecryptableSecret() {
|
||||
console.log('creating an env variable with a decryptable secret');
|
||||
|
||||
const name = `my-secret${Math.floor(Math.random() * 10000)}`;
|
||||
|
||||
const res = await apiFetch('/v2/now/secrets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
value: 'decryptable value',
|
||||
decryptable: true,
|
||||
}),
|
||||
});
|
||||
|
||||
t.is(res.status, 200);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
const link = require(path.join(target, '.vercel/project.json'));
|
||||
|
||||
const resEnv = await apiFetch(`/v4/projects/${link.projectId}/env`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
key: 'MY_DECRYPTABLE_SECRET_ENV',
|
||||
value: json.uid,
|
||||
target: ['development'],
|
||||
type: 'secret',
|
||||
}),
|
||||
});
|
||||
|
||||
t.is(resEnv.status, 200);
|
||||
}
|
||||
|
||||
async function nowEnvPull() {
|
||||
const { exitCode, stderr, stdout } = await execa(
|
||||
binaryPath,
|
||||
@@ -621,7 +656,8 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
const lines = new Set(contents.split('\n'));
|
||||
t.true(lines.has('MY_PLAINTEXT_ENV_VAR="my plaintext value"'));
|
||||
t.true(lines.has('MY_STDIN_VAR="{"expect":"quotes"}"'));
|
||||
t.true(lines.has('VERCEL_URL=""'));
|
||||
t.true(lines.has('NEXT_PUBLIC_VERCEL_URL=""'));
|
||||
t.true(lines.has('MY_DECRYPTABLE_SECRET_ENV="decryptable value"'));
|
||||
}
|
||||
|
||||
async function nowEnvPullOverwrite() {
|
||||
@@ -675,7 +711,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
const apiJson = await apiRes.json();
|
||||
t.is(apiJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
|
||||
t.is(apiJson['MY_SECRET_ENV_VAR'], 'my secret');
|
||||
t.is(apiJson['VERCEL_URL'], host);
|
||||
t.is(apiJson['NEXT_PUBLIC_VERCEL_URL'], host);
|
||||
|
||||
const homeUrl = `https://${host}`;
|
||||
console.log({ homeUrl });
|
||||
@@ -684,7 +720,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
|
||||
t.is(homeJson['MY_SECRET_ENV_VAR'], 'my secret');
|
||||
t.is(homeJson['VERCEL_URL'], host);
|
||||
t.is(homeJson['NEXT_PUBLIC_VERCEL_URL'], host);
|
||||
}
|
||||
|
||||
async function nowDevWithEnv() {
|
||||
@@ -702,8 +738,6 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const localhostNoProtocol = localhost[0].slice('http://'.length);
|
||||
|
||||
const apiUrl = `${localhost[0]}/api/get-env`;
|
||||
const apiRes = await fetch(apiUrl);
|
||||
|
||||
@@ -712,14 +746,16 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
const apiJson = await apiRes.json();
|
||||
|
||||
t.is(apiJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
|
||||
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(apiJson['NEXT_PUBLIC_VERCEL_URL'], '');
|
||||
t.is(apiJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
|
||||
|
||||
const homeUrl = localhost[0];
|
||||
|
||||
const homeRes = await fetch(homeUrl);
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
|
||||
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(homeJson['NEXT_PUBLIC_VERCEL_URL'], '');
|
||||
t.is(homeJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
|
||||
|
||||
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
|
||||
|
||||
@@ -751,16 +787,105 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
|
||||
const apiJson = await apiRes.json();
|
||||
|
||||
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(apiJson['NEXT_PUBLIC_VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(apiJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
|
||||
t.is(apiJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
|
||||
t.is(apiJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
|
||||
|
||||
const homeUrl = localhost[0];
|
||||
const homeRes = await fetch(homeUrl);
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
|
||||
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(homeJson['NEXT_PUBLIC_VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(homeJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
|
||||
t.is(homeJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
|
||||
|
||||
// system env vars are not automatically exposed
|
||||
t.is(apiJson['VERCEL'], undefined);
|
||||
t.is(homeJson['VERCEL'], undefined);
|
||||
|
||||
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
|
||||
|
||||
const { exitCode, stderr, stdout } = await vc;
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
}
|
||||
|
||||
async function enableAutoExposeSystemEnvs() {
|
||||
const link = require(path.join(target, '.vercel/project.json'));
|
||||
|
||||
const res = await apiFetch(`/v2/projects/${link.projectId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ autoExposeSystemEnvs: true }),
|
||||
});
|
||||
|
||||
t.is(res.status, 200);
|
||||
if (res.status === 200) {
|
||||
console.log(
|
||||
`Set autoExposeSystemEnvs=true for project ${link.projectId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function nowEnvPullFetchSystemVars() {
|
||||
const { exitCode, stderr, stdout } = await execa(
|
||||
binaryPath,
|
||||
['env', 'pull', '-y', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
cwd: target,
|
||||
}
|
||||
);
|
||||
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
|
||||
const contents = fs.readFileSync(path.join(target, '.env'), 'utf8');
|
||||
|
||||
const lines = new Set(contents.split('\n'));
|
||||
t.true(lines.has('VERCEL="1"'));
|
||||
t.true(lines.has('VERCEL_URL=""'));
|
||||
t.true(lines.has('NEXT_PUBLIC_VERCEL_URL=""'));
|
||||
t.true(lines.has('VERCEL_ENV="development"'));
|
||||
t.true(lines.has('VERCEL_GIT_PROVIDER=""'));
|
||||
t.true(lines.has('VERCEL_GIT_REPO_SLUG=""'));
|
||||
}
|
||||
|
||||
async function nowDevAndFetchSystemVars() {
|
||||
const vc = execa(binaryPath, ['dev', ...defaultArgs], {
|
||||
reject: false,
|
||||
cwd: target,
|
||||
});
|
||||
|
||||
let localhost = undefined;
|
||||
await waitForPrompt(vc, chunk => {
|
||||
if (chunk.includes('Ready! Available at')) {
|
||||
localhost = /(https?:[^\s]+)/g.exec(chunk);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const apiUrl = `${localhost[0]}/api/get-env`;
|
||||
const apiRes = await fetch(apiUrl);
|
||||
|
||||
const localhostNoProtocol = localhost[0].slice('http://'.length);
|
||||
|
||||
const apiJson = await apiRes.json();
|
||||
t.is(apiJson['VERCEL'], '1');
|
||||
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(apiJson['VERCEL_ENV'], 'development');
|
||||
t.is(apiJson['VERCEL_REGION'], 'dev1');
|
||||
t.is(apiJson['VERCEL_GIT_PROVIDER'], '');
|
||||
t.is(apiJson['VERCEL_GIT_REPO_SLUG'], '');
|
||||
|
||||
const homeUrl = localhost[0];
|
||||
const homeRes = await fetch(homeUrl);
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['VERCEL'], '1');
|
||||
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(homeJson['VERCEL_ENV'], 'development');
|
||||
t.is(homeJson['VERCEL_REGION'], undefined);
|
||||
t.is(homeJson['VERCEL_GIT_PROVIDER'], '');
|
||||
t.is(homeJson['VERCEL_GIT_REPO_SLUG'], '');
|
||||
|
||||
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
|
||||
|
||||
@@ -831,12 +956,34 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
);
|
||||
|
||||
t.is(exitCode2, 0, formatOutput({ stderr2, stdout2 }));
|
||||
|
||||
const {
|
||||
exitCode: exitCode3,
|
||||
stderr: stderr3,
|
||||
stdout: stdout3,
|
||||
} = await execa(
|
||||
binaryPath,
|
||||
[
|
||||
'env',
|
||||
'rm',
|
||||
'MY_DECRYPTABLE_SECRET_ENV',
|
||||
'development',
|
||||
'-y',
|
||||
...defaultArgs,
|
||||
],
|
||||
{
|
||||
reject: false,
|
||||
cwd: target,
|
||||
}
|
||||
);
|
||||
|
||||
t.is(exitCode3, 0, formatOutput({ stderr3, stdout3 }));
|
||||
}
|
||||
|
||||
async function nowEnvRemoveWithNameOnly() {
|
||||
const vc = execa(
|
||||
binaryPath,
|
||||
['env', 'rm', 'VERCEL_URL', '-y', ...defaultArgs],
|
||||
['env', 'rm', 'NEXT_PUBLIC_VERCEL_URL', '-y', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
cwd: target,
|
||||
@@ -846,7 +993,8 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
await waitForPrompt(
|
||||
vc,
|
||||
chunk =>
|
||||
chunk.includes('which Environments') && chunk.includes('VERCEL_URL')
|
||||
chunk.includes('which Environments') &&
|
||||
chunk.includes('NEXT_PUBLIC_VERCEL_URL')
|
||||
);
|
||||
vc.stdin.write('a\n'); // select all
|
||||
|
||||
@@ -862,6 +1010,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
await nowEnvAddFromStdin();
|
||||
await nowEnvAddSystemEnv();
|
||||
await nowEnvLsIncludesVar();
|
||||
await createEnvWithDecryptableSecret();
|
||||
await nowEnvPull();
|
||||
await nowEnvPullOverwrite();
|
||||
await nowEnvPullConfirm();
|
||||
@@ -869,6 +1018,10 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
await nowDevWithEnv();
|
||||
fs.unlinkSync(path.join(target, '.env'));
|
||||
await nowDevAndFetchCloudVars();
|
||||
await enableAutoExposeSystemEnvs();
|
||||
await nowEnvPullFetchSystemVars();
|
||||
fs.unlinkSync(path.join(target, '.env'));
|
||||
await nowDevAndFetchSystemVars();
|
||||
await nowEnvRemove();
|
||||
await nowEnvRemoveWithArgs();
|
||||
await nowEnvRemoveWithNameOnly();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "9.0.4",
|
||||
"version": "9.0.5-canary.0",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
@@ -37,7 +37,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.6.0",
|
||||
"@vercel/build-utils": "2.6.1-canary.0",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/next",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.5-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/resolve-from": "5.0.1",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "2.4.1",
|
||||
"@vercel/nft": "0.9.2",
|
||||
"@vercel/nft": "0.9.4",
|
||||
"async-sema": "3.0.1",
|
||||
"buffer-crc32": "0.2.13",
|
||||
"escape-string-regexp": "2.0.0",
|
||||
|
||||
@@ -448,7 +448,7 @@ export async function build({
|
||||
const prerenderManifest = await getPrerenderManifest(entryPath);
|
||||
const headers: Route[] = [];
|
||||
const rewrites: Route[] = [];
|
||||
const redirects: Route[] = [];
|
||||
let redirects: Route[] = [];
|
||||
const dataRoutes: Route[] = [];
|
||||
let dynamicRoutes: Route[] = [];
|
||||
// whether they have enabled pages/404.js as the custom 404 page
|
||||
@@ -718,6 +718,11 @@ export async function build({
|
||||
// with that routing section
|
||||
...rewrites,
|
||||
|
||||
// make sure 404 page is used when a directory is matched without
|
||||
// an index page
|
||||
{ handle: 'resource' },
|
||||
{ src: path.join('/', entryDirectory, '.*'), status: 404 },
|
||||
|
||||
// We need to make sure to 404 for /_next after handle: miss since
|
||||
// handle: miss is called before rewrites and to prevent rewriting
|
||||
// /_next
|
||||
@@ -1769,12 +1774,7 @@ export async function build({
|
||||
if (nonDynamicSsg || isFallback) {
|
||||
outputPathData = outputPathData.replace(
|
||||
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
|
||||
`${routeFileNoExt}${
|
||||
routeFileNoExt !== origRouteFileNoExt &&
|
||||
origRouteFileNoExt === '/index'
|
||||
? '/index'
|
||||
: ''
|
||||
}.json`
|
||||
`${routeFileNoExt}.json`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2020,6 +2020,29 @@ export async function build({
|
||||
|
||||
const { i18n } = routesManifest || {};
|
||||
|
||||
const trailingSlashRedirects: Route[] = [];
|
||||
|
||||
redirects = redirects.filter(_redir => {
|
||||
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 here to prevent the redirect from
|
||||
// moving underneath i18n routes
|
||||
redir.continue = true;
|
||||
trailingSlashRedirects.push(redir);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
output: {
|
||||
...publicDirectoryFiles,
|
||||
@@ -2059,36 +2082,15 @@ export async function build({
|
||||
- Builder rewrites
|
||||
*/
|
||||
routes: [
|
||||
// headers
|
||||
...headers,
|
||||
|
||||
// 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;
|
||||
}),
|
||||
// force trailingSlashRedirect to the very top so it doesn't
|
||||
// conflict with i18n routes that don't have or don't have the
|
||||
// trailing slash
|
||||
...trailingSlashRedirects,
|
||||
|
||||
...(i18n
|
||||
? [
|
||||
// Handle auto-adding current default locale to path based on $wildcard
|
||||
// Handle auto-adding current default locale to path based on
|
||||
// $wildcard
|
||||
{
|
||||
src: `^${path.join(
|
||||
'/',
|
||||
@@ -2097,8 +2099,8 @@ export async function build({
|
||||
)}(?!(?:_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
|
||||
// we aren't able to ensure trailing slash mode here
|
||||
// so ensure this comes after the trailing slash redirect
|
||||
dest: '$wildcard/$1',
|
||||
continue: true,
|
||||
},
|
||||
@@ -2107,8 +2109,6 @@ export async function build({
|
||||
...(i18n.domains && i18n.localeDetection !== false
|
||||
? [
|
||||
{
|
||||
// TODO: enable redirecting between domains, will require
|
||||
// updating the src with the desired locales to redirect
|
||||
src: `^${path.join(
|
||||
'/',
|
||||
entryDirectory
|
||||
@@ -2144,10 +2144,10 @@ export async function build({
|
||||
...(i18n.localeDetection !== false
|
||||
? [
|
||||
{
|
||||
// TODO: if default locale is included in this src it won't be
|
||||
// visitable by users who prefer another language since a
|
||||
// cookie isn't set signaling the default locale is preferred
|
||||
// on redirect currently, investigate adding this
|
||||
// TODO: if default locale is included in this src it won't
|
||||
// be visitable by users who prefer another language since a
|
||||
// cookie isn't set signaling the default locale is
|
||||
// preferred on redirect currently, investigate adding this
|
||||
src: '/',
|
||||
locale: {
|
||||
redirect: i18n.locales.reduce(
|
||||
@@ -2190,6 +2190,12 @@ export async function build({
|
||||
]
|
||||
: []),
|
||||
|
||||
// headers
|
||||
...headers,
|
||||
|
||||
// redirects
|
||||
...redirects,
|
||||
|
||||
// Make sure to 404 for the /404 path itself
|
||||
...(i18n
|
||||
? [
|
||||
@@ -2239,6 +2245,11 @@ export async function build({
|
||||
// with that routing section
|
||||
...rewrites,
|
||||
|
||||
// make sure 404 page is used when a directory is matched without
|
||||
// an index page
|
||||
{ handle: 'resource' },
|
||||
{ src: path.join('/', entryDirectory, '.*'), status: 404 },
|
||||
|
||||
// We need to make sure to 404 for /_next after handle: miss since
|
||||
// handle: miss is called before rewrites and to prevent rewriting /_next
|
||||
{ handle: 'miss' },
|
||||
@@ -2253,16 +2264,16 @@ export async function build({
|
||||
dest: '$0',
|
||||
},
|
||||
|
||||
// remove default locale prefix to check public files
|
||||
// remove locale prefixes to check public files
|
||||
...(i18n
|
||||
? [
|
||||
{
|
||||
src: `${path.join(
|
||||
src: `^${path.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
i18n.defaultLocale,
|
||||
'/'
|
||||
)}(.*)`,
|
||||
entryDirectory
|
||||
)}/?(?:${i18n.locales
|
||||
.map(locale => escapeStringRegexp(locale))
|
||||
.join('|')})/(.*)`,
|
||||
dest: `${path.join('/', entryDirectory, '/')}$1`,
|
||||
check: true,
|
||||
},
|
||||
|
||||
@@ -345,17 +345,21 @@
|
||||
"mustContain": "slug\":\"first\""
|
||||
},
|
||||
|
||||
// TODO: update when directory listing is disabled
|
||||
// and these are proper 404s
|
||||
{
|
||||
"path": "/en/not-found",
|
||||
"status": 200,
|
||||
"mustContain": "Index of"
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/en/not-found",
|
||||
"mustContain": "lang=\"en\""
|
||||
},
|
||||
{
|
||||
"path": "/nl/not-found",
|
||||
"status": 200,
|
||||
"mustContain": "Index of"
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/nl/not-found",
|
||||
"mustContain": "lang=\"nl\""
|
||||
},
|
||||
{
|
||||
"path": "/en-US/not-found",
|
||||
@@ -427,22 +431,22 @@
|
||||
},
|
||||
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/en-US/index.json",
|
||||
"path": "/_next/data/testing-build-id/en-US.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"en-US\""
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/en/index.json",
|
||||
"path": "/_next/data/testing-build-id/en.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"en\""
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/fr/index.json",
|
||||
"path": "/_next/data/testing-build-id/fr.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"fr\""
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/nl/index.json",
|
||||
"path": "/_next/data/testing-build-id/nl.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"nl\""
|
||||
},
|
||||
|
||||
@@ -4,13 +4,6 @@ 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);
|
||||
|
||||
@@ -20,7 +13,7 @@ module.exports = function (ctx) {
|
||||
expect($('#router-locale').text()).toBe('en-US');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -32,13 +25,6 @@ module.exports = function (ctx) {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -48,7 +34,7 @@ module.exports = function (ctx) {
|
||||
expect($('#router-locale').text()).toBe('fr');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/fr`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -60,13 +46,6 @@ module.exports = function (ctx) {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -76,7 +55,7 @@ module.exports = function (ctx) {
|
||||
expect($('#router-locale').text()).toBe('nl-NL');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -88,15 +67,6 @@ module.exports = function (ctx) {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -107,7 +77,7 @@ module.exports = function (ctx) {
|
||||
expect($('#router-locale').text()).toBe('en-US');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/second`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -119,15 +89,6 @@ module.exports = function (ctx) {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -138,7 +99,7 @@ module.exports = function (ctx) {
|
||||
expect($('#router-locale').text()).toBe('fr');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/fr/second`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -150,15 +111,6 @@ module.exports = function (ctx) {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -169,7 +121,7 @@ module.exports = function (ctx) {
|
||||
expect($('#router-locale').text()).toBe('nl-NL');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL/second`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
@@ -5,12 +5,12 @@ 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`
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US.json`
|
||||
);
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -22,7 +22,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({});
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -36,12 +36,12 @@ module.exports = function (ctx) {
|
||||
|
||||
it('should revalidate content properly from /fr', async () => {
|
||||
const dataRes = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/index.json`
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr.json`
|
||||
);
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/fr`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -53,7 +53,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({});
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/fr`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -67,12 +67,12 @@ module.exports = function (ctx) {
|
||||
|
||||
it('should revalidate content properly from /nl-NL', async () => {
|
||||
const dataRes = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/index.json`
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL.json`
|
||||
);
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/nl-NL`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -84,7 +84,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({});
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -104,7 +104,7 @@ module.exports = function (ctx) {
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/gsp/fallback/first`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -118,7 +118,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/fallback/first`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -139,7 +139,7 @@ module.exports = function (ctx) {
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/first`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -153,7 +153,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/first`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -174,7 +174,7 @@ module.exports = function (ctx) {
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/first`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -188,7 +188,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/first`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -212,7 +212,7 @@ module.exports = function (ctx) {
|
||||
const initRes = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
|
||||
expect(initRes.status).toBe(200);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -226,7 +226,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -246,7 +246,7 @@ module.exports = function (ctx) {
|
||||
);
|
||||
expect(dataRes.status).toBe(200);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/new-page`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -260,7 +260,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/new-page`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -278,7 +278,7 @@ module.exports = function (ctx) {
|
||||
);
|
||||
expect(dataRes.status).toBe(200);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/new-page`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -292,7 +292,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/nl-NL/gsp/fallback/new-page`
|
||||
@@ -314,7 +314,7 @@ module.exports = function (ctx) {
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/gsp/no-fallback/first`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -327,7 +327,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/no-fallback/first`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -347,7 +347,7 @@ module.exports = function (ctx) {
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/no-fallback/first`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -360,7 +360,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/no-fallback/first`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -380,7 +380,7 @@ module.exports = function (ctx) {
|
||||
expect(dataRes.status).toBe(200);
|
||||
await dataRes.json();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/nl-NL/gsp/no-fallback/second`
|
||||
@@ -395,7 +395,7 @@ module.exports = function (ctx) {
|
||||
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'second' });
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/nl-NL/gsp/no-fallback/second`
|
||||
|
||||
@@ -18,4 +18,81 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/en-US/redirect-1',
|
||||
destination: '/somewhere-else',
|
||||
permanent: false,
|
||||
locale: false,
|
||||
},
|
||||
{
|
||||
source: '/nl/redirect-2',
|
||||
destination: '/somewhere-else',
|
||||
permanent: false,
|
||||
locale: false,
|
||||
},
|
||||
{
|
||||
source: '/redirect-3',
|
||||
destination: '/somewhere-else',
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/en-US/rewrite-1',
|
||||
destination: '/another',
|
||||
locale: false,
|
||||
},
|
||||
{
|
||||
source: '/nl/rewrite-2',
|
||||
destination: '/nl/another',
|
||||
locale: false,
|
||||
},
|
||||
{
|
||||
source: '/fr/rewrite-3',
|
||||
destination: '/nl/another',
|
||||
locale: false,
|
||||
},
|
||||
{
|
||||
source: '/rewrite-4',
|
||||
destination: '/another',
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/en-US/add-header-1',
|
||||
locale: false,
|
||||
headers: [
|
||||
{
|
||||
key: 'x-hello',
|
||||
value: 'world',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/nl/add-header-2',
|
||||
locale: false,
|
||||
headers: [
|
||||
{
|
||||
key: 'x-hello',
|
||||
value: 'world',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/add-header-3',
|
||||
headers: [
|
||||
{
|
||||
key: 'x-hello',
|
||||
value: 'world',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -353,17 +353,21 @@
|
||||
"mustContain": "slug\":\"first\""
|
||||
},
|
||||
|
||||
// TODO: update when directory listing is disabled
|
||||
// and these are proper 404s
|
||||
{
|
||||
"path": "/en/not-found",
|
||||
"status": 200,
|
||||
"mustContain": "Index of"
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/en/not-found",
|
||||
"mustContain": "lang=\"en\""
|
||||
},
|
||||
{
|
||||
"path": "/nl/not-found",
|
||||
"status": 200,
|
||||
"mustContain": "Index of"
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/nl/not-found",
|
||||
"mustContain": "lang=\"nl\""
|
||||
},
|
||||
{
|
||||
"path": "/en-US/not-found",
|
||||
@@ -435,22 +439,22 @@
|
||||
},
|
||||
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/en-US/index.json",
|
||||
"path": "/_next/data/testing-build-id/en-US.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"en-US\""
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/en/index.json",
|
||||
"path": "/_next/data/testing-build-id/en.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"en\""
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/fr/index.json",
|
||||
"path": "/_next/data/testing-build-id/fr.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"fr\""
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/nl/index.json",
|
||||
"path": "/_next/data/testing-build-id/nl.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"locale\":\"nl\""
|
||||
},
|
||||
@@ -520,6 +524,280 @@
|
||||
"path": "/_next/data/testing-build-id/fr/gsp/blocking/first.json",
|
||||
"status": 200,
|
||||
"mustContain": "\"catchall\":\"yes\""
|
||||
},
|
||||
|
||||
{
|
||||
"path": "/en-US/redirect-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 307,
|
||||
"responseHeaders": {
|
||||
"location": "//somewhere-else/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/en/redirect-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/nl/redirect-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 307,
|
||||
"responseHeaders": {
|
||||
"location": "//somewhere-else/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/en-US/redirect-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/redirect-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 307,
|
||||
"responseHeaders": {
|
||||
"location": "//somewhere-else/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/en-US/redirect-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 307,
|
||||
"responseHeaders": {
|
||||
"location": "//somewhere-else/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/fr/redirect-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 307,
|
||||
"responseHeaders": {
|
||||
"location": "//somewhere-else/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nl-NL/redirect-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 307,
|
||||
"responseHeaders": {
|
||||
"location": "//somewhere-else/"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "/en-US/rewrite-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "another page"
|
||||
},
|
||||
{
|
||||
"path": "/en-US/rewrite-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "lang=\"en-US\""
|
||||
},
|
||||
{
|
||||
"path": "/nl/rewrite-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/nl/rewrite-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "another page"
|
||||
},
|
||||
{
|
||||
"path": "/nl/rewrite-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "lang=\"nl\""
|
||||
},
|
||||
{
|
||||
"path": "/rewrite-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/fr/rewrite-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "another page"
|
||||
},
|
||||
{
|
||||
"path": "/fr/rewrite-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "lang=\"nl\""
|
||||
},
|
||||
{
|
||||
"path": "/rewrite-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/rewrite-4",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "another page"
|
||||
},
|
||||
{
|
||||
"path": "/rewrite-4",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "lang=\"en-US\""
|
||||
},
|
||||
{
|
||||
"path": "/en/rewrite-4",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "another page"
|
||||
},
|
||||
{
|
||||
"path": "/en/rewrite-4",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "lang=\"en\""
|
||||
},
|
||||
{
|
||||
"path": "/fr/rewrite-4",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "another page"
|
||||
},
|
||||
{
|
||||
"path": "/fr/rewrite-4",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 200,
|
||||
"mustContain": "lang=\"fr\""
|
||||
},
|
||||
|
||||
{
|
||||
"path": "/en-US/add-header-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": "world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/en/add-header-1",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nl/add-header-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": "world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/en-US/add-header-2",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/add-header-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": "world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/en-US/add-header-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": "world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/fr/add-header-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": "world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nl-NL/add-header-3",
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 404,
|
||||
"responseHeaders": {
|
||||
"x-hello": "world"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports = function (ctx) {
|
||||
const props = await getProps('/', { params: {} });
|
||||
expect(props.params).toEqual({});
|
||||
|
||||
await waitFor(2000);
|
||||
await waitFor(4000);
|
||||
await getProps('/');
|
||||
|
||||
const newProps = await getProps('/', { params: {} });
|
||||
@@ -30,7 +30,7 @@ module.exports = function (ctx) {
|
||||
const props = await getProps('/a');
|
||||
expect(props.params).toEqual({ slug: ['a'] });
|
||||
|
||||
await waitFor(2000);
|
||||
await waitFor(4000);
|
||||
await getProps('/a');
|
||||
|
||||
const newProps = await getProps('/a');
|
||||
@@ -42,7 +42,7 @@ module.exports = function (ctx) {
|
||||
const props = await getProps('/hello/world');
|
||||
expect(props.params).toEqual({ slug: ['hello', 'world'] });
|
||||
|
||||
await waitFor(2000);
|
||||
await waitFor(4000);
|
||||
await getProps('/hello/world');
|
||||
|
||||
const newProps = await getProps('/hello/world');
|
||||
|
||||
@@ -13,7 +13,7 @@ module.exports = function (ctx) {
|
||||
expect($('#hello').text()).toBe('hello: world');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/another`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -34,7 +34,7 @@ module.exports = function (ctx) {
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -56,7 +56,7 @@ module.exports = function (ctx) {
|
||||
expect($('#comment').text()).toBe('Comment: comment-321');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -82,7 +82,7 @@ module.exports = function (ctx) {
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
|
||||
@@ -111,7 +111,7 @@ module.exports = function (ctx) {
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
|
||||
@@ -141,7 +141,7 @@ module.exports = function (ctx) {
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
|
||||
|
||||
@@ -5,7 +5,7 @@ const cheerio = require('cheerio');
|
||||
module.exports = function (ctx) {
|
||||
it('should revalidate content properly from dynamic pathname', async () => {
|
||||
// wait for revalidation to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}/regenerated/blue`);
|
||||
expect(res.status).toBe(200);
|
||||
@@ -15,7 +15,7 @@ module.exports = function (ctx) {
|
||||
expect($('#slug').text()).toBe('blue');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/regenerated/blue`);
|
||||
expect(res2.status).toBe(200);
|
||||
@@ -27,7 +27,7 @@ module.exports = function (ctx) {
|
||||
|
||||
it('should revalidate content properly from /_next/data dynamic pathname', async () => {
|
||||
// wait for revalidation to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/regenerated/blue.json`
|
||||
@@ -40,7 +40,7 @@ module.exports = function (ctx) {
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/regenerated/blue.json`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/node",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.6-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@types/etag": "1.8.0",
|
||||
"@types/test-listen": "1.1.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.9.2",
|
||||
"@vercel/nft": "0.9.4",
|
||||
"content-type": "1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"etag": "1.8.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/routing-utils",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.2-canary.2",
|
||||
"description": "Vercel routing utilities",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -89,6 +89,7 @@ export function mergeRoutes({ userRoutes, builds }: MergeRoutesProps): Route[] {
|
||||
|
||||
const outputRoutes: Route[] = [];
|
||||
const uniqueHandleValues = new Set([
|
||||
null,
|
||||
...userHandleMap.keys(),
|
||||
...builderHandleMap.keys(),
|
||||
]);
|
||||
|
||||
@@ -80,6 +80,9 @@ export const routesSchema = {
|
||||
check: {
|
||||
type: 'boolean',
|
||||
},
|
||||
important: {
|
||||
type: 'boolean',
|
||||
},
|
||||
status: {
|
||||
type: 'integer',
|
||||
minimum: 100,
|
||||
|
||||
@@ -119,6 +119,9 @@ export function convertHeaders(headers: NowHeader[]): Route[] {
|
||||
export function convertTrailingSlash(enable: boolean, status = 308): Route[] {
|
||||
const routes: Route[] = [];
|
||||
if (enable) {
|
||||
routes.push({
|
||||
src: '^/\\.well-known(?:/.*)?$'
|
||||
});
|
||||
routes.push({
|
||||
src: '^/((?:[^/]+/)*[^/\\.]+)$',
|
||||
headers: { Location: '/$1/' },
|
||||
|
||||
@@ -72,6 +72,7 @@ describe('normalizeRoutes', () => {
|
||||
src: '^/missed-me$',
|
||||
headers: { 'Cache-Control': 'max-age=10' },
|
||||
continue: true,
|
||||
important: true,
|
||||
},
|
||||
{ handle: 'rewrite' },
|
||||
{ src: '^.*$', dest: '/somewhere' },
|
||||
|
||||
82
packages/now-routing-utils/test/merge.spec.js
vendored
@@ -1,4 +1,4 @@
|
||||
const { deepEqual } = require('assert');
|
||||
const { deepStrictEqual } = require('assert');
|
||||
const { mergeRoutes } = require('../dist/merge');
|
||||
|
||||
test('mergeRoutes simple', () => {
|
||||
@@ -10,7 +10,10 @@ test('mergeRoutes simple', () => {
|
||||
{
|
||||
use: '@now/node',
|
||||
entrypoint: 'api/home.js',
|
||||
routes: [{ src: '/node1', dest: '/n1' }, { src: '/node2', dest: '/n2' }],
|
||||
routes: [
|
||||
{ src: '/node1', dest: '/n1' },
|
||||
{ src: '/node2', dest: '/n2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
use: '@now/python',
|
||||
@@ -30,7 +33,7 @@ test('mergeRoutes simple', () => {
|
||||
{ dest: '/py1', src: '/python1' },
|
||||
{ dest: '/py2', src: '/python2' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes handle filesystem user routes', () => {
|
||||
@@ -43,7 +46,10 @@ test('mergeRoutes handle filesystem user routes', () => {
|
||||
{
|
||||
use: '@now/node',
|
||||
entrypoint: 'api/home.js',
|
||||
routes: [{ src: '/node1', dest: '/n1' }, { src: '/node2', dest: '/n2' }],
|
||||
routes: [
|
||||
{ src: '/node1', dest: '/n1' },
|
||||
{ src: '/node2', dest: '/n2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
use: '@now/python',
|
||||
@@ -64,7 +70,7 @@ test('mergeRoutes handle filesystem user routes', () => {
|
||||
{ handle: 'filesystem' },
|
||||
{ dest: '/u2', src: '/user2' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes handle filesystem build routes', () => {
|
||||
@@ -102,7 +108,7 @@ test('mergeRoutes handle filesystem build routes', () => {
|
||||
{ dest: '/n2', src: '/node2' },
|
||||
{ dest: '/py2', src: '/python2' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes handle filesystem both user and builds', () => {
|
||||
@@ -141,7 +147,7 @@ test('mergeRoutes handle filesystem both user and builds', () => {
|
||||
{ dest: '/n2', src: '/node2' },
|
||||
{ dest: '/py2', src: '/python2' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes continue true', () => {
|
||||
@@ -182,7 +188,7 @@ test('mergeRoutes continue true', () => {
|
||||
{ dest: '/py1', src: '/python1' },
|
||||
{ dest: '/py3', src: '/python3' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes check true', () => {
|
||||
@@ -223,7 +229,7 @@ test('mergeRoutes check true', () => {
|
||||
{ dest: '/py1', src: '/python1' },
|
||||
{ dest: '/py3', src: '/python3' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes check true, continue true, handle filesystem middle', () => {
|
||||
@@ -268,7 +274,7 @@ test('mergeRoutes check true, continue true, handle filesystem middle', () => {
|
||||
{ dest: '/n3', src: '/node3' },
|
||||
{ dest: '/py3', src: '/python3' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes check true, continue true, handle filesystem top', () => {
|
||||
@@ -306,7 +312,7 @@ test('mergeRoutes check true, continue true, handle filesystem top', () => {
|
||||
{ dest: '/n1', src: '/node1' },
|
||||
{ dest: '/py1', src: '/python1' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes multiple handle values', () => {
|
||||
@@ -359,5 +365,57 @@ test('mergeRoutes multiple handle values', () => {
|
||||
{ dest: '/u3', src: '/user3' },
|
||||
{ check: true, dest: '/py2', src: '/python2' },
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('mergeRoutes ensure `handle: error` comes last', () => {
|
||||
const userRoutes = [];
|
||||
const builds = [
|
||||
{
|
||||
use: '@vercel/static-build',
|
||||
entrypoint: 'packge.json',
|
||||
routes: [
|
||||
{
|
||||
src: '^/home$',
|
||||
status: 301,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
use: '@vercel/zero-config-routes',
|
||||
entrypoint: '/',
|
||||
routes: [
|
||||
{
|
||||
handle: 'error',
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
dest: '404.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const actual = mergeRoutes({ userRoutes, builds });
|
||||
const expected = [
|
||||
{
|
||||
status: 301,
|
||||
src: '^/home$',
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
},
|
||||
{
|
||||
handle: 'error',
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
dest: '404.html',
|
||||
},
|
||||
];
|
||||
deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
@@ -671,6 +671,7 @@ test('convertHeaders', () => {
|
||||
test('convertTrailingSlash enabled', () => {
|
||||
const actual = convertTrailingSlash(true);
|
||||
const expected = [
|
||||
{ src: '^/\\.well-known(?:/.*)?$' },
|
||||
{
|
||||
src: '^/((?:[^/]+/)*[^/\\.]+)$',
|
||||
headers: { Location: '/$1/' },
|
||||
@@ -685,11 +686,23 @@ test('convertTrailingSlash enabled', () => {
|
||||
deepEqual(actual, expected);
|
||||
|
||||
const mustMatch = [
|
||||
[
|
||||
'/.well-known',
|
||||
'/.well-known/',
|
||||
'/.well-known/asdf',
|
||||
'/.well-known/asdf/',
|
||||
],
|
||||
['/dir', '/dir/foo', '/dir/foo/bar'],
|
||||
['/foo.html/', '/dir/foo.html/', '/dir/foo/bar.css/', '/dir/about.map.js/'],
|
||||
];
|
||||
|
||||
const mustNotMatch = [
|
||||
[
|
||||
'/swell-known',
|
||||
'/swell-known/',
|
||||
'/swell-known/asdf',
|
||||
'/swell-known/asdf/',
|
||||
],
|
||||
[
|
||||
'/',
|
||||
'/index.html',
|
||||
|
||||
@@ -251,7 +251,9 @@ async function testDeployment(
|
||||
const expected = probe.responseHeaders[header];
|
||||
const isEqual = Array.isArray(expected)
|
||||
? expected.every(h => actual.includes(h))
|
||||
: expected.startsWith('/') && expected.endsWith('/')
|
||||
: typeof expected === 'string' &&
|
||||
expected.startsWith('/') &&
|
||||
expected.endsWith('/')
|
||||
? new RegExp(expected.slice(1, -1)).test(actual)
|
||||
: expected === actual;
|
||||
if (!isEqual) {
|
||||
|
||||
@@ -2226,10 +2226,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@vercel/ncc/-/ncc-0.24.0.tgz#a2e8783a185caa99b5d8961a57dfc9665de16296"
|
||||
integrity sha512-crqItMcIwCkvdXY/V3/TzrHJQx6nbIaRqE1cOopJhgGX6izvNov40SmD//nS5flfEvdK54YGjwVVq+zG6crjOg==
|
||||
|
||||
"@vercel/nft@0.9.2":
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-0.9.2.tgz#677ecefb0bd618143281c62c719baca57a36ac4d"
|
||||
integrity sha512-Dr2yJlCnfkQEt4QHKcPJKTxCyoBX0YCzHDzozd8upBFm8kKbh2yMSu5wp+1btevQXOMkOUtxntovwwPHDIU51w==
|
||||
"@vercel/nft@0.9.4":
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-0.9.4.tgz#43df00808e63fd1b04e7ac15898661985b48ff2c"
|
||||
integrity sha512-vy9i2A9oZDiYgznMXHSdzgHa1sc3bHVJ42DUpRe5jcxJl0eOGgSmulVWkDkuXIeWA7cqYzN/ofb21B/le2CDNg==
|
||||
dependencies:
|
||||
acorn "^7.1.1"
|
||||
acorn-class-fields "^0.3.2"
|
||||
|
||||