Compare commits

..

44 Commits

Author SHA1 Message Date
Steven
25bea3f83e Publish Canary
- vercel@21.0.2-canary.1
 - @vercel/next@2.7.5-canary.0
 - @vercel/node@1.8.6-canary.0
2020-11-24 15:24:52 -05:00
Steven
b3e1828ebe [node][next] Bump nft to 0.9.4 (#5455)
This PR bumps nft to [0.9.4](https://github.com/vercel/nft/releases/tag/0.9.4)
2020-11-24 14:32:46 -05:00
Andy Bitz
e0e2a8e87e Publish Canary
- @vercel/routing-utils@1.9.2-canary.2
2020-11-24 19:52:19 +01:00
Andy
37ec89796d [routing-utils] Handle null phase at the beginning (#5470)
* [routing-utils] Handle `null` phase at the beginning

* Add fixed list

* Add tests

* Change hit and miss order

* Put null at the start

* Undo order changes

Co-authored-by: Steven <steven@ceriously.com>
2020-11-24 19:51:06 +01:00
JJ Kasper
d3e2a4c4db Publish Stable
- @vercel/next@2.7.4
2020-11-24 11:14:03 -06:00
JJ Kasper
ece3645914 Publish Canary
- vercel@21.0.2-canary.0
 - @vercel/next@2.7.4-canary.0
2020-11-24 10:53:50 -06:00
JJ Kasper
fee386493b [next] Add handle: resource to ensure 404 page (#5454)
This adds `handle: resource` to ensure the 404 page is shown instead of a directory listing when there isn't an index route for an output directory. This also removes the `.well-known` route added in https://github.com/vercel/vercel/pull/5447 in favor of adding it in Next.js itself here https://github.com/vercel/next.js/pull/19364 and revalidate tests wait times are increased in this PR due to [this thread](https://vercel.slack.com/archives/C9MMK4674/p1605821738156200)

x-ref: https://github.com/vercel/next.js/pull/19364
Fixes: https://github.com/vercel/next.js/issues/16555
2020-11-24 16:38:43 +00:00
Luc Leray
d95ed184ab [tests] Add test for secrets decryption in vc env pull and vc dev (#5461)
* add test for env with decryptable secret

* Apply suggestions from code review

Co-authored-by: Nathan Rajlich <nathan@tootallnate.net>

Co-authored-by: Nathan Rajlich <nathan@tootallnate.net>
2020-11-23 10:38:29 +01:00
luc
aef8e6388e Publish Stable
- vercel@21.0.1
2020-11-23 00:48:10 +01:00
luc
ba43e88603 Publish Canary
- vercel@21.0.1-canary.0
2020-11-23 00:39:59 +01:00
Igor Klopov
e61f9740c4 [cli] Fix secrets decryption when running vc env pull and vc dev (#5460) 2020-11-23 00:39:17 +01:00
luc
c4b010fe8b Publish Stable
- vercel@21.0.0
2020-11-20 23:10:15 +01:00
luc
db18eb091f Publish Canary
- vercel@20.1.5-canary.6
2020-11-20 17:33:55 +01:00
Luc Leray
360e62d172 [cli] Download automatically exposed System Environment Variables when running vercel env pull (#5451)
* fix VERCEL_REGION and NOW_REGION

* remove VERCEL_REGION from exposeSystemEnvs

* refactor exposeSystemEnvs

* refactor getDecryptedEnvRecords

* consider project envs in exposeSystemEnvs

* simplify exposeSystemEnvs

* correctly set value of VERCEL_URL system envs

* make exposeSystemEnvs return all envs object

* parse url in server

* simplify

* refactor getDecryptedEnvRecords

* add comment

* remove unnecessary code

* add test

* fix test

* fix dev server unit tests

* always expose NOW_REGION

* fix dev test

* fix dev test

* only retrieve system env values when autoExposeSystemEnvs is true
2020-11-20 17:33:16 +01:00
JJ Kasper
8c3cd0332d Publish Stable
- @vercel/next@2.7.3
2020-11-19 14:47:12 -06:00
JJ Kasper
f5f276021e Publish Canary
- @vercel/next@2.7.3-canary.0
2020-11-19 14:30:56 -06:00
JJ Kasper
9fbec823f3 [next] Ensure public files are matched with i18n domains (#5447)
This makes sure we strip all locales during the `handle: 'miss'` phase to allow locale domains to match public files correctly. This also updates the trailing slash redirect handling to match the behavior for `.well-known` added in https://github.com/vercel/vercel/pull/5407

We aren't able to add tests for locale domains with the current test flow yet since they require domains be assigned although the changes in this PR was tested manually against http://i18n-support.vercel.app/ with `@ijjk/now-next@0.1.3-i18n`

x-ref: https://github.com/vercel/vercel/pull/5407
Fixes: https://github.com/vercel/next.js/issues/19324
2020-11-19 20:24:46 +00:00
luc
18c3dd3a63 Publish Canary
- vercel@20.1.5-canary.5
2020-11-19 16:01:27 +01:00
Ana Trajkovska
5a4a20b33f Remove code which removes auto-generated secrets (#5440) 2020-11-19 16:00:57 +01:00
luc
4489ed0c85 Publish Canary
- vercel@20.1.5-canary.4
2020-11-18 16:11:28 +01:00
Luc Leray
359f23daf1 [cli] Expose VERCEL_REGION when autoExposeSystemEnvs is true (#5437)
* auto expose VERCEL_REGION

* add test
2020-11-18 16:00:20 +01:00
luc
4ef92e85db Publish Canary
- vercel@20.1.5-canary.3
2020-11-18 00:27:15 +01:00
Naoyuki Kanezawa
659c4d6ccd [cli] Expose system envs when autoExposeSystemEnvs is enabled on vc dev (#5434)
Ref: https://app.clubhouse.io/vercel/story/15112

We added a property called `autoExposeSystemEnvs` to projects. If that property is `true`, we automatically expose system env variables such as `VERCEL=1`, `VERCEL_ENV=<production | preview>`, ... to the runtime and build time.

This PR makes sure we mirror this behavior when running `vc dev` locally.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2020-11-17 23:21:40 +00:00
JJ Kasper
e93d477df8 Publish Stable
- @vercel/next@2.7.2
2020-11-17 14:49:04 -06:00
JJ Kasper
f64625655b Publish Canary
- @vercel/next@2.7.2-canary.0
2020-11-17 14:11:32 -06:00
JJ Kasper
25a8189997 [next] Correct non-dynamic index SSG data route output (#5435)
Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
2020-11-17 15:09:56 -05:00
Andy Bitz
25c3e627cf Publish Canary
- @vercel/build-utils@2.6.1-canary.0
 - vercel@20.1.5-canary.2
 - @vercel/client@9.0.5-canary.0
2020-11-17 18:13:30 +01:00
Andy
1d6d8b530f [build-utils] Remove continue: true from 404 route (#5432)
* [build-utils] Remove `continue: true` from 404 route

* Update tests
2020-11-17 18:08:29 +01:00
Steven
e821cc0ae7 [cli] Add tests to compare dev/prd image optimization (#5428)
Add E2E test for to test image optimization against `vc dev` as well as a prod deployment.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR (CH13104)
2020-11-16 20:20:27 +00:00
JJ Kasper
8ecbdc5d03 Publish Stable
- @vercel/next@2.7.1
2020-11-14 16:48:35 -06:00
JJ Kasper
895224985b Publish Canary
- @vercel/next@2.7.1-canary.0
2020-11-14 16:23:24 -06:00
JJ Kasper
0f42a63c03 [next] Ensure correct route order for i18n + custom-routes (#5421)
Co-authored-by: Steven <steven@ceriously.com>
2020-11-14 17:13:08 -05:00
dav-is
81e4c9e6fe Publish Canary
- @vercel/routing-utils@1.9.2-canary.1
2020-11-13 13:48:28 -05:00
Connor Davis
a0a29dc836 [routing-utils] Exclude /.well-known from trailing slash redirect (#5407)
`/.well-known` files shouldn't have trailing slashes added as these clients likely do not follow redirects

Specifically, `/.well-known/apple-developer-merchantid-domain-association` cannot have trailing slash.

### Related Issues

https://vercel.slack.com/archives/CLDDX2Y0G/p1605127589058800

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2020-11-12 22:38:01 +00:00
luc
c1f9d51d7a Publish Canary
- vercel@20.1.5-canary.1
2020-11-12 22:06:16 +01:00
Luc Leray
422f0558c1 [cli] Rename "Provided by system" to "Reference ..." (#5415)
* Revert "Revert "[cli] (major) Update `vercel env` (#5372)" (#5410)"

This reverts commit 2d24a75ca6.

* fetch system env values from dedicated endpoint

* rename "Provided by System"
2020-11-12 22:05:24 +01:00
luc
f064ae2908 Publish Canary
- vercel@20.1.5-canary.0
 - @vercel/routing-utils@1.9.2-canary.0
2020-11-12 21:07:28 +01:00
Luc Leray
58c3e636f0 [cli] (Major) Update vc env (#5413)
* Revert "Revert "[cli] (major) Update `vercel env` (#5372)" (#5410)"

This reverts commit 2d24a75ca6.

* fetch system env values from dedicated endpoint
2020-11-12 21:06:25 +01:00
Connor Davis
d5081367f3 [routing-utils] Add important headers to disallow users from overriding (#5409)
If a builder wants to set a header that isn't allowed to be overridden by users, it should use `important: true`

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR: CH-14921
2020-11-12 17:45:33 +00:00
luc
0ee88366ff Publish Stable
- vercel@20.1.4
2020-11-12 17:16:29 +01:00
luc
9ae42c9e92 Publish Canary
- vercel@20.1.4-canary.1
2020-11-12 16:59:28 +01:00
Luc Leray
62b8df4a8d [cli] Fix vc env rm with advanced env variables (#5411) 2020-11-12 16:58:41 +01:00
luc
73ec7f3018 Publish Canary
- vercel@20.1.4-canary.0
2020-11-12 15:31:54 +01:00
Luc Leray
2d24a75ca6 Revert "[cli] (major) Update vercel env (#5372)" (#5410)
* Revert "[cli] (major) Update `vercel env` (#5372)"

This reverts commit 9a57cc72dd.

* fix test

* do not change prompt UI
2020-11-12 15:30:47 +01:00
54 changed files with 1072 additions and 402 deletions

View File

@@ -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",

View File

@@ -989,7 +989,6 @@ function getRouteResult(
rewriteRoutes.push({
src: '^/api(/.*)?$',
status: 404,
continue: true,
});
}
} else {

View File

@@ -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,
},
]);
}

View File

@@ -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"

View File

@@ -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());

View File

@@ -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: `Whats the value of ${envName}?`,
choices: SYSTEM_ENV_VALUES.map(value => ({ name: value, value })),
choices: systemEnvValues.map(value => ({ name: value, value })),
});
envValue = systemEnvValue;

View File

@@ -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 =

View File

@@ -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 {

View File

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

View 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;
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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;
}

View 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);
}

View File

@@ -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' });
}
}
}

View File

@@ -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',
];

View File

@@ -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 };
}

View File

@@ -0,0 +1,4 @@
.next
!public
yarn.lock
.vercel

View File

@@ -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"
}
}

View File

@@ -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" />
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -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

View File

@@ -0,0 +1,8 @@
{
"version": 2,
"build": {
"env": {
"FORCE_BUILDER_TAG": "canary"
}
}
}

View File

@@ -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 => {

View File

@@ -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();

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
},

View File

@@ -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\""
},

View File

@@ -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);

View File

@@ -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`

View File

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

View File

@@ -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"
}
}
]
}

View File

@@ -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');

View File

@@ -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`

View File

@@ -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`

View File

@@ -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",

View File

@@ -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",

View File

@@ -89,6 +89,7 @@ export function mergeRoutes({ userRoutes, builds }: MergeRoutesProps): Route[] {
const outputRoutes: Route[] = [];
const uniqueHandleValues = new Set([
null,
...userHandleMap.keys(),
...builderHandleMap.keys(),
]);

View File

@@ -80,6 +80,9 @@ export const routesSchema = {
check: {
type: 'boolean',
},
important: {
type: 'boolean',
},
status: {
type: 'integer',
minimum: 100,

View File

@@ -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/' },

View File

@@ -72,6 +72,7 @@ describe('normalizeRoutes', () => {
src: '^/missed-me$',
headers: { 'Cache-Control': 'max-age=10' },
continue: true,
important: true,
},
{ handle: 'rewrite' },
{ src: '^.*$', dest: '/somewhere' },

View File

@@ -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);
});

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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"