import path from 'path'; import { URL, parse as parseUrl } from 'url'; import fetch, { RequestInit } from 'node-fetch'; import retry from 'async-retry'; import fs, { writeFile, readFile, remove, copy, ensureDir, mkdir, } from 'fs-extra'; import sleep from '../src/util/sleep'; import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy'; import waitForPrompt from './helpers/wait-for-prompt'; import { execCli } from './helpers/exec'; import getGlobalDir from './helpers/get-global-dir'; import { listTmpDirs } from './helpers/get-tmp-dir'; import { setupE2EFixture, prepareE2EFixtures, } from './helpers/setup-e2e-fixture'; import formatOutput from './helpers/format-output'; import type { PackageJson } from '@vercel/build-utils'; import type http from 'http'; import { CLIProcess } from './helpers/types'; const TEST_TIMEOUT = 3 * 60 * 1000; jest.setTimeout(TEST_TIMEOUT); const binaryPath = path.resolve(__dirname, `../scripts/start.js`); const example = (name: string) => path.join(__dirname, '..', '..', '..', 'examples', name); let session = 'temp-session'; function fetchTokenInformation(token: string, 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}", status: ${ res.status }, id: ${res.headers.get('x-vercel-id')}` ); } const data = await res.json(); return data.user; }, { retries, factor: 1 } ); } let token: string | undefined; let email: string | undefined; let contextName: string | undefined; function mockLoginApi(req: http.IncomingMessage, res: http.ServerResponse) { const { url = '/', method } = req; let { pathname = '/', query = {} } = parseUrl(url, true); // eslint-disable-next-line no-console 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 if (method === 'GET' && pathname === '/v2/user') { res.end(JSON.stringify({ user: { email } })); } 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}`; // eslint-disable-next-line no-console console.log(`[mock-login-server] Listening on ${loginApiUrl}`); }); const apiFetch = (url: string, { headers, ...options }: RequestInit = {}) => { return fetch(`https://api.vercel.com${url}`, { headers: { Authorization: `Bearer ${token}`, ...(headers || {}), }, ...options, }); }; const createUser = async () => { await retry( async () => { 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 } ); }; function getConfigAuthPath() { return path.join(getGlobalDir(), 'auth.json'); } async function setupProject( process: CLIProcess, projectName: string, overrides: { devCommand?: string; buildCommand?: string; outputDirectory?: string; } ) { await waitForPrompt(process, /Set up [^?]+\?/); process.stdin?.write('yes\n'); await waitForPrompt(process, /Which scope [^?]+\?/); process.stdin?.write('\n'); await waitForPrompt(process, 'Link to existing project?'); process.stdin?.write('no\n'); await waitForPrompt(process, 'What’s your project’s name?'); process.stdin?.write(`${projectName}\n`); await waitForPrompt(process, 'In which directory is your code located?'); process.stdin?.write('\n'); await waitForPrompt(process, 'Want to modify these settings?'); if (overrides) { process.stdin?.write('yes\n'); const { buildCommand, outputDirectory, devCommand } = overrides; await waitForPrompt( process, 'Which settings would you like to overwrite (select multiple)?' ); process.stdin?.write('a\n'); // 'a' means select all await waitForPrompt(process, `What's your Build Command?`); process.stdin?.write(`${buildCommand || ''}\n`); await waitForPrompt(process, `What's your Development Command?`); process.stdin?.write(`${devCommand || ''}\n`); await waitForPrompt(process, `What's your Output Directory?`); process.stdin?.write(`${outputDirectory || ''}\n`); } else { process.stdin?.write('no\n'); } await waitForPrompt(process, 'Linked to'); } beforeAll(async () => { try { await createUser(); if (!contextName) { throw new Error('Shared state "contextName" not set.'); } await prepareE2EFixtures(contextName, binaryPath); } catch (err) { // eslint-disable-next-line no-console console.log('Failed test suite `beforeAll`'); // eslint-disable-next-line no-console console.log(err); // force test suite to actually stop process.exit(1); } }); afterAll(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 execCli(binaryPath, ['logout']); } const allTmpDirs = listTmpDirs(); for (const tmpDir of allTmpDirs) { tmpDir.removeCallback(); } }); test( 'change user', async () => { if (!email) { throw new Error('Shared state "email" not set.'); } const { stdout: prevUser } = await execCli(binaryPath, ['whoami']); // Delete the current token await execCli(binaryPath, ['logout', '--debug'], { stdio: 'inherit' }); await createUser(); await execCli( binaryPath, ['login', email, '--api', loginApiUrl, '--debug'], { stdio: 'inherit', env: { FORCE_TTY: '1', }, } ); const auth = await fs.readJSON(getConfigAuthPath()); expect(auth.token).toBe(token); const { stdout: nextUser } = await execCli(binaryPath, ['whoami']); expect(typeof prevUser, prevUser).toBe('string'); expect(typeof nextUser, nextUser).toBe('string'); expect(prevUser).not.toBe(nextUser); }, 60 * 1000 ); test('assign a domain to a project', async () => { const domain = `project-domain.${contextName}.vercel.app`; const directory = await setupE2EFixture('static-deployment'); const deploymentOutput = await execCli(binaryPath, [ directory, '--public', '--yes', ]); expect(deploymentOutput.exitCode, formatOutput(deploymentOutput)).toBe(0); const host = deploymentOutput.stdout?.trim().replace('https://', ''); const deployment = await apiFetch( `/v10/now/deployments/unknown?url=${host}` ).then(resp => resp.json()); expect(typeof deployment.name).toBe('string'); const project = deployment.name; const output = await execCli(binaryPath, [ 'domains', 'add', domain, project, '--force', ]); expect(output.exitCode, formatOutput(output)).toBe(0); const removeResponse = await execCli(binaryPath, ['rm', project, '-y']); expect(removeResponse.exitCode, formatOutput(removeResponse)).toBe(0); }); test('ensure `github` and `scope` are not sent to the API', async () => { const directory = await setupE2EFixture('github-and-scope-config'); const output = await execCli(binaryPath, [directory, '--yes']); expect(output.exitCode, formatOutput(output)).toBe(0); }); test('should show prompts to set up project during first deploy', async () => { const dir = await setupE2EFixture('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 = execCli(binaryPath, [dir]); await setupProject(now, projectName, { buildCommand: `mkdir -p o && echo '

custom hello

' > o/index.html`, outputDirectory: 'o', }); const output = await now; // Ensure the exit code is right expect(output.exitCode, formatOutput(output)).toBe(0); // Ensure .gitignore is created const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8'); expect(gitignore).toBe('.vercel\n'); // Ensure .vercel/project.json and .vercel/README.txt are created expect( fs.existsSync(path.join(dir, '.vercel', 'project.json')), 'project.json' ).toBe(true); expect( fs.existsSync(path.join(dir, '.vercel', 'README.txt')), 'README.txt' ).toBe(true); const { href } = new URL(output.stdout); // Send a test request to the deployment const response = await fetch(href); const text = await response.text(); expect(text).toContain('

custom hello

'); // Ensure that `vc dev` also uses the configured build command // and output directory let stderr = ''; const port = 58351; const dev = execCli(binaryPath, ['dev', '--listen', port.toString(), dir]); dev.stderr?.setEncoding('utf8'); try { dev.stdin?.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(); expect(text2).toContain('

custom hello

'); } finally { process.kill(dev.pid, 'SIGTERM'); } }); test('should prefill "project name" prompt with folder name', async () => { const projectName = `static-deployment-${ Math.random().toString(36).split('.')[1] }`; const src = await setupE2EFixture('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 = execCli(binaryPath, [directory], { env: { FORCE_TTY: '1', }, }); await waitForPrompt(now, /Set up and deploy [^?]+\?/); now.stdin?.write('yes\n'); await waitForPrompt(now, 'Which scope do you want to deploy to?'); now.stdin?.write('\n'); await waitForPrompt(now, 'Link to existing project?'); now.stdin?.write('no\n'); await waitForPrompt(now, `What’s your project’s name? (${projectName})`); now.stdin?.write(`\n`); await waitForPrompt(now, 'In which directory is your code located?'); now.stdin?.write('\n'); await waitForPrompt(now, 'Want to modify these settings?'); now.stdin?.write('no\n'); const output = await now; expect(output.exitCode, formatOutput(output)).toBe(0); }); test('should prefill "project name" prompt with --name', async () => { const directory = await setupE2EFixture('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 = execCli(binaryPath, [directory, '--name', projectName], { 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'); expect(isDeprecated, 'isDeprecated').toBe(true); await waitForPrompt(now, 'Which scope do you want to deploy to?'); now.stdin?.write('\n'); await waitForPrompt(now, 'Link to existing project?'); now.stdin?.write('no\n'); await waitForPrompt(now, `What’s your project’s name? (${projectName})`); now.stdin?.write(`\n`); await waitForPrompt(now, 'In which directory is your code located?'); now.stdin?.write('\n'); await waitForPrompt(now, 'Want to modify these settings?'); now.stdin?.write('no\n'); const output = await now; expect(output.exitCode, formatOutput(output)).toBe(0); }); test('should prefill "project name" prompt with now.json `name`', async () => { const directory = await setupE2EFixture('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 = execCli(binaryPath, [directory], { 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, /Set up and deploy [^?]+\?/); now.stdin?.write('yes\n'); await waitForPrompt(now, 'Which scope do you want to deploy to?'); now.stdin?.write('\n'); await waitForPrompt(now, 'Link to existing project?'); now.stdin?.write('no\n'); await waitForPrompt(now, `What’s your project’s name? (${projectName})`); now.stdin?.write(`\n`); await waitForPrompt(now, 'In which directory is your code located?'); now.stdin?.write('\n'); await waitForPrompt(now, 'Want to modify these settings?'); now.stdin?.write('no\n'); const output = await now; expect(output.exitCode, formatOutput(output)).toBe(0); expect(isDeprecated, 'isDeprecated').toBe(true); // clean up await remove(path.join(directory, 'vercel.json')); }); test('deploy with unknown `VERCEL_PROJECT_ID` should fail', async () => { if (!token) { throw new Error('Shared state "token" not set.'); } const directory = await setupE2EFixture('static-deployment'); const user = await fetchTokenInformation(token); const output = await execCli(binaryPath, [directory], { env: { VERCEL_ORG_ID: user.id, VERCEL_PROJECT_ID: 'asdf', }, }); expect(output.exitCode, formatOutput(output)).toBe(1); expect(output.stderr).toContain('Project not found'); }); test('deploy with `VERCEL_ORG_ID` but without `VERCEL_PROJECT_ID` should fail', async () => { if (!token) { throw new Error('Shared state "token" not set.'); } const directory = await setupE2EFixture('static-deployment'); const user = await fetchTokenInformation(token); const output = await execCli(binaryPath, [directory], { env: { VERCEL_ORG_ID: user.id }, }); expect(output.exitCode, formatOutput(output)).toBe(1); expect(output.stderr).toContain( 'You specified `VERCEL_ORG_ID` but you forgot to specify `VERCEL_PROJECT_ID`. You need to specify both to deploy to a custom project.' ); }); test('deploy with `VERCEL_PROJECT_ID` but without `VERCEL_ORG_ID` should fail', async () => { const directory = await setupE2EFixture('static-deployment'); const output = await execCli(binaryPath, [directory], { env: { VERCEL_PROJECT_ID: 'asdf' }, }); expect(output.exitCode, formatOutput(output)).toBe(1); expect(output.stderr).toContain( 'You specified `VERCEL_PROJECT_ID` but you forgot to specify `VERCEL_ORG_ID`. You need to specify both to deploy to a custom project.' ); }); test('deploy with `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID`', async () => { const directory = await setupE2EFixture('static-deployment'); // generate `.vercel` await execCli(binaryPath, [directory, '--yes']); const link = require(path.join(directory, '.vercel/project.json')); await remove(path.join(directory, '.vercel')); const output = await execCli(binaryPath, [directory], { env: { VERCEL_ORG_ID: link.orgId, VERCEL_PROJECT_ID: link.projectId, }, }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stdout).not.toContain('Linked to'); }); test('deploy shows notice when project in `.vercel` does not exists', async () => { const directory = await setupE2EFixture('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 = execCli(binaryPath, [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 don’t have access to it anymore' ); return /Set up and deploy [^?]+\?/.test(chunk); }); now.stdin?.write('no\n'); expect(detectedNotice, 'detectedNotice').toBe(true); }); test('use `rootDirectory` from project when deploying', async () => { const directory = await setupE2EFixture('project-root-directory'); const firstResult = await execCli(binaryPath, [ directory, '--yes', '--public', ]); expect(firstResult.exitCode, formatOutput(firstResult)).toBe(0); const { host: firstHost } = new URL(firstResult.stdout); const response = await apiFetch(`/v12/now/deployments/get?url=${firstHost}`); expect(response.status).toBe(200); const { projectId } = await response.json(); expect(typeof projectId).toBe('string'); const projectResponse = await apiFetch(`/v2/projects/${projectId}`, { method: 'PATCH', body: JSON.stringify({ rootDirectory: 'src', }), }); expect(projectResponse.status, await projectResponse.text()).toBe(200); const secondResult = await execCli(binaryPath, [directory, '--public']); expect(secondResult.exitCode, formatOutput(secondResult)).toBe(0); const { href } = new URL(secondResult.stdout); const pageResponse1 = await fetch(href); expect(pageResponse1.status).toBe(200); expect(await pageResponse1.text()).toMatch(/I am a website/gm); // Ensures that the `now.json` file has been applied const pageResponse2 = await fetch(`${secondResult.stdout}/i-do-exist`); expect(pageResponse2.status).toBe(200); expect(await pageResponse2.text()).toMatch(/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 () => { const output = await execCli(binaryPath, ['deploy'], { env: { VERCEL_ORG_ID: 'asdf', VERCEL_PROJECT_ID: 'asdf' }, }); expect(output.exitCode, formatOutput(output)).toBe(1); expect(output.stderr).toContain('Project not found'); }); test('vercel env with unknown `VERCEL_ORG_ID` or `VERCEL_PROJECT_ID` should error', async () => { const output = await execCli(binaryPath, ['env'], { env: { VERCEL_ORG_ID: 'asdf', VERCEL_PROJECT_ID: 'asdf' }, }); expect(output.exitCode, formatOutput(output)).toBe(1); expect(output.stderr).toContain('Project not found'); }); test('add a sensitive env var', async () => { const dir = await setupE2EFixture('project-sensitive-env-vars'); const projectName = `project-sensitive-env-vars-${ Math.random().toString(36).split('.')[1] }`; // remove previously linked project if it exists await remove(path.join(dir, '.vercel')); const vc = execCli(binaryPath, ['link'], { cwd: dir, env: { FORCE_TTY: '1', }, }); await setupProject(vc, projectName, { buildCommand: `mkdir -p o && echo '

custom hello

' > o/index.html`, outputDirectory: 'o', }); await vc; const link = require(path.join(dir, '.vercel/project.json')); const addEnvCommand = execCli( binaryPath, ['env', 'add', 'envVarName', 'production', '--sensitive'], { env: { VERCEL_ORG_ID: link.orgId, VERCEL_PROJECT_ID: link.projectId, }, } ); await waitForPrompt(addEnvCommand, /What’s the value of [^?]+\?/); addEnvCommand.stdin?.write('test\n'); const output = await addEnvCommand; expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain( 'Added Environment Variable envVarName to Project' ); }); test('override an existing env var', async () => { const dir = await setupE2EFixture('project-override-env-vars'); const projectName = `project-override-env-vars-${ Math.random().toString(36).split('.')[1] }`; // remove previously linked project if it exists await remove(path.join(dir, '.vercel')); const vc = execCli(binaryPath, ['link'], { cwd: dir, env: { FORCE_TTY: '1', }, }); await setupProject(vc, projectName, { buildCommand: `mkdir -p o && echo '

custom hello

' > o/index.html`, outputDirectory: 'o', }); await vc; const link = require(path.join(dir, '.vercel/project.json')); const options = { env: { VERCEL_ORG_ID: link.orgId, VERCEL_PROJECT_ID: link.projectId, }, }; // 1. Initial add const addEnvCommand = execCli( binaryPath, ['env', 'add', 'envVarName', 'production'], options ); await waitForPrompt(addEnvCommand, /What’s the value of [^?]+\?/); addEnvCommand.stdin?.write('test\n'); const output = await addEnvCommand; expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain( 'Added Environment Variable envVarName to Project' ); // 2. Override const overrideEnvCommand = execCli( binaryPath, ['env', 'add', 'envVarName', 'production', '--force'], options ); await waitForPrompt(overrideEnvCommand, /What’s the value of [^?]+\?/); overrideEnvCommand.stdin?.write('test\n'); const outputOverride = await overrideEnvCommand; expect(outputOverride.exitCode, formatOutput(outputOverride)).toBe(0); expect(outputOverride.stderr).toContain( 'Overrode Environment Variable envVarName to Project' ); }); test('whoami with `VERCEL_ORG_ID` should favor `--scope` and should error', async () => { if (!token) { throw new Error('Shared state "token" not set.'); } const user = await fetchTokenInformation(token); const output = await execCli(binaryPath, ['whoami', '--scope', 'asdf'], { env: { VERCEL_ORG_ID: user.id }, }); expect(output.exitCode, formatOutput(output)).toBe(1); expect(output.stderr).toContain('The specified scope does not exist'); }); test('whoami with local .vercel scope', async () => { if (!token) { throw new Error('Shared state "token" not set.'); } const directory = await setupE2EFixture('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 execCli(binaryPath, ['whoami'], { cwd: directory, }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stdout).toContain(contextName); // clean up await remove(path.join(directory, '.vercel')); }); test('deploys with only now.json and README.md', async () => { const directory = await setupE2EFixture('deploy-with-only-readme-now-json'); const { exitCode, stdout, stderr } = await execCli(binaryPath, ['--yes'], { cwd: directory, }); expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); const { host } = new URL(stdout); const res = await fetch(`https://${host}/README.md`); const text = await res.text(); expect(text).toMatch(/readme contents/); }); test('deploys with only vercel.json and README.md', async () => { const directory = await setupE2EFixture( 'deploy-with-only-readme-vercel-json' ); const { exitCode, stdout, stderr } = await execCli(binaryPath, ['--yes'], { cwd: directory, }); expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); // assert timing order of showing URLs vs status updates expect(stderr).toMatch( /Inspect.*\nProduction.*\nQueued.*\nBuilding.*\nCompleting/ ); const { host } = new URL(stdout); const res = await fetch(`https://${host}/README.md`); const text = await res.text(); expect(text).toMatch(/readme contents/); }); test('reject conflicting `vercel.json` and `now.json` files', async () => { const directory = await setupE2EFixture('conflicting-now-json-vercel-json'); const { exitCode, stdout, stderr } = await execCli(binaryPath, ['--yes'], { cwd: directory, }); expect(exitCode, formatOutput({ stdout, stderr })).toBe(1); expect(stderr).toContain( 'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.' ); }); test('`vc --debug project ls` should output the projects listing', async () => { const { exitCode, stdout, stderr } = await execCli(binaryPath, [ '--debug', 'project', 'ls', ]); expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); expect(stderr).toContain('> Projects found under'); }); test( 'deploy gatsby twice and print cached directories', async () => { const directory = example('gatsby'); const packageJsonPath = path.join(directory, 'package.json'); const packageJsonOriginal = await readFile(packageJsonPath, 'utf8'); const pkg = JSON.parse(packageJsonOriginal) as PackageJson; if (!pkg.scripts) { throw new Error(`"scripts" not found in "${packageJsonPath}"`); } async function tryDeploy(cwd: string) { const { exitCode, stdout, stderr } = await execCli( binaryPath, ['--public', '--yes'], { cwd, stdio: 'inherit', } ); expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); } // 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); } }, 6 * 60 * 1000 ); test('deploy pnpm twice using pnp and symlink=false', async () => { const directory = path.join(__dirname, 'fixtures/unit/pnpm-pnp-symlink'); await remove(path.join(directory, '.vercel')); async function deploy() { const res = await execCli(binaryPath, [ directory, '--name', session, '--public', '--yes', ]); return res; } let { exitCode, stdout, stderr } = await deploy(); expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); let page = await fetch(stdout); let text = await page.text(); expect(text).toBe('no cache\n'); ({ exitCode, stdout, stderr } = await deploy()); expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); page = await fetch(stdout); text = await page.text(); expect(text).toContain('cache exists\n'); }); test('reject deploying with wrong team .vercel config', async () => { const directory = await setupE2EFixture('unauthorized-vercel-config'); const { exitCode, stdout, stderr } = await execCli(binaryPath, ['--yes'], { cwd: directory, }); expect(exitCode, formatOutput({ stdout, stderr })).toBe(1); expect(stderr).toContain( 'Could not retrieve Project Settings. To link your Project, remove the `.vercel` directory and deploy again.' ); }); test('reject deploying with invalid token', async () => { const directory = await setupE2EFixture('unauthorized-vercel-config'); const { exitCode, stdout, stderr } = await execCli(binaryPath, ['--yes'], { cwd: directory, }); expect(exitCode, formatOutput({ stdout, stderr })).toBe(1); expect(stderr).toMatch( /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 () => { const dir = await setupE2EFixture('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 = execCli(binaryPath, ['link'], { cwd: dir, env: { FORCE_TTY: '1', }, }); await setupProject(vc, projectName, { buildCommand: `mkdir -p o && echo '

custom hello

' > o/index.html`, outputDirectory: 'o', }); const output = await vc; // Ensure the exit code is right expect(output.exitCode, formatOutput(output)).toBe(0); // Ensure .gitignore is created const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8'); expect(gitignore).toBe('.vercel\n'); // Ensure .vercel/project.json and .vercel/README.txt are created expect( fs.existsSync(path.join(dir, '.vercel', 'project.json')), 'project.json' ).toBe(true); expect( fs.existsSync(path.join(dir, '.vercel', 'README.txt')), 'README.txt' ).toBe(true); }); test('[vc link --yes] should not show prompts and autolink', async () => { const dir = await setupE2EFixture('project-link-confirm'); // remove previously linked project if it exists await remove(path.join(dir, '.vercel')); const { exitCode, stdout, stderr } = await execCli( binaryPath, ['link', '--yes'], { cwd: dir } ); // Ensure the exit code is right expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); // Ensure the message is correct pattern expect(stderr).toMatch(/Linked to /m); // Ensure .gitignore is created const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8'); expect(gitignore).toBe('.vercel\n'); // Ensure .vercel/project.json and .vercel/README.txt are created expect( fs.existsSync(path.join(dir, '.vercel', 'project.json')), 'project.json' ).toBe(true); expect( fs.existsSync(path.join(dir, '.vercel', 'README.txt')), 'README.txt' ).toBe(true); }); test('[vc link] should detect frameworks in project rootDirectory', async () => { const dir = await setupE2EFixture('zero-config-next-js-nested'); const projectRootDir = 'app'; 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 vc = execCli(binaryPath, ['link', `--project=${projectName}`], { cwd: dir, env: { FORCE_TTY: '1', }, }); await waitForPrompt(vc, /Set up [^?]+\?/); vc.stdin?.write('yes\n'); await waitForPrompt(vc, 'Which scope should contain your project?'); vc.stdin?.write('\n'); await waitForPrompt(vc, 'Link to existing project?'); vc.stdin?.write('no\n'); await waitForPrompt(vc, 'What’s your project’s name?'); vc.stdin?.write(`${projectName}\n`); await waitForPrompt(vc, 'In which directory is your code located?'); vc.stdin?.write(`${projectRootDir}\n`); // This means the framework detection worked! await waitForPrompt(vc, 'Auto-detected Project Settings (Next.js)'); vc.kill(); }); test('[vc link] should not duplicate paths in .gitignore', async () => { const dir = await setupE2EFixture('project-link-gitignore'); // remove previously linked project if it exists await remove(path.join(dir, '.vercel')); const { exitCode, stdout, stderr } = await execCli( binaryPath, ['link', '--yes'], { cwd: dir, env: { FORCE_TTY: '1', }, } ); // Ensure the exit code is right expect(exitCode, formatOutput({ stdout, stderr })).toBe(0); // Ensure the message is correct pattern expect(stderr).toMatch(/Linked to /m); // Ensure .gitignore is created const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8'); expect(gitignore).toBe('.vercel\n'); }); test('[vc dev] should show prompts to set up project', async () => { const dir = await setupE2EFixture('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 = execCli(binaryPath, ['dev', '--listen', port.toString()], { cwd: dir, env: { FORCE_TTY: '1', }, }); await setupProject(dev, projectName, { buildCommand: `mkdir -p o && echo '

custom hello

' > o/index.html`, outputDirectory: 'o', }); // Ensure .gitignore is created const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8'); expect(gitignore).toBe('.vercel\n'); // Ensure .vercel/project.json and .vercel/README.txt are created expect( fs.existsSync(path.join(dir, '.vercel', 'project.json')), 'project.json' ).toBe(true); expect( fs.existsSync(path.join(dir, '.vercel', 'README.txt')), 'README.txt' ).toBe(true); await waitForPrompt(dev, 'Ready! Available at'); // Ensure that `vc dev` also works try { const response = await fetch(`http://localhost:${port}/`); const text = await response.text(); expect(text).toContain('

custom hello

'); } finally { process.kill(dev.pid, 'SIGTERM'); } }); test('[vc link] should show project prompts but not framework when `builds` defined', async () => { const dir = await setupE2EFixture('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 = execCli(binaryPath, ['link'], { cwd: dir, env: { FORCE_TTY: '1', }, }); await waitForPrompt(vc, /Set up [^?]+\?/); vc.stdin?.write('yes\n'); await waitForPrompt(vc, 'Which scope should contain your project?'); vc.stdin?.write('\n'); await waitForPrompt(vc, 'Link to existing project?'); vc.stdin?.write('no\n'); await waitForPrompt(vc, 'What’s your project’s name?'); vc.stdin?.write(`${projectName}\n`); await waitForPrompt(vc, 'In which directory is your code located?'); vc.stdin?.write('\n'); await waitForPrompt(vc, 'Linked to'); const output = await vc; // Ensure the exit code is right expect(output.exitCode, formatOutput(output)).toBe(0); // Ensure .gitignore is created const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8'); expect(gitignore).toBe('.vercel\n'); // Ensure .vercel/project.json and .vercel/README.txt are created expect( fs.existsSync(path.join(dir, '.vercel', 'project.json')), 'project.json' ).toBe(true); expect( fs.existsSync(path.join(dir, '.vercel', 'README.txt')), 'README.txt' ).toBe(true); }); test('[vc dev] should send the platform proxy request headers to frontend dev server ', async () => { const dir = await setupE2EFixture('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 = execCli(binaryPath, ['dev', '--listen', port.toString()], { cwd: dir, env: { FORCE_TTY: '1', }, }); await setupProject(dev, projectName, { buildCommand: `mkdir -p o && echo '

custom hello

' > o/index.html`, outputDirectory: 'o', devCommand: 'node server.js', }); await waitForPrompt(dev, 'Ready! Available at'); // Ensure that `vc dev` also works try { const response = await fetch(`http://localhost:${port}/`); const body = await response.json(); expect(body.headers['x-vercel-deployment-url']).toBe(`localhost:${port}`); expect(body.env.NOW_REGION).toBe('dev1'); } finally { process.kill(dev.pid, 'SIGTERM'); } }); test('[vc link] should support the `--project` flag', async () => { if (!token) { throw new Error('Shared state "token" not set.'); } const projectName = 'link-project-flag'; const directory = await setupE2EFixture('static-deployment'); const [user, output] = await Promise.all([ fetchTokenInformation(token), execCli(binaryPath, ['link', '--yes', '--project', projectName, directory]), ]); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain(`Linked to ${user.username}/${projectName}`); }); test('[vc build] should build project with `@vercel/static-build`', async () => { const directory = await setupE2EFixture('vc-build-static-build'); const output = await execCli(binaryPath, ['build'], { cwd: directory }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain('Build Completed in .vercel/output'); expect( await fs.readFile( path.join(directory, '.vercel/output/static/index.txt'), 'utf8' ) ).toBe('hi\n'); const config = await fs.readJSON( path.join(directory, '.vercel/output/config.json') ); expect(config.version).toBe(3); const builds = await fs.readJSON( path.join(directory, '.vercel/output/builds.json') ); expect(builds.target).toBe('preview'); expect(builds.builds[0].src).toBe('package.json'); expect(builds.builds[0].use).toBe('@vercel/static-build'); }); test('[vc build] should build project with `@vercel/speed-insights`', async () => { const directory = await setupE2EFixture('vc-build-speed-insights'); const output = await execCli(binaryPath, ['build'], { cwd: directory }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain('Build Completed in .vercel/output'); const builds = await fs.readJSON( path.join(directory, '.vercel/output/builds.json') ); expect(builds?.features?.speedInsightsVersion).toEqual('0.0.4'); }); test('[vc build] should build project with an indirect dependency to `@vercel/analytics`', async () => { const directory = await setupE2EFixture('vc-build-indirect-web-analytics'); const output = await execCli(binaryPath, ['build'], { cwd: directory }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain('Build Completed in .vercel/output'); const builds = await fs.readJSON( path.join(directory, '.vercel/output/builds.json') ); expect(builds?.features?.webAnalyticsVersion).toEqual('1.1.1'); }); test('[vc build] should build project with `@vercel/analytics`', async () => { const directory = await setupE2EFixture('vc-build-web-analytics'); const output = await execCli(binaryPath, ['build'], { cwd: directory }); expect(output.exitCode, formatOutput(output)).toBe(0); const builds = await fs.readJSON( path.join(directory, '.vercel/output/builds.json') ); expect(builds?.features?.webAnalyticsVersion).toEqual('1.0.0'); }); test('[vc build] should not include .vercel when distDir is "."', async () => { const directory = await setupE2EFixture('static-build-dist-dir'); const output = await execCli(binaryPath, ['build'], { cwd: directory }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain('Build Completed in .vercel/output'); const dir = await fs.readdir(path.join(directory, '.vercel/output/static')); expect(dir).not.toContain('.vercel'); expect(dir).toContain('index.txt'); }); test('[vc build] should not include .vercel when zeroConfig is true and outputDirectory is "."', async () => { const directory = await setupE2EFixture( 'static-build-zero-config-output-directory' ); const output = await execCli(binaryPath, ['build'], { cwd: directory }); expect(output.exitCode, formatOutput(output)).toBe(0); expect(output.stderr).toContain('Build Completed in .vercel/output'); const dir = await fs.readdir(path.join(directory, '.vercel/output/static')); expect(dir).not.toContain('.vercel'); expect(dir).toContain('index.txt'); }); test('vercel.json configuration overrides in a new project prompt user and merges settings correctly', async () => { const directory = await setupE2EFixture( 'vercel-json-configuration-overrides-merging-prompts' ); // remove previously linked project if it exists await remove(path.join(directory, '.vercel')); const vc = execCli(binaryPath, [directory]); await waitForPrompt(vc, 'Set up and deploy'); vc.stdin?.write('y\n'); await waitForPrompt(vc, 'Which scope do you want to deploy to?'); vc.stdin?.write('\n'); await waitForPrompt(vc, 'Link to existing project?'); vc.stdin?.write('n\n'); await waitForPrompt(vc, 'What’s your project’s name?'); vc.stdin?.write('\n'); await waitForPrompt(vc, 'In which directory is your code located?'); vc.stdin?.write('\n'); await waitForPrompt(vc, 'Want to modify these settings?'); vc.stdin?.write('y\n'); await waitForPrompt( vc, 'Which settings would you like to overwrite (select multiple)?' ); vc.stdin?.write('a\n'); await waitForPrompt(vc, "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, "What's your Output Directory?"); vc.stdin?.write('output\n'); await waitForPrompt(vc, 'Linked to'); const deployment = await vc; expect(deployment.exitCode, formatOutput(deployment)).toBe(0); // assert the command were executed let page = await fetch(deployment.stdout); let text = await page.text(); expect(text).toBe('1\n'); }); test('vercel.json configuration overrides in an existing project do not prompt user and correctly apply overrides', async () => { // create project directory and get path to vercel.json const directory = await setupE2EFixture( 'vercel-json-configuration-overrides' ); const vercelJsonPath = path.join(directory, 'vercel.json'); async function deploy(autoConfirm = false) { const deployment = await execCli( binaryPath, [directory, '--public'].concat(autoConfirm ? ['--yes'] : []) ); expect(deployment.exitCode, formatOutput(deployment)).toBe(0); 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); const { href } = new URL(deployment.stdout); let page = await fetch(href); let text = await page.text(); expect(text).toBe('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(); expect(text).toBe('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(); expect(text).toMatch(/Next\.js Test/); });