Files
vercel/packages/cli/test/integration.js
Felix Haus 1d3f2b5a62 [build-utils][cli] Update link to project-configuration (#9439)
The link to the documentation has changed.
It is now available under https://vercel.com/docs/concepts/projects/project-configuration

This updates:
- link generation `https://vercel.com/docs/configuration#project/*` -> `https://vercel.com/docs/concepts/projects/project-configuration#*`
-  Updates test files
- Updates static references of https://vercel.com/docs/configuration across the repo
2023-02-14 16:44:24 +00:00

3974 lines
105 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ms from 'ms';
import path from 'path';
import { URL, parse as parseUrl } from 'url';
import test from 'ava';
import semVer from 'semver';
import { Readable } from 'stream';
import { homedir, tmpdir } from 'os';
import _execa from 'execa';
import XDGAppPaths from 'xdg-app-paths';
import fetch from 'node-fetch';
import tmp from 'tmp-promise';
import retry from 'async-retry';
import fs, {
writeFile,
readFile,
remove,
copy,
ensureDir,
exists,
mkdir,
} from 'fs-extra';
import logo from '../src/util/output/logo';
import sleep from '../src/util/sleep';
import pkg from '../package';
import prepareFixtures from './helpers/prepare';
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
// log command when running `execa`
function execa(file, args, options) {
console.log(`$ vercel ${args.join(' ')}`);
return _execa(file, args, options);
}
function fixture(name) {
const directory = path.join(tmpFixturesDir, name);
const config = path.join(directory, 'project.json');
// We need to remove it, otherwise we can't re-use fixtures
if (fs.existsSync(config)) {
fs.unlinkSync(config);
}
return directory;
}
const binaryPath = path.resolve(__dirname, `../scripts/start.js`);
const example = name =>
path.join(__dirname, '..', '..', '..', 'examples', name);
const deployHelpMessage = `${logo} vercel [options] <command | path>`;
let session = 'temp-session';
const isCanary = pkg.version.includes('canary');
const pickUrl = stdout => {
const lines = stdout.split('\n');
return lines[lines.length - 1];
};
const createFile = dest => fs.closeSync(fs.openSync(dest, 'w'));
const waitForDeployment = async href => {
console.log(`waiting for ${href} to become ready...`);
const start = Date.now();
const max = ms('4m');
const inspectorText = '<title>Deployment Overview';
// eslint-disable-next-line
while (true) {
const response = await fetch(href, { redirect: 'manual' });
const text = await response.text();
if (response.status === 200 && !text.includes(inspectorText)) {
break;
}
const current = Date.now();
if (current - start > max || response.status >= 500) {
throw new Error(
`Waiting for "${href}" failed since it took longer than 4 minutes.\n` +
`Received status ${response.status}:\n"${text}"`
);
}
await sleep(2000);
}
};
function fetchTokenInformation(token, retries = 3) {
const url = `https://api.vercel.com/v2/user`;
const headers = { Authorization: `Bearer ${token}` };
return retry(
async () => {
const res = await fetch(url, { headers });
if (!res.ok) {
throw new Error(
`Failed to fetch ${url}, received status ${res.status}`
);
}
const data = await res.json();
return data.user;
},
{ retries, factor: 1 }
);
}
function formatOutput({ stderr, stdout }) {
return `
-----
Stderr:
${stderr}
-----
Stdout:
${stdout}
-----
`;
}
async function vcLink(t, projectPath) {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--yes', ...defaultArgs],
{
reject: false,
cwd: projectPath,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
// AVA's `t.context` can only be set before the tests,
// but we want to set it within as well
const context = {};
const defaultOptions = { reject: false };
const defaultArgs = [];
let token;
let email;
let contextName;
let tmpDir;
let tmpFixturesDir = path.join(tmpdir(), 'tmp-fixtures');
let globalDir = XDGAppPaths('com.vercel.cli').dataDirs()[0];
if (!process.env.CI) {
tmpDir = tmp.dirSync({
// This ensures the directory gets
// deleted even if it has contents
unsafeCleanup: true,
});
globalDir = path.join(tmpDir.name, 'com.vercel.tests');
defaultArgs.push('-Q', globalDir);
console.log(
'No CI detected, adding defaultArgs to avoid polluting user settings',
defaultArgs
);
}
function mockLoginApi(req, res) {
const { url = '/', method } = req;
let { pathname = '/', query = {} } = parseUrl(url, true);
console.log(`[mock-login-server] ${method} ${pathname}`);
const securityCode = 'Bears Beets Battlestar Galactica';
res.setHeader('content-type', 'application/json');
if (
method === 'POST' &&
pathname === '/registration' &&
query.mode === 'login'
) {
res.end(JSON.stringify({ token, securityCode }));
} else if (
method === 'GET' &&
pathname === '/registration/verify' &&
query.email === email
) {
res.end(JSON.stringify({ token }));
} else {
res.statusCode = 405;
res.end(JSON.stringify({ code: 'method_not_allowed' }));
}
}
let loginApiUrl = '';
const loginApiServer = require('http')
.createServer(mockLoginApi)
.listen(0, () => {
const { port } = loginApiServer.address();
loginApiUrl = `http://localhost:${port}`;
console.log(`[mock-login-server] Listening on ${loginApiUrl}`);
});
const execute = (args, options) =>
execa(binaryPath, [...defaultArgs, ...args], {
...defaultOptions,
...options,
});
const apiFetch = (url, { headers, ...options } = {}) => {
return fetch(`https://api.vercel.com${url}`, {
headers: {
Authorization: `Bearer ${token}`,
...(headers || {}),
},
...options,
});
};
const waitForPrompt = (cp, assertion) =>
new Promise((resolve, reject) => {
console.log('Waiting for prompt...');
setTimeout(() => reject(new Error('timeout in waitForPrompt')), 60000);
const listener = chunk => {
console.log('> ' + chunk);
if (assertion(chunk)) {
cp.stdout.off && cp.stdout.off('data', listener);
cp.stderr.off && cp.stderr.off('data', listener);
resolve();
}
};
cp.stdout.on('data', listener);
cp.stderr.on('data', listener);
});
const createUser = async () => {
await retry(
async () => {
if (!fs.existsSync(globalDir)) {
console.log('Creating global config directory ', globalDir);
await ensureDir(globalDir);
} else {
console.log('Found global config directory ', globalDir);
}
token = await fetchTokenWithRetry();
await fs.writeJSON(getConfigAuthPath(), { token });
const user = await fetchTokenInformation(token);
email = user.email;
contextName = user.username;
session = Math.random().toString(36).split('.')[1];
},
{ retries: 3, factor: 1 }
);
};
const getConfigAuthPath = () => path.join(globalDir, 'auth.json');
async function setupProject(process, projectName, overrides) {
await waitForPrompt(process, chunk => /Set up [^?]+\?/.test(chunk));
process.stdin.write('yes\n');
await waitForPrompt(process, chunk => /Which scope [^?]+\?/.test(chunk));
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Link to existing project?')
);
process.stdin.write('no\n');
await waitForPrompt(process, chunk =>
chunk.includes('Whats your projects name?')
);
process.stdin.write(`${projectName}\n`);
await waitForPrompt(process, chunk =>
chunk.includes('In which directory is your code located?')
);
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Want to modify these settings?')
);
if (overrides) {
process.stdin.write('yes\n');
const { buildCommand, outputDirectory, devCommand } = overrides;
await waitForPrompt(process, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
process.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Build Command?`)
);
process.stdin.write(`${buildCommand || ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Development Command?`)
);
process.stdin.write(`${devCommand || ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Output Directory?`)
);
process.stdin.write(`${outputDirectory || ''}\n`);
} else {
process.stdin.write('no\n');
}
await waitForPrompt(process, chunk => chunk.includes('Linked to'));
}
test.before(async () => {
try {
await createUser();
await prepareFixtures(contextName, binaryPath, tmpFixturesDir);
} catch (err) {
console.log('Failed `test.before`');
console.log(err);
}
});
test.after.always(async () => {
delete process.env.ENABLE_EXPERIMENTAL_COREPACK;
if (loginApiServer) {
// Stop mock server
loginApiServer.close();
}
// Make sure the token gets revoked unless it's passed in via environment
if (!process.env.VERCEL_TOKEN) {
await execa(binaryPath, ['logout', ...defaultArgs]);
}
if (tmpDir) {
// Remove config directory entirely
tmpDir.removeCallback();
}
if (tmpFixturesDir) {
console.log('removing tmpFixturesDir', tmpFixturesDir);
fs.removeSync(tmpFixturesDir);
}
});
test('default command should prompt login with empty auth.json', async t => {
await fs.writeFile(getConfigAuthPath(), JSON.stringify({}));
try {
await execa(binaryPath, [...defaultArgs]);
t.fail();
} catch (err) {
t.true(
err.stderr.includes(
'Error: No existing credentials found. Please run `vercel login` or pass "--token"'
)
);
}
});
// NOTE: Test order is important here.
// This test MUST run before the tests below for them to work.
test('login', async t => {
t.timeout(ms('1m'));
await fs.remove(getConfigAuthPath());
const loginOutput = await execa(binaryPath, [
'login',
email,
'--api',
loginApiUrl,
...defaultArgs,
]);
t.is(loginOutput.exitCode, 0, formatOutput(loginOutput));
t.regex(
loginOutput.stderr,
/You are now logged in\./gm,
formatOutput(loginOutput)
);
const auth = await fs.readJSON(getConfigAuthPath());
t.is(auth.token, token);
});
test('[vc build] should build project with corepack and select npm@8.1.0', async t => {
process.env.ENABLE_EXPERIMENTAL_COREPACK = '1';
const directory = fixture('vc-build-corepack-npm');
const before = await _execa('npm', ['--version'], {
cwd: directory,
reject: false,
});
const output = await execute(['build'], { cwd: directory });
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /Build Completed/gm);
const after = await _execa('npm', ['--version'], {
cwd: directory,
reject: false,
});
// Ensure global npm didn't change
t.is(before.stdout, after.stdout);
// Ensure version is correct
t.is(
await fs.readFile(
path.join(directory, '.vercel/output/static/index.txt'),
'utf8'
),
'8.1.0\n'
);
// Ensure corepack will be cached
const contents = fs.readdirSync(
path.join(directory, '.vercel/cache/corepack')
);
t.deepEqual(contents, ['home', 'shim']);
});
test('[vc build] should build project with corepack and select pnpm@7.1.0', async t => {
process.env.ENABLE_EXPERIMENTAL_COREPACK = '1';
const directory = fixture('vc-build-corepack-pnpm');
const before = await _execa('pnpm', ['--version'], {
cwd: directory,
reject: false,
});
const output = await execute(['build'], { cwd: directory });
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /Build Completed/gm);
const after = await _execa('pnpm', ['--version'], {
cwd: directory,
reject: false,
});
// Ensure global pnpm didn't change
t.is(before.stdout, after.stdout);
// Ensure version is correct
t.is(
await fs.readFile(
path.join(directory, '.vercel/output/static/index.txt'),
'utf8'
),
'7.1.0\n'
);
// Ensure corepack will be cached
const contents = fs.readdirSync(
path.join(directory, '.vercel/cache/corepack')
);
t.deepEqual(contents, ['home', 'shim']);
});
test('[vc build] should build project with corepack and select yarn@2.4.3', async t => {
process.env.ENABLE_EXPERIMENTAL_COREPACK = '1';
const directory = fixture('vc-build-corepack-yarn');
const before = await _execa('yarn', ['--version'], {
cwd: directory,
reject: false,
});
const output = await execute(['build'], { cwd: directory });
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /Build Completed/gm);
const after = await _execa('yarn', ['--version'], {
cwd: directory,
reject: false,
});
// Ensure global yarn didn't change
t.is(before.stdout, after.stdout);
// Ensure version is correct
t.is(
await fs.readFile(
path.join(directory, '.vercel/output/static/index.txt'),
'utf8'
),
'2.4.3\n'
);
// Ensure corepack will be cached
const contents = fs.readdirSync(
path.join(directory, '.vercel/cache/corepack')
);
t.deepEqual(contents, ['home', 'shim']);
});
test('[vc dev] should print help from `vc develop --help`', async t => {
const directory = fixture('static-deployment');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['develop', '--help', ...defaultArgs],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 2, formatOutput({ stdout, stderr }));
t.regex(stdout, /▲ vercel dev/gm);
});
test('default command should deploy directory', async t => {
const projectDir = fixture('deploy-default-with-sub-directory');
const target = 'output';
await vcLink(t, path.join(projectDir, target));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
// omit the default "deploy" command
target,
...defaultArgs,
],
{
cwd: projectDir,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.regex(stdout, /https:\/\/output-.+\.vercel\.app/);
});
test('default command should warn when deploying with conflicting subdirectory', async t => {
const projectDir = fixture('deploy-default-with-conflicting-sub-directory');
const target = 'list'; // command that conflicts with a sub directory
await vcLink(t, projectDir);
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
// omit the default "deploy" command
target,
...defaultArgs,
],
{
cwd: projectDir,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.regex(
stderr || '',
/Did you mean to deploy the subdirectory "list"\? Use `vc --cwd list` instead./
);
const listHeader = /No deployments found/;
t.regex(stderr || '', listHeader); // ensure `list` command still ran
});
test('deploy command should not warn when deploying with conflicting subdirectory and using --cwd', async t => {
const projectDir = fixture('deploy-default-with-conflicting-sub-directory');
const target = 'list'; // command that conflicts with a sub directory
await vcLink(t, path.join(projectDir, target));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['list', '--cwd', target, ...defaultArgs],
{
cwd: projectDir,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.notRegex(
stderr || '',
/Did you mean to deploy the subdirectory "list"\? Use `vc --cwd list` instead./
);
const listHeader = /No deployments found/;
t.regex(stderr || '', listHeader); // ensure `list` command still ran
});
test('default command should work with --cwd option', async t => {
const projectDir = fixture('deploy-default-with-conflicting-sub-directory');
const target = 'list'; // command that conflicts with a sub directory
await vcLink(t, path.join(projectDir, 'list'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
// omit the default "deploy" command
'--cwd',
target,
...defaultArgs,
],
{
cwd: projectDir,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const url = stdout;
const deploymentResult = await fetch(`${url}/README.md`);
const body = await deploymentResult.text();
t.deepEqual(
body,
'readme contents for deploy-default-with-conflicting-sub-directory'
);
});
test('should allow deploying a directory that was built with a target environment of "preview" and `--prebuilt` is used without specifying a target', async t => {
const projectDir = fixture('deploy-default-with-prebuilt-preview');
await vcLink(t, projectDir);
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
// omit the default "deploy" command
'--prebuilt',
...defaultArgs,
],
{
cwd: projectDir,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const url = stdout;
const deploymentResult = await fetch(`${url}/README.md`);
const body = await deploymentResult.text();
t.deepEqual(body, 'readme contents for deploy-default-with-prebuilt-preview');
});
test('should allow deploying a directory that was prebuilt, but has no builds.json', async t => {
const projectDir = fixture('build-output-api-raw');
await vcLink(t, projectDir);
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
// omit the default "deploy" command
'--prebuilt',
...defaultArgs,
],
{
cwd: projectDir,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const url = stdout;
const deploymentResult = await fetch(`${url}/README.md`);
const body = await deploymentResult.text();
t.deepEqual(body, 'readme contents for build-output-api-raw');
});
test('[vc link] with vercel.json configuration overrides should create a valid deployment', async t => {
const directory = fixture('vercel-json-configuration-overrides-link');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--yes', ...defaultArgs],
{
reject: false,
cwd: directory,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const link = require(path.join(directory, '.vercel/project.json'));
const resEnv = await apiFetch(`/v4/projects/${link.projectId}`);
t.is(resEnv.status, 200);
const json = await resEnv.json();
t.is(json.buildCommand, 'mkdir public && echo "1" > public/index.txt');
});
test('deploy using only now.json with `redirects` defined', async t => {
const target = fixture('redirects-v2');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[target, ...defaultArgs, '--yes'],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const url = stdout;
const res = await fetch(`${url}/foo/bar`, { redirect: 'manual' });
const location = res.headers.get('location');
t.is(location, 'https://example.com/foo/bar');
});
test('deploy using --local-config flag v2', async t => {
const target = fixture('local-config-v2');
const configPath = path.join(target, 'now-test.json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['deploy', target, '--local-config', configPath, ...defaultArgs, '--yes'],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
t.regex(host, /secondary/gm, `Expected "secondary" but received "${host}"`);
const testRes = await fetch(`https://${host}/test-${contextName}.html`);
const testText = await testRes.text();
t.is(testText, '<h1>hello test</h1>');
const anotherTestRes = await fetch(`https://${host}/another-test`);
const anotherTestText = await anotherTestRes.text();
t.is(anotherTestText, testText);
const mainRes = await fetch(`https://${host}/main-${contextName}.html`);
t.is(mainRes.status, 404, 'Should not deploy/build main now.json');
const anotherMainRes = await fetch(`https://${host}/another-main`);
t.is(anotherMainRes.status, 404, 'Should not deploy/build main now.json');
});
test('deploy fails using --local-config flag with non-existent path', async t => {
const target = fixture('local-config-v2');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
'deploy',
target,
'--local-config',
'does-not-exist.json',
...defaultArgs,
'--yes',
],
{
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.regex(stderr, /Error: Couldn't find a project configuration file at/);
t.regex(stderr, /does-not-exist\.json/);
});
test('deploy using --local-config flag above target', async t => {
const root = fixture('local-config-above-target');
const target = path.join(root, 'dir');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[
'deploy',
target,
'--local-config',
'./now-root.json',
...defaultArgs,
'--yes',
],
{
cwd: root,
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const testRes = await fetch(`https://${host}/index.html`);
const testText = await testRes.text();
t.is(testText, '<h1>hello index</h1>');
const anotherTestRes = await fetch(`https://${host}/another.html`);
const anotherTestText = await anotherTestRes.text();
t.is(anotherTestText, '<h1>hello another</h1>');
t.regex(host, /root-level/gm, `Expected "root-level" but received "${host}"`);
});
test('Deploy `api-env` fixture and test `vercel env` command', async t => {
const target = fixture('api-env');
async function vcLink() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--yes', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
console.log({ stdout });
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvLsIsEmpty() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'ls', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /No Environment Variables found in Project/gm);
}
async function vcEnvAddWithPrompts() {
const vc = execa(binaryPath, ['env', 'add', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(vc, chunk =>
chunk.includes('Whats the name of the variable?')
);
vc.stdin.write('MY_NEW_ENV_VAR\n');
await waitForPrompt(
vc,
chunk =>
chunk.includes('Whats the value of') &&
chunk.includes('MY_NEW_ENV_VAR')
);
vc.stdin.write('my plaintext value\n');
await waitForPrompt(
vc,
chunk =>
chunk.includes('which Environments') && chunk.includes('MY_NEW_ENV_VAR')
);
vc.stdin.write('a\n'); // select all
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvAddFromStdin() {
const vc = execa(
binaryPath,
['env', 'add', 'MY_STDIN_VAR', 'development', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
vc.stdin.end('{"expect":"quotes"}');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvAddFromStdinPreview() {
const vc = execa(
binaryPath,
['env', 'add', 'MY_PREVIEW', 'preview', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
vc.stdin.end('preview-no-branch');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvAddFromStdinPreviewWithBranch() {
const vc = execa(
binaryPath,
['env', 'add', 'MY_PREVIEW', 'preview', 'staging', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
vc.stdin.end('preview-with-branch');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.regex(stderr, /does not have a connected Git repository/gm);
}
async function vcEnvLsIncludesVar() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'ls', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Environment Variables found in Project/gm);
console.log(stdout);
const lines = stdout.split('\n');
const plaintextEnvs = lines.filter(line => line.includes('MY_NEW_ENV_VAR'));
t.is(plaintextEnvs.length, 1);
t.regex(plaintextEnvs[0], /Production, Preview, Development/gm);
const stdinEnvs = lines.filter(line => line.includes('MY_STDIN_VAR'));
t.is(stdinEnvs.length, 1);
t.regex(stdinEnvs[0], /Development/gm);
const previewEnvs = lines.filter(line => line.includes('MY_PREVIEW'));
t.is(previewEnvs.length, 1);
t.regex(previewEnvs[0], /Encrypted .* Preview /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 vcEnvPull() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'pull', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Created .env file/gm);
const contents = fs.readFileSync(path.join(target, '.env'), 'utf8');
t.regex(contents, /^# Created by Vercel CLI\n/);
t.regex(contents, /MY_NEW_ENV_VAR="my plaintext value"/);
t.regex(contents, /MY_STDIN_VAR="{"expect":"quotes"}"/);
t.regex(contents, /MY_DECRYPTABLE_SECRET_ENV="decryptable value"/);
t.notRegex(contents, /MY_PREVIEW/);
}
async function vcEnvPullOverwrite() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'pull', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Overwriting existing .env file/gm);
t.regex(stderr, /Updated .env file/gm);
}
async function vcEnvPullConfirm() {
fs.writeFileSync(path.join(target, '.env'), 'hahaha');
const vc = execa(binaryPath, ['env', 'pull', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(vc, chunk =>
chunk.includes('Found existing file ".env". Do you want to overwrite?')
);
vc.stdin.end('y\n');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcDeployWithVar() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const apiUrl = `https://${host}/api/get-env`;
console.log({ apiUrl });
const apiRes = await fetch(apiUrl);
t.is(apiRes.status, 200, formatOutput({ stderr, stdout }));
const apiJson = await apiRes.json();
t.is(apiJson['MY_NEW_ENV_VAR'], 'my plaintext value');
const homeUrl = `https://${host}`;
console.log({ homeUrl });
const homeRes = await fetch(homeUrl);
t.is(homeRes.status, 200, formatOutput({ stderr, stdout }));
const homeJson = await homeRes.json();
t.is(homeJson['MY_NEW_ENV_VAR'], 'my plaintext value');
}
async function vcDevWithEnv() {
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);
t.is(apiRes.status, 200);
const apiJson = await apiRes.json();
t.is(apiJson['MY_NEW_ENV_VAR'], 'my plaintext value');
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_NEW_ENV_VAR'], 'my plaintext value');
t.is(homeJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcDevAndFetchCloudVars() {
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);
t.is(apiRes.status, 200);
const apiJson = await apiRes.json();
t.is(apiJson['MY_NEW_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_NEW_ENV_VAR'], 'my plaintext value');
t.is(homeJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
t.is(homeJson['MY_DECRYPTABLE_SECRET_ENV'], 'decryptable value');
// system env vars are automatically exposed
t.is(apiJson['VERCEL'], '1');
t.is(homeJson['VERCEL'], '1');
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 vcEnvPullFetchSystemVars() {
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"'), 'VERCEL');
t.true(lines.has('VERCEL_URL=""'), 'VERCEL_URL');
t.true(lines.has('VERCEL_ENV="development"'), 'VERCEL_ENV');
t.true(lines.has('VERCEL_GIT_PROVIDER=""'), 'VERCEL_GIT_PROVIDER');
t.true(lines.has('VERCEL_GIT_REPO_SLUG=""'), 'VERCEL_GIT_REPO_SLUG');
}
async function vcDevAndFetchSystemVars() {
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 });
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvRemove() {
const vc = execa(binaryPath, ['env', 'rm', '-y', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(vc, chunk =>
chunk.includes('Whats the name of the variable?')
);
vc.stdin.write('MY_PREVIEW\n');
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function vcEnvRemoveWithArgs() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'rm', 'MY_STDIN_VAR', 'development', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
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 vcEnvRemoveWithNameOnly() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'rm', 'MY_NEW_ENV_VAR', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
function vcEnvRemoveByName(name) {
return execa(binaryPath, ['env', 'rm', name, '-y', ...defaultArgs], {
reject: false,
cwd: target,
});
}
async function vcEnvRemoveAll() {
await vcEnvRemoveByName('MY_PREVIEW');
await vcEnvRemoveByName('MY_STDIN_VAR');
await vcEnvRemoveByName('MY_DECRYPTABLE_SECRET_ENV');
await vcEnvRemoveByName('MY_NEW_ENV_VAR');
}
try {
await vcEnvRemoveAll();
await vcLink();
await vcEnvLsIsEmpty();
await vcEnvAddWithPrompts();
await vcEnvAddFromStdin();
await vcEnvAddFromStdinPreview();
await vcEnvAddFromStdinPreviewWithBranch();
await vcEnvLsIncludesVar();
await createEnvWithDecryptableSecret();
await vcEnvPull();
await vcEnvPullOverwrite();
await vcEnvPullConfirm();
await vcDeployWithVar();
await vcDevWithEnv();
fs.unlinkSync(path.join(target, '.env'));
await vcDevAndFetchCloudVars();
await enableAutoExposeSystemEnvs();
await vcEnvPullFetchSystemVars();
fs.unlinkSync(path.join(target, '.env'));
await vcDevAndFetchSystemVars();
await vcEnvRemove();
await vcEnvRemoveWithArgs();
await vcEnvRemoveWithNameOnly();
await vcEnvLsIsEmpty();
} finally {
await vcEnvRemoveAll();
}
});
test('[vc projects] should create a project successfully', async t => {
const projectName = `vc-projects-add-${
Math.random().toString(36).split('.')[1]
}`;
const vc = execa(binaryPath, ['project', 'add', projectName, ...defaultArgs]);
await waitForPrompt(vc, chunk =>
chunk.includes(`Success! Project ${projectName} added`)
);
const { exitCode, stderr, stdout } = await vc;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// creating the same project again should succeed
const vc2 = execa(binaryPath, [
'project',
'add',
projectName,
...defaultArgs,
]);
await waitForPrompt(vc2, chunk =>
chunk.includes(`Success! Project ${projectName} added`)
);
const { exitCode: exitCode2, stderr: stderr2, stdout: stdout2 } = await vc;
t.is(exitCode2, 0, formatOutput({ stderr2, stdout2 }));
});
test('deploy with metadata containing "=" in the value', async t => {
const target = fixture('static-v2-meta');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[target, ...defaultArgs, '--yes', '--meta', 'someKey=='],
{ reject: false }
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const res = await fetch(
`https://api.vercel.com/v12/now/deployments/get?url=${host}`,
{ headers: { authorization: `Bearer ${token}` } }
);
const deployment = await res.json();
t.is(deployment.meta.someKey, '=');
});
test('print the deploy help message', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['help', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 2);
t.true(stderr.includes(deployHelpMessage), `Received:\n${stderr}\n${stdout}`);
t.false(
stderr.includes('ExperimentalWarning'),
`Received:\n${stderr}\n${stdout}`
);
});
test('output the version', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['--version', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const version = stdout.trim();
t.is(exitCode, 0);
t.truthy(semVer.valid(version));
t.is(version, pkg.version);
});
test('should add secret with hyphen prefix', async t => {
const target = fixture('build-secret');
const key = 'mysecret';
const value = '-foo_bar';
let secretCall = await execa(
binaryPath,
['secrets', 'add', ...defaultArgs, key, value],
{
cwd: target,
reject: false,
}
);
t.is(
secretCall.exitCode,
0,
formatOutput({ stderr: secretCall.stderr, stdout: secretCall.stdout })
);
let targetCall = await execa(binaryPath, [...defaultArgs, '--yes'], {
cwd: target,
reject: false,
});
t.is(
targetCall.exitCode,
0,
formatOutput({ stderr: targetCall.stderr, stdout: targetCall.stdout })
);
const { host } = new URL(targetCall.stdout);
const response = await fetch(`https://${host}`);
t.is(
response.status,
200,
formatOutput({ stderr: targetCall.stderr, stdout: targetCall.stdout })
);
t.is(
await response.text(),
`${value}\n`,
formatOutput({ stderr: targetCall.stderr, stdout: targetCall.stdout })
);
});
test('login with unregistered user', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['login', `${session}@${session}.com`, ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const goal = `Error: Please sign up: https://vercel.com/signup`;
const lines = stderr.trim().split('\n');
const last = lines[lines.length - 1];
t.is(exitCode, 1);
t.true(last.includes(goal));
});
test('ignore files specified in .nowignore', async t => {
const directory = fixture('nowignore');
const args = [
'--debug',
'--public',
'--name',
session,
...defaultArgs,
'--yes',
];
const targetCall = await execa(binaryPath, args, {
cwd: directory,
reject: false,
});
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
const { host } = new URL(targetCall.stdout);
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
t.is(ignoredFile.status, 404);
const presentFile = await fetch(`https://${host}/index.txt`);
t.is(presentFile.status, 200);
});
test('ignore files specified in .nowignore via allowlist', async t => {
const directory = fixture('nowignore-allowlist');
const args = [
'--debug',
'--public',
'--name',
session,
...defaultArgs,
'--yes',
];
const targetCall = await execa(binaryPath, args, {
cwd: directory,
reject: false,
});
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
const { host } = new URL(targetCall.stdout);
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
t.is(ignoredFile.status, 404);
const presentFile = await fetch(`https://${host}/index.txt`);
t.is(presentFile.status, 200);
});
test('list the scopes', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['teams', 'ls', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
const include = new RegExp(`${contextName}\\s+${email}`);
t.true(
include.test(stdout),
`Expected: ${include}\n\nReceived instead:\n${stdout}\n${stderr}`
);
});
test('domains inspect', async t => {
const domainName = `inspect-${contextName}-${Math.random()
.toString()
.slice(2, 8)}.org`;
const directory = fixture('static-multiple-files');
const projectName = Math.random().toString().slice(2);
const output = await execute([
directory,
`-V`,
`2`,
`--name=${projectName}`,
'--yes',
'--public',
]);
t.is(output.exitCode, 0, formatOutput(output));
{
// Add a domain that can be inspected
const result = await execa(
binaryPath,
[`domains`, `add`, domainName, projectName, ...defaultArgs],
{ reject: false }
);
t.is(result.exitCode, 0, formatOutput(result));
}
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['domains', 'inspect', domainName, ...defaultArgs],
{
reject: false,
}
);
t.true(stderr.includes(`Renewal Price`));
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
{
// Remove the domain again
const result = await execa(
binaryPath,
[`domains`, `rm`, domainName, ...defaultArgs],
{ reject: false, input: 'y' }
);
t.is(result.exitCode, 0, formatOutput(result));
}
});
test('try to purchase a domain', async t => {
if (process.env.VERCEL_TOKEN || process.env.NOW_TOKEN) {
console.log(
'Skipping test `try to purchase a domain` because a personal VERCEL_TOKEN was provided.'
);
t.pass();
return;
}
const stream = new Readable();
stream._read = () => {};
setTimeout(async () => {
await sleep(ms('1s'));
stream.push('y');
await sleep(ms('1s'));
stream.push('y');
stream.push(null);
}, ms('1s'));
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['domains', 'buy', `${session}-test.com`, ...defaultArgs],
{
reject: false,
input: stream,
env: {
FORCE_TTY: '1',
},
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.regex(
stderr,
/Error: Could not purchase domain\. Please add a payment method using/
);
});
test('try to transfer-in a domain with "--code" option', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
'domains',
'transfer-in',
'--code',
'xyz',
`${session}-test.com`,
...defaultArgs,
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(
stderr.includes(
`Error: The domain "${session}-test.com" is not transferable.`
)
);
t.is(exitCode, 1);
});
test('try to move an invalid domain', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
'domains',
'move',
`${session}-invalid-test.org`,
`${session}-invalid-user`,
...defaultArgs,
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(stderr.includes(`Error: Domain not found under `));
t.is(exitCode, 1);
});
/*
* Disabled 2 tests because these temp users don't have certs
test('create wildcard alias for deployment', async t => {
const hosts = {
deployment: context.deployment,
alias: `*.${contextName}.now.sh`,
};
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['alias', hosts.deployment, hosts.alias, ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const goal = `> Success! ${hosts.alias} now points to https://${hosts.deployment}`;
t.is(exitCode, 0);
t.true(stdout.startsWith(goal));
// Send a test request to the alias
// Retries to make sure we consider the time it takes to update
const response = await retry(
async () => {
const response = await fetch(`https://test.${contextName}.now.sh`);
if (response.ok) {
return response;
}
throw new Error(`Error: Returned code ${response.status}`);
},
{ retries: 3 }
);
const content = await response.text();
t.true(response.ok);
t.true(content.includes(contextName));
context.wildcardAlias = hosts.alias;
});
test('remove the wildcard alias', async t => {
const goal = `> Success! Alias ${context.wildcardAlias} removed`;
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['alias', 'rm', context.wildcardAlias, '--yes', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.true(stdout.startsWith(goal));
});
*/
test('ensure we render a warning for deployments with no files', async t => {
const directory = fixture('empty-directory');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--yes',
'--force',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the warning is printed
t.regex(stderr, /There are no files inside your deployment/);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
if (host) {
context.deployment = host;
}
// Ensure the exit code is right
t.is(exitCode, 0);
// Send a test request to the deployment
const res = await fetch(href);
t.is(res.status, 404);
});
test('output logs with "short" output', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['logs', context.deployment, ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(
stderr.includes(`Fetched deployment "${context.deployment}"`),
formatOutput({ stderr, stdout })
);
// "short" format includes timestamps
t.truthy(
stdout.match(
/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/
),
formatOutput({ stderr, stdout })
);
t.is(exitCode, 0);
});
test('output logs with "raw" output', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['logs', context.deployment, ...defaultArgs, '--output', 'raw'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.true(
stderr.includes(`Fetched deployment "${context.deployment}"`),
formatOutput({ stderr, stdout })
);
// "raw" format does not include timestamps
t.is(
null,
stdout.match(
/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/
)
);
t.is(exitCode, 0);
});
test('ensure we render a prompt when deploying home directory', async t => {
const directory = homedir();
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[directory, '--public', '--name', session, ...defaultArgs, '--force'],
{
reject: false,
input: 'N',
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
t.true(
stderr.includes(
'You are deploying your home directory. Do you want to continue? [y/N]'
)
);
t.true(stderr.includes('Canceled'));
});
test('ensure the `scope` property works with email', async t => {
const directory = fixture('config-scope-property-email');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--force',
'--yes',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure we're deploying under the right scope
t.true(stderr.includes(session));
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test('ensure the `scope` property works with username', async t => {
const directory = fixture('config-scope-property-username');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--force',
'--yes',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure we're deploying under the right scope
t.true(stderr.includes(contextName));
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test('try to create a builds deployments with wrong now.json', async t => {
const directory = fixture('builds-wrong');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[directory, '--public', ...defaultArgs, '--yes'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 1);
t.true(
stderr.includes(
'Error: Invalid now.json - should NOT have additional property `builder`. Did you mean `builds`?'
)
);
t.true(
stderr.includes(
'https://vercel.com/docs/concepts/projects/project-configuration'
)
);
});
test('try to create a builds deployments with wrong vercel.json', async t => {
const directory = fixture('builds-wrong-vercel');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[directory, '--public', ...defaultArgs, '--yes'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(
stderr.includes(
'Error: Invalid vercel.json - should NOT have additional property `fake`. Please remove it.'
)
);
t.true(
stderr.includes(
'https://vercel.com/docs/concepts/projects/project-configuration'
)
);
});
test('try to create a builds deployments with wrong `build.env` property', async t => {
const directory = fixture('builds-wrong-build-env');
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['--public', ...defaultArgs, '--yes'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(
stderr.includes(
'Error: Invalid vercel.json - should NOT have additional property `build.env`. Did you mean `{ "build": { "env": {"name": "value"} } }`?'
),
formatOutput({ stdout, stderr })
);
t.true(
stderr.includes(
'https://vercel.com/docs/concepts/projects/project-configuration'
),
formatOutput({ stdout, stderr })
);
});
test('create a builds deployments with no actual builds', async t => {
const directory = fixture('builds-no-list');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'--force',
'--yes',
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { host } = new URL(stdout);
t.is(host.split('-')[0], session);
});
test('create a staging deployment', async t => {
const directory = fixture('static-deployment');
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
const targetCall = await execa(binaryPath, [
directory,
'--target=staging',
...args,
'--yes',
]);
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
t.regex(
targetCall.stderr,
/Setting target to staging/gm,
formatOutput(targetCall)
);
t.regex(targetCall.stdout, /https:\/\//gm);
t.is(targetCall.exitCode, 0, formatOutput(targetCall));
const { host } = new URL(targetCall.stdout);
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(deployment.target, 'staging', JSON.stringify(deployment, null, 2));
});
test('create a production deployment', async t => {
const directory = fixture('static-deployment');
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
const targetCall = await execa(binaryPath, [
directory,
'--target=production',
...args,
'--yes',
]);
console.log(targetCall.stderr);
console.log(targetCall.stdout);
console.log(targetCall.exitCode);
t.is(targetCall.exitCode, 0, formatOutput(targetCall));
t.regex(
targetCall.stderr,
/`--prod` option instead/gm,
formatOutput(targetCall)
);
t.regex(
targetCall.stderr,
/Setting target to production/gm,
formatOutput(targetCall)
);
t.regex(
targetCall.stderr,
/Inspect: https:\/\/vercel.com\//gm,
formatOutput(targetCall)
);
t.regex(targetCall.stdout, /https:\/\//gm);
const { host: targetHost } = new URL(targetCall.stdout);
const targetDeployment = await apiFetch(
`/v10/now/deployments/unknown?url=${targetHost}`
).then(resp => resp.json());
t.is(
targetDeployment.target,
'production',
JSON.stringify(targetDeployment, null, 2)
);
const call = await execa(binaryPath, [directory, '--prod', ...args]);
console.log(call.stderr);
console.log(call.stdout);
console.log(call.exitCode);
t.is(call.exitCode, 0, formatOutput(call));
t.regex(
call.stderr,
/Setting target to production/gm,
formatOutput(targetCall)
);
t.regex(call.stdout, /https:\/\//gm);
const { host } = new URL(call.stdout);
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(deployment.target, 'production', JSON.stringify(deployment, null, 2));
});
test('use build-env', async t => {
const directory = fixture('build-env');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[directory, '--public', ...defaultArgs, '--yes'],
{
reject: false,
}
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Test if the output is really a URL
const deploymentUrl = pickUrl(stdout);
const { href } = new URL(deploymentUrl);
await waitForDeployment(href);
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), 'bar');
});
test('try to deploy non-existing path', async t => {
const goal = `Error: The specified file or directory "${session}" does not exist.`;
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[session, ...defaultArgs, '--yes'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(stderr.trim().endsWith(goal));
});
test('try to deploy with non-existing team', async t => {
const target = fixture('static-deployment');
const goal = `Error: The specified scope does not exist`;
const { stderr, stdout, exitCode } = await execa(
binaryPath,
[target, '--scope', session, ...defaultArgs, '--yes'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1);
t.true(stderr.includes(goal));
});
const verifyExampleAngular = (cwd, dir) =>
fs.existsSync(path.join(cwd, dir, 'package.json')) &&
fs.existsSync(path.join(cwd, dir, 'tsconfig.json')) &&
fs.existsSync(path.join(cwd, dir, 'angular.json'));
test('initialize example "angular"', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "angular" example in';
const { stdout, stderr, exitCode } = await execute(['init', 'angular'], {
cwd,
});
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(
verifyExampleAngular(cwd, 'angular'),
formatOutput({ stdout, stderr })
);
});
test('initialize example ("angular") to specified directory', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "angular" example in';
const { stdout, stderr, exitCode } = await execute(
['init', 'angular', 'ang'],
{
cwd,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(verifyExampleAngular(cwd, 'ang'), formatOutput({ stdout, stderr }));
});
test('initialize example to existing directory with "-f"', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal = '> Success! Initialized "angular" example in';
await ensureDir(path.join(cwd, 'angular'));
createFile(path.join(cwd, 'angular', '.gitignore'));
const { stdout, stderr, exitCode } = await execute(
['init', 'angular', '-f'],
{
cwd,
}
);
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
t.true(
verifyExampleAngular(cwd, 'angular'),
formatOutput({ stdout, stderr })
);
});
test('try to initialize example to existing directory', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal =
'Error: Destination path "angular" already exists and is not an empty directory. You may use `--force` or `-f` to override it.';
await ensureDir(path.join(cwd, 'angular'));
createFile(path.join(cwd, 'angular', '.gitignore'));
const { stdout, stderr, exitCode } = await execute(['init', 'angular'], {
cwd,
input: '\n',
});
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
});
test('try to initialize misspelled example (noce) in non-tty', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal =
'Error: No example found for noce, run `vercel init` to see the list of available examples.';
const { stdout, stderr, exitCode } = await execute(['init', 'noce'], { cwd });
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
});
test('try to initialize example "example-404"', async t => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
const cwd = tmpDir.name;
const goal =
'Error: No example found for example-404, run `vercel init` to see the list of available examples.';
const { stdout, stderr, exitCode } = await execute(['init', 'example-404'], {
cwd,
});
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
t.true(stderr.includes(goal), formatOutput({ stdout, stderr }));
});
test('try to revert a deployment and assign the automatic aliases', async t => {
const firstDeployment = fixture('now-revert-alias-1');
const secondDeployment = fixture('now-revert-alias-2');
const { name } = JSON.parse(
fs.readFileSync(path.join(firstDeployment, 'now.json'))
);
t.true(!!name, 'name has a value');
const url = `https://${name}.user.vercel.app`;
{
const {
stdout: deploymentUrl,
stderr,
exitCode,
} = await execute([firstDeployment, '--yes']);
t.is(exitCode, 0, formatOutput({ stderr, stdout: deploymentUrl }));
await waitForDeployment(deploymentUrl);
await sleep(20000);
const result = await fetch(url).then(r => r.json());
t.is(
result.name,
'now-revert-alias-1',
`[First run] Received ${result.name} instead on ${url} (${deploymentUrl})`
);
}
{
const {
stdout: deploymentUrl,
stderr,
exitCode,
} = await execute([secondDeployment, '--yes']);
t.is(exitCode, 0, formatOutput({ stderr, stdout: deploymentUrl }));
await waitForDeployment(deploymentUrl);
await sleep(20000);
await fetch(url);
await sleep(5000);
const result = await fetch(url).then(r => r.json());
t.is(
result.name,
'now-revert-alias-2',
`[Second run] Received ${result.name} instead on ${url} (${deploymentUrl})`
);
}
{
const {
stdout: deploymentUrl,
stderr,
exitCode,
} = await execute([firstDeployment, '--yes']);
t.is(exitCode, 0, formatOutput({ stderr, stdout: deploymentUrl }));
await waitForDeployment(deploymentUrl);
await sleep(20000);
await fetch(url);
await sleep(5000);
const result = await fetch(url).then(r => r.json());
t.is(
result.name,
'now-revert-alias-1',
`[Third run] Received ${result.name} instead on ${url} (${deploymentUrl})`
);
}
});
test('whoami', async t => {
const { exitCode, stdout, stderr } = await execute(['whoami']);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
t.is(exitCode, 0);
t.is(stdout, contextName, formatOutput({ stdout, stderr }));
});
test('[vercel dev] fails when dev script calls vercel dev recursively', async t => {
const deploymentPath = fixture('now-dev-fail-dev-script');
const { exitCode, stderr } = await execute(['dev', deploymentPath]);
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel dev] fails when development commad calls vercel dev recursively', async t => {
const dir = fixture('dev-fail-on-recursion-command');
const projectName = `dev-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const dev = execa(binaryPath, ['dev', ...defaultArgs], {
cwd: dir,
reject: false,
env: {
FORCE_TTY: '1',
},
});
await setupProject(dev, projectName, {
devCommand: `${binaryPath} dev`,
});
const { exitCode, stderr } = await dev;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('`vercel rm` removes a deployment', async t => {
const directory = fixture('static-deployment');
const { stdout } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'-V',
2,
'--force',
'--yes',
],
{
reject: false,
}
);
const { host } = new URL(stdout);
const { exitCode, stdout: stdoutRemove } = await execute([
'rm',
host,
'--yes',
]);
t.truthy(stdoutRemove.includes(host));
t.is(exitCode, 0);
});
test('`vercel rm` should fail with unexpected option', async t => {
const output = await execute(['rm', 'example.example.com', '--fake']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Error: unknown or unexpected option: --fake/gm,
formatOutput(output)
);
});
test('`vercel rm` 404 exits quickly', async t => {
const start = Date.now();
const { exitCode, stderr, stdout } = await execute([
'rm',
'this.is.a.deployment.that.does.not.exist.example.com',
]);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
const delta = Date.now() - start;
// "does not exist" case is exit code 1, similar to Unix `rm`
t.is(exitCode, 1);
t.truthy(
stderr.includes(
'Could not find any deployments or projects matching "this.is.a.deployment.that.does.not.exist.example.com"'
)
);
// "quickly" meaning < 5 seconds, because it used to hang from a previous bug
t.truthy(delta < 5000);
});
test('render build errors', async t => {
const deploymentPath = fixture('failing-build');
const output = await execute([deploymentPath, '--yes']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Command "yarn run build" exited with 1/gm,
formatOutput(output)
);
});
test('invalid deployment, projects and alias names', async t => {
const check = async (...args) => {
const output = await execute(args);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
const print = `\`${args.join(' ')}\`\n${formatOutput(output)}`;
t.is(output.exitCode, 1, print);
t.regex(output.stderr, /The provided argument/gm, print);
};
await Promise.all([
check('alias', '/', 'test'),
check('alias', 'test', '/'),
check('rm', '/'),
check('ls', '/'),
]);
});
test('vercel certs ls', async t => {
const output = await execute(['certs', 'ls']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /certificates? found under/gm, formatOutput(output));
});
test('vercel certs ls --next=123456', async t => {
const output = await execute(['certs', 'ls', '--next=123456']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /No certificates found under/gm, formatOutput(output));
});
test('vercel hasOwnProperty not a valid subcommand', async t => {
const output = await execute(['hasOwnProperty']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/The specified file or directory "hasOwnProperty" does not exist/gm,
formatOutput(output)
);
});
test('create zero-config deployment', async t => {
const fixturePath = fixture('zero-config-next-js');
const output = await execute([fixturePath, '--force', '--public', '--yes']);
console.log('isCanary', isCanary);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
const { host } = new URL(output.stdout);
const response = await apiFetch(`/v10/now/deployments/unkown?url=${host}`);
const text = await response.text();
t.is(response.status, 200, text);
const data = JSON.parse(text);
t.is(data.error, undefined, JSON.stringify(data, null, 2));
const validBuilders = data.builds.every(build =>
isCanary ? build.use.endsWith('@canary') : !build.use.endsWith('@canary')
);
t.true(
validBuilders,
'Builders are not valid: ' + JSON.stringify(data, null, 2)
);
});
test('next unsupported functions config shows warning link', async t => {
const fixturePath = fixture('zero-config-next-js-functions-warning');
const output = await execute([fixturePath, '--force', '--public', '--yes']);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(
output.stderr,
/Ignoring function property `runtime`\. When using Next\.js, only `memory`, `maxDuration`, and `cron` can be used\./gm,
formatOutput(output)
);
t.regex(
output.stderr,
/Learn More: https:\/\/vercel\.link\/functions-property-next/gm,
formatOutput(output)
);
});
test('vercel secret add', async t => {
context.secretName = `my-secret-${Date.now().toString(36)}`;
const value = 'https://my-secret-endpoint.com';
const output = await execute(['secret', 'add', context.secretName, value]);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
});
test('vercel secret ls', async t => {
const output = await execute(['secret', 'ls']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stdout, /Secrets found under/gm, formatOutput(output));
t.regex(output.stdout, new RegExp(), formatOutput(output));
});
test('vercel secret ls --test-warning', async t => {
const output = await execute(['secret', 'ls', '--test-warning']);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /Test warning message./gm, formatOutput(output));
t.regex(
output.stderr,
/Learn more: https:\/\/vercel.com/gm,
formatOutput(output)
);
t.regex(output.stdout, /No secrets found under/gm, formatOutput(output));
});
test('vercel secret rename', async t => {
const nextName = `renamed-secret-${Date.now().toString(36)}`;
const output = await execute([
'secret',
'rename',
context.secretName,
nextName,
]);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
context.secretName = nextName;
});
test('vercel secret rm', async t => {
const output = await execute(['secret', 'rm', context.secretName, '-y']);
console.log(output.stderr);
console.log(output.stdout);
console.log(output.exitCode);
t.is(output.exitCode, 0, formatOutput(output));
});
test('deploy a Lambda with 128MB of memory', async t => {
const directory = fixture('lambda-with-128-memory');
const output = await execute([directory, '--yes']);
t.is(output.exitCode, 0, formatOutput(output));
const { host: url } = new URL(output.stdout);
const response = await fetch('https://' + url + '/api/memory');
t.is(response.status, 200, url);
// It won't be exactly 128MB,
// so we just compare if it is lower than 450MB
const { memory } = await response.json();
t.is(memory, 128, `Lambda has ${memory} bytes of memory`);
});
test('fail to deploy a Lambda with an incorrect value for of memory', async t => {
const directory = fixture('lambda-with-123-memory');
const output = await execute([directory, '--yes']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Serverless Functions.+memory/gm,
formatOutput(output)
);
t.regex(output.stderr, /Learn More/gm, formatOutput(output));
});
test('deploy a Lambda with 3 seconds of maxDuration', async t => {
const directory = fixture('lambda-with-3-second-timeout');
const output = await execute([directory, '--yes']);
t.is(output.exitCode, 0, formatOutput(output));
const url = new URL(output.stdout);
// Should time out
url.pathname = '/api/wait-for/5';
const response1 = await fetch(url.href);
t.is(
response1.status,
504,
`Expected 504 status, got ${response1.status}: ${url}`
);
// Should not time out
url.pathname = '/api/wait-for/1';
const response2 = await fetch(url.href);
t.is(
response2.status,
200,
`Expected 200 status, got ${response1.status}: ${url}`
);
});
test('fail to deploy a Lambda with an incorrect value for maxDuration', async t => {
const directory = fixture('lambda-with-1000-second-timeout');
const output = await execute([directory, '--yes']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/maxDuration must be between 1 second and 10 seconds/gm,
formatOutput(output)
);
});
test('invalid `--token`', async t => {
const output = await execute(['--token', 'he\nl,o.']);
t.is(output.exitCode, 1, formatOutput(output));
t.true(
output.stderr.includes(
'Error: You defined "--token", but its contents are invalid. Must not contain: "\\n", ",", "."'
)
);
});
test('deploy a Lambda with a specific runtime', async t => {
const directory = fixture('lambda-with-php-runtime');
const output = await execute([directory, '--public', '--yes']);
t.is(output.exitCode, 0, formatOutput(output));
const url = new URL(output.stdout);
const res = await fetch(`${url}/api/test`);
const text = await res.text();
t.is(text, 'Hello from PHP');
});
test('fail to deploy a Lambda with a specific runtime but without a locked version', async t => {
const directory = fixture('lambda-with-invalid-runtime');
const output = await execute([directory, '--yes']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Function Runtimes must have a valid version/gim,
formatOutput(output)
);
});
test('fail to add a domain without a project', async t => {
const output = await execute(['domains', 'add', 'my-domain.vercel.app']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(output.stderr, /expects two arguments/gm, formatOutput(output));
});
test('change user', async t => {
t.timeout(ms('1m'));
const { stdout: prevUser } = await execute(['whoami']);
// Delete the current token
await execute(['logout', '--debug'], { stdio: 'inherit' });
await createUser();
await execute(['login', email, '--api', loginApiUrl, '--debug'], {
stdio: 'inherit',
env: {
FORCE_TTY: '1',
},
});
const auth = await fs.readJSON(getConfigAuthPath());
t.is(auth.token, token);
const { stdout: nextUser } = await execute(['whoami']);
console.log('prev user', prevUser);
console.log('next user', nextUser);
t.is(typeof prevUser, 'string', prevUser);
t.is(typeof nextUser, 'string', nextUser);
t.not(prevUser, nextUser, JSON.stringify({ prevUser, nextUser }));
});
test('assign a domain to a project', async t => {
const domain = `project-domain.${contextName}.vercel.app`;
const directory = fixture('static-deployment');
const deploymentOutput = await execute([directory, '--public', '--yes']);
t.is(deploymentOutput.exitCode, 0, formatOutput(deploymentOutput));
const host = deploymentOutput.stdout.trim().replace('https://', '');
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(typeof deployment.name, 'string', JSON.stringify(deployment, null, 2));
const project = deployment.name;
const output = await execute(['domains', 'add', domain, project, '--force']);
t.is(output.exitCode, 0, formatOutput(output));
const removeResponse = await execute(['rm', project, '-y']);
t.is(removeResponse.exitCode, 0, formatOutput(removeResponse));
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory, '--yes']);
t.is(output.exitCode, 0, formatOutput(output));
});
test('should show prompts to set up project during first deploy', async t => {
const dir = fixture('project-link-deploy');
const projectName = `project-link-deploy-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const now = execa(binaryPath, [dir, ...defaultArgs]);
await setupProject(now, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await now;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
// Send a test request to the deployment
const response = await fetch(new URL(output.stdout));
const text = await response.text();
t.is(text.includes('<h1>custom hello</h1>'), true, text);
// Ensure that `vc dev` also uses the configured build command
// and output directory
let stderr = '';
const port = 58351;
const dev = execa(binaryPath, ['dev', '--listen', port, dir, ...defaultArgs]);
dev.stderr.setEncoding('utf8');
try {
dev.stdout.pipe(process.stdout);
dev.stderr.pipe(process.stderr);
await new Promise((resolve, reject) => {
dev.once('close', (code, signal) => {
reject(`"vc dev" failed with ${signal || code}`);
});
dev.stderr.on('data', data => {
stderr += data;
if (stderr.includes('Ready! Available at')) {
resolve();
}
});
});
const res2 = await fetch(`http://localhost:${port}/`);
const text2 = await res2.text();
t.is(text2.includes('<h1>custom hello</h1>'), true, text2);
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('should prefill "project name" prompt with folder name', async t => {
const projectName = `static-deployment-${
Math.random().toString(36).split('.')[1]
}`;
const src = fixture('static-deployment');
// remove previously linked project if it exists
await remove(path.join(src, '.vercel'));
const directory = path.join(src, '../', projectName);
await copy(src, directory);
const now = execa(binaryPath, [directory, ...defaultArgs], {
env: {
FORCE_TTY: '1',
},
});
await waitForPrompt(now, chunk => /Set up and deploy [^?]+\?/.test(chunk));
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes(`Whats your projects name? (${projectName})`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to modify these settings?')
);
now.stdin.write('no\n');
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
});
test('should prefill "project name" prompt with --name', async t => {
const directory = fixture('static-deployment');
const projectName = `static-deployment-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(directory, '.vercel'));
const now = execa(
binaryPath,
[directory, '--name', projectName, ...defaultArgs],
{
env: {
FORCE_TTY: '1',
},
}
);
let isDeprecated = false;
await waitForPrompt(now, chunk => {
if (chunk.includes('The "--name" option is deprecated')) {
isDeprecated = true;
}
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('yes\n');
t.is(isDeprecated, true);
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes(`Whats your projects name? (${projectName})`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to modify these settings?')
);
now.stdin.write('no\n');
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
});
test('should prefill "project name" prompt with now.json `name`', async t => {
const directory = fixture('static-deployment');
const projectName = `static-deployment-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(directory, '.vercel'));
await fs.writeFile(
path.join(directory, 'vercel.json'),
JSON.stringify({
name: projectName,
})
);
const now = execa(binaryPath, [directory, ...defaultArgs], {
env: {
FORCE_TTY: '1',
},
});
let isDeprecated = false;
now.stderr.on('data', data => {
if (
data
.toString()
.includes('The `name` property in vercel.json is deprecated')
) {
isDeprecated = true;
}
});
await waitForPrompt(now, chunk => {
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes(`Whats your projects name? (${projectName})`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to modify these settings?')
);
now.stdin.write('no\n');
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
t.is(isDeprecated, true);
// clean up
await remove(path.join(directory, 'vercel.json'));
});
test('deploy with unknown `VERCEL_PROJECT_ID` should fail', async t => {
const directory = fixture('static-deployment');
const user = await fetchTokenInformation(token);
const output = await execute([directory], {
env: {
VERCEL_ORG_ID: user.id,
VERCEL_PROJECT_ID: 'asdf',
},
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
});
test('deploy with `VERCEL_ORG_ID` but without `VERCEL_PROJECT_ID` should fail', async t => {
const directory = fixture('static-deployment');
const user = await fetchTokenInformation(token);
const output = await execute([directory], {
env: { VERCEL_ORG_ID: user.id },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(
output.stderr.includes(
'You specified `VERCEL_ORG_ID` but you forgot to specify `VERCEL_PROJECT_ID`. You need to specify both to deploy to a custom project.'
),
true,
formatOutput(output)
);
});
test('deploy with `VERCEL_PROJECT_ID` but without `VERCEL_ORG_ID` should fail', async t => {
const directory = fixture('static-deployment');
const output = await execute([directory], {
env: { VERCEL_PROJECT_ID: 'asdf' },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(
output.stderr.includes(
'You specified `VERCEL_PROJECT_ID` but you forgot to specify `VERCEL_ORG_ID`. You need to specify both to deploy to a custom project.'
),
true,
formatOutput(output)
);
});
test('deploy with `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID`', async t => {
const directory = fixture('static-deployment');
// generate `.vercel`
await execute([directory, '--yes']);
const link = require(path.join(directory, '.vercel/project.json'));
await remove(path.join(directory, '.vercel'));
const output = await execute([directory], {
env: {
VERCEL_ORG_ID: link.orgId,
VERCEL_PROJECT_ID: link.projectId,
},
});
t.is(output.exitCode, 0, formatOutput(output));
t.is(output.stdout.includes('Linked to'), false);
});
test('deploy shows notice when project in `.vercel` does not exists', async t => {
const directory = fixture('static-deployment');
// overwrite .vercel with unexisting project
await ensureDir(path.join(directory, '.vercel'));
await writeFile(
path.join(directory, '.vercel/project.json'),
JSON.stringify({
orgId: 'asdf',
projectId: 'asdf',
})
);
const now = execute([directory]);
let detectedNotice = false;
// kill after first prompt
await waitForPrompt(now, chunk => {
detectedNotice =
detectedNotice ||
chunk.includes(
'Your Project was either deleted, transferred to a new Team, or you dont have access to it anymore'
);
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('no\n');
t.is(detectedNotice, true, 'did not detect notice');
});
test('use `rootDirectory` from project when deploying', async t => {
const directory = fixture('project-root-directory');
const firstResult = await execute([directory, '--yes', '--public']);
t.is(firstResult.exitCode, 0, formatOutput(firstResult));
const { host: firstHost } = new URL(firstResult.stdout);
const response = await apiFetch(`/v12/now/deployments/get?url=${firstHost}`);
t.is(response.status, 200);
const { projectId } = await response.json();
t.is(typeof projectId, 'string', projectId);
const projectResponse = await apiFetch(`/v2/projects/${projectId}`, {
method: 'PATCH',
body: JSON.stringify({
rootDirectory: 'src',
}),
});
console.log('response', await projectResponse.text());
t.is(projectResponse.status, 200);
const secondResult = await execute([directory, '--public']);
t.is(secondResult.exitCode, 0, formatOutput(secondResult));
const pageResponse1 = await fetch(secondResult.stdout);
t.is(pageResponse1.status, 200);
t.regex(await pageResponse1.text(), /I am a website/gm);
// Ensures that the `now.json` file has been applied
const pageResponse2 = await fetch(`${secondResult.stdout}/i-do-exist`);
t.is(pageResponse2.status, 200);
t.regex(await pageResponse2.text(), /I am a website/gm);
await apiFetch(`/v2/projects/${projectId}`, {
method: 'DELETE',
});
});
test('vercel deploy with unknown `VERCEL_ORG_ID` or `VERCEL_PROJECT_ID` should error', async t => {
const output = await execute(['deploy'], {
env: { VERCEL_ORG_ID: 'asdf', VERCEL_PROJECT_ID: 'asdf' },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
});
test('vercel env with unknown `VERCEL_ORG_ID` or `VERCEL_PROJECT_ID` should error', async t => {
const output = await execute(['env'], {
env: { VERCEL_ORG_ID: 'asdf', VERCEL_PROJECT_ID: 'asdf' },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
});
test('whoami with `VERCEL_ORG_ID` should favor `--scope` and should error', async t => {
const user = await fetchTokenInformation(token);
const output = await execute(['whoami', '--scope', 'asdf'], {
env: { VERCEL_ORG_ID: user.id },
});
t.is(output.exitCode, 1, formatOutput(output));
t.is(
output.stderr.includes('The specified scope does not exist'),
true,
formatOutput(output)
);
});
test('whoami with local .vercel scope', async t => {
const directory = fixture('static-deployment');
const user = await fetchTokenInformation(token);
// create local .vercel
await ensureDir(path.join(directory, '.vercel'));
await fs.writeFile(
path.join(directory, '.vercel', 'project.json'),
JSON.stringify({ orgId: user.id, projectId: 'xxx' })
);
const output = await execute(['whoami'], {
cwd: directory,
});
t.is(output.exitCode, 0, formatOutput(output));
t.is(output.stdout.includes(contextName), true, formatOutput(output));
// clean up
await remove(path.join(directory, '.vercel'));
});
test('deploys with only now.json and README.md', async t => {
const directory = fixture('deploy-with-only-readme-now-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--yes'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const res = await fetch(`https://${host}/README.md`);
const text = await res.text();
t.regex(text, /readme contents/);
});
test('deploys with only vercel.json and README.md', async t => {
const directory = fixture('deploy-with-only-readme-vercel-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--yes'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const res = await fetch(`https://${host}/README.md`);
const text = await res.text();
t.regex(text, /readme contents/);
});
test('reject conflicting `vercel.json` and `now.json` files', async t => {
const directory = fixture('conflicting-now-json-vercel-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--yes'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.true(
stderr.includes(
'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.'
),
formatOutput({ stderr, stdout })
);
});
test('`vc --debug project ls` should output the projects listing', async t => {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--debug', 'project', 'ls'],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.true(
stderr.includes('> Projects found under'),
formatOutput({ stderr, stdout })
);
});
test('deploy gatsby twice and print cached directories', async t => {
const directory = example('gatsby');
const packageJsonPath = path.join(directory, 'package.json');
const packageJsonOriginal = await readFile(packageJsonPath, 'utf8');
const pkg = JSON.parse(packageJsonOriginal);
async function tryDeploy(cwd) {
await execa(binaryPath, [...defaultArgs, '--public', '--yes'], {
cwd,
stdio: 'inherit',
reject: true,
});
t.true(true);
}
// Deploy once to populate the cache
await tryDeploy(directory);
// Wait because the cache is not available right away
// See https://codeburst.io/quick-explanation-of-the-s3-consistency-model-6c9f325e3f82
await sleep(60000);
// Update build script to ensure cached files were restored in the next deploy
pkg.scripts.build = `ls -lA && ls .cache && ls public && ${pkg.scripts.build}`;
await writeFile(packageJsonPath, JSON.stringify(pkg));
try {
await tryDeploy(directory);
} finally {
await writeFile(packageJsonPath, packageJsonOriginal);
}
});
test('deploy pnpm twice using pnp and symlink=false', async t => {
const directory = path.join(__dirname, 'fixtures/unit/pnpm-pnp-symlink');
await remove(path.join(directory, '.vercel'));
function deploy() {
return execa(binaryPath, [
directory,
'--name',
session,
...defaultArgs,
'--public',
'--yes',
]);
}
let { exitCode, stderr, stdout } = await deploy();
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
let page = await fetch(stdout);
let text = await page.text();
t.is(text, 'no cache\n');
({ exitCode, stderr, stdout } = await deploy());
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
page = await fetch(stdout);
text = await page.text();
t.is(text, 'cache exists\n');
});
test('reject deploying with wrong team .vercel config', async t => {
const directory = fixture('unauthorized-vercel-config');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--yes'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.true(
stderr.includes(
'Could not retrieve Project Settings. To link your Project, remove the `.vercel` directory and deploy again.'
),
formatOutput({ stderr, stdout })
);
});
test('reject deploying with invalid token', async t => {
const directory = fixture('unauthorized-vercel-config');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--yes'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.regex(
stderr,
/Error: Could not retrieve Project Settings\. To link your Project, remove the `\.vercel` directory and deploy again\./g
);
});
test('[vc link] should show prompts to set up project', async t => {
const dir = fixture('project-link-zeroconf');
const projectName = `project-link-zeroconf-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const vc = execa(binaryPath, ['link', ...defaultArgs], {
cwd: dir,
env: {
FORCE_TTY: '1',
},
});
await setupProject(vc, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await vc;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc link --yes] should not show prompts and autolink', async t => {
const dir = fixture('project-link-confirm');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--yes', ...defaultArgs],
{ cwd: dir, reject: false }
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc link] should not duplicate paths in .gitignore', async t => {
const dir = fixture('project-link-gitignore');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--yes', ...defaultArgs],
{
cwd: dir,
reject: false,
env: {
FORCE_TTY: '1',
},
}
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n');
});
test('[vc dev] should show prompts to set up project', async t => {
const dir = fixture('project-link-dev');
const port = 58352;
const projectName = `project-link-dev-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
cwd: dir,
env: {
FORCE_TTY: '1',
},
});
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works
try {
const response = await fetch(`http://localhost:${port}/`);
const text = await response.text();
t.is(text.includes('<h1>custom hello</h1>'), true, text);
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('[vc link] should show project prompts but not framework when `builds` defined', async t => {
const dir = fixture('project-link-legacy');
const projectName = `project-link-legacy-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const vc = execa(binaryPath, ['link', ...defaultArgs], {
cwd: dir,
env: {
FORCE_TTY: '1',
},
});
await waitForPrompt(vc, chunk => /Set up [^?]+\?/.test(chunk));
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Which scope should contain your project?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
vc.stdin.write('no\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Whats your projects name?')
);
vc.stdin.write(`${projectName}\n`);
await waitForPrompt(vc, chunk =>
chunk.includes('In which directory is your code located?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
const output = await vc;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.vercel\n');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc dev] should send the platform proxy request headers to frontend dev server ', async t => {
const dir = fixture('dev-proxy-headers-and-env');
const port = 58353;
const projectName = `dev-proxy-headers-and-env-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
cwd: dir,
env: {
FORCE_TTY: '1',
},
});
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
devCommand: 'node server.js',
});
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works
try {
const response = await fetch(`http://localhost:${port}/`);
const body = await response.json();
t.is(body.headers['x-vercel-deployment-url'], `localhost:${port}`);
t.is(body.env.NOW_REGION, 'dev1');
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('[vc link] should support the `--project` flag', async t => {
const projectName = 'link-project-flag';
const directory = fixture('static-deployment');
const [user, output] = await Promise.all([
fetchTokenInformation(token),
execute(['link', '--yes', '--project', projectName, directory]),
]);
t.is(output.exitCode, 0, formatOutput(output));
t.true(
output.stderr.includes(`Linked to ${user.username}/${projectName}`),
formatOutput(output)
);
});
test('[vc build] should build project with `@vercel/static-build`', async t => {
const directory = fixture('vc-build-static-build');
const output = await execute(['build'], { cwd: directory });
t.is(output.exitCode, 0);
t.true(output.stderr.includes('Build Completed in .vercel/output'));
t.is(
await fs.readFile(
path.join(directory, '.vercel/output/static/index.txt'),
'utf8'
),
'hi\n'
);
const config = await fs.readJSON(
path.join(directory, '.vercel/output/config.json')
);
t.is(config.version, 3);
const builds = await fs.readJSON(
path.join(directory, '.vercel/output/builds.json')
);
t.is(builds.target, 'preview');
t.is(builds.builds[0].src, 'package.json');
t.is(builds.builds[0].use, '@vercel/static-build');
});
test('[vc build] should not include .vercel when distDir is "."', async t => {
const directory = fixture('static-build-dist-dir');
const output = await execute(['build'], { cwd: directory });
t.is(output.exitCode, 0);
t.true(output.stderr.includes('Build Completed in .vercel/output'));
const dir = await fs.readdir(path.join(directory, '.vercel/output/static'));
t.false(dir.includes('.vercel'));
t.true(dir.includes('index.txt'));
});
test('[vc build] should not include .vercel when zeroConfig is true and outputDirectory is "."', async t => {
const directory = fixture('static-build-zero-config-output-directory');
const output = await execute(['build'], { cwd: directory });
t.is(output.exitCode, 0);
t.true(output.stderr.includes('Build Completed in .vercel/output'));
const dir = await fs.readdir(path.join(directory, '.vercel/output/static'));
t.false(dir.includes('.vercel'));
t.true(dir.includes('index.txt'));
});
test('vercel.json configuration overrides in a new project prompt user and merges settings correctly', async t => {
const directory = fixture(
'vercel-json-configuration-overrides-merging-prompts'
);
// remove previously linked project if it exists
await remove(path.join(directory, '.vercel'));
const vc = execa(binaryPath, [directory, ...defaultArgs], { reject: false });
await waitForPrompt(vc, chunk => chunk.includes('Set up and deploy'));
vc.stdin.write('y\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
vc.stdin.write('n\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Whats your projects name?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk =>
chunk.includes('In which directory is your code located?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Want to modify these settings?')
);
vc.stdin.write('y\n');
await waitForPrompt(vc, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
vc.stdin.write('a\n');
await waitForPrompt(vc, chunk =>
chunk.includes("What's your Development Command?")
);
vc.stdin.write('echo "DEV COMMAND"\n');
// the crux of this test is to make sure that the outputDirectory is properly set by the prompts.
// otherwise the output from the build command will not be the index route and the page text assertion below will fail.
await waitForPrompt(vc, chunk =>
chunk.includes("What's your Output Directory?")
);
vc.stdin.write('output\n');
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
const deployment = await vc;
t.is(deployment.exitCode, 0, formatOutput(deployment));
// assert the command were executed
let page = await fetch(deployment.stdout);
let text = await page.text();
t.is(text, '1\n');
});
test('vercel.json configuration overrides in an existing project do not prompt user and correctly apply overrides', async t => {
// create project directory and get path to vercel.json
const directory = fixture('vercel-json-configuration-overrides');
const vercelJsonPath = path.join(directory, 'vercel.json');
async function deploy(autoConfirm = false) {
const deployment = await execa(
binaryPath,
[directory, ...defaultArgs, '--public'].concat(
autoConfirm ? ['--yes'] : []
),
{ reject: false }
);
t.is(
deployment.exitCode,
0,
formatOutput({
stderr: deployment.stderr,
stdout: deployment.stdout,
})
);
return deployment;
}
// Step 1. Create a simple static deployment with no configuration.
// Deployment should succeed and page should display "0"
await mkdir(path.join(directory, 'public'));
await writeFile(path.join(directory, 'public/index.txt'), '0');
// auto-confirm this deployment
let deployment = await deploy(true);
let page = await fetch(deployment.stdout);
let text = await page.text();
t.is(text, '0');
// Step 2. Now that the project exists, override the buildCommand and outputDirectory.
// The CLI should not prompt the user about the overrides.
const BUILD_COMMAND = 'mkdir output && echo "1" >> output/index.txt';
const OUTPUT_DIRECTORY = 'output';
await writeFile(
vercelJsonPath,
JSON.stringify({
buildCommand: BUILD_COMMAND,
outputDirectory: OUTPUT_DIRECTORY,
})
);
deployment = await deploy();
page = await fetch(deployment.stdout);
text = await page.text();
t.is(text, '1\n');
// // Step 3. Do a more complex deployment using a framework this time
await mkdir(`${directory}/pages`);
await writeFile(
`${directory}/pages/index.js`,
`export default () => 'Next.js Test'`
);
await writeFile(
vercelJsonPath,
JSON.stringify({
framework: 'nextjs',
})
);
await writeFile(
`${directory}/package.json`,
JSON.stringify({
scripts: {
dev: 'next',
start: 'next start',
build: 'next build',
},
dependencies: {
next: 'latest',
react: 'latest',
'react-dom': 'latest',
},
})
);
deployment = await deploy();
page = await fetch(deployment.stdout);
text = await page.text();
t.regex(text, /Next\.js Test/);
});