diff --git a/packages/cli/src/commands/deploy/args.ts b/packages/cli/src/commands/deploy/args.ts index f041904f9..14cc06f99 100644 --- a/packages/cli/src/commands/deploy/args.ts +++ b/packages/cli/src/commands/deploy/args.ts @@ -42,6 +42,7 @@ export const help = () => ` -h, --help Output usage information -v, --version Output the version number + --cwd Current working directory -V, --platform-version Set the platform version to deploy to -A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline( 'FILE' diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a8ebcecce..72eaddfb0 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -12,7 +12,7 @@ try { } import { join } from 'path'; -import { existsSync, lstatSync } from 'fs'; +import { existsSync } from 'fs'; import sourceMap from '@zeit/source-map-support'; import { mkdirp } from 'fs-extra'; import chalk from 'chalk'; @@ -136,6 +136,11 @@ const main = async () => { return 1; } + const cwd = argv['--cwd']; + if (cwd) { + process.chdir(cwd); + } + // Print update information, if available if (notifier.update && notifier.update.latest !== pkg.version && isTTY) { const { latest } = notifier.update; @@ -386,34 +391,11 @@ const main = async () => { GLOBAL_COMMANDS.has(targetOrSubcommand) || commands.has(targetOrSubcommand); - if (targetPathExists && subcommandExists) { - const fileType = lstatSync(targetPath).isDirectory() - ? 'subdirectory' - : 'file'; - const plural = targetOrSubcommand + 's'; - const singular = targetOrSubcommand.endsWith('s') - ? targetOrSubcommand.slice(0, -1) - : ''; - let alternative = ''; - if (commands.has(plural)) { - alternative = plural; - } else if (commands.has(singular)) { - alternative = singular; - } - console.error( - error( - `The supplied argument ${param(targetOrSubcommand)} is ambiguous.` + - `\nIf you wish to deploy the ${fileType} ${param( - targetOrSubcommand - )}, first run "cd ${targetOrSubcommand}". ` + - (alternative - ? `\nIf you wish to use the subcommand ${param( - targetOrSubcommand - )}, use ${param(alternative)} instead.` - : '') - ) + if (targetPathExists && subcommandExists && !argv['--cwd']) { + output.warn( + `Did you mean to deploy the subdirectory "${targetOrSubcommand}"? ` + + `Use \`vc --cwd ${targetOrSubcommand}\` instead.` ); - return 1; } if (subcommandExists) { diff --git a/packages/cli/src/util/arg-common.ts b/packages/cli/src/util/arg-common.ts index f79b52640..03f039f71 100644 --- a/packages/cli/src/util/arg-common.ts +++ b/packages/cli/src/util/arg-common.ts @@ -25,7 +25,9 @@ const ARG_COMMON = { '--api': String, - '--target': String + '--target': String, + + '--cwd': String, }; export default () => ARG_COMMON; diff --git a/packages/cli/src/util/env/add-env-record.ts b/packages/cli/src/util/env/add-env-record.ts index 73125bd52..ce53f95f8 100644 --- a/packages/cli/src/util/env/add-env-record.ts +++ b/packages/cli/src/util/env/add-env-record.ts @@ -26,7 +26,7 @@ export default async function addEnvRecord( target: targets, gitBranch: gitBranch || undefined, }; - const url = `/v7/projects/${projectId}/env`; + const url = `/v8/projects/${projectId}/env`; await client.fetch(url, { method: 'POST', body: JSON.stringify(body), diff --git a/packages/cli/src/util/env/get-env-records.ts b/packages/cli/src/util/env/get-env-records.ts index eda20b314..ce6c9b966 100644 --- a/packages/cli/src/util/env/get-env-records.ts +++ b/packages/cli/src/util/env/get-env-records.ts @@ -32,7 +32,7 @@ export default async function getEnvRecords( query.set('decrypt', decrypt.toString()); } - const url = `/v7/projects/${projectId}/env?${query}`; + const url = `/v8/projects/${projectId}/env?${query}`; return client.fetch<{ envs: ProjectEnvVariable[] }>(url); } diff --git a/packages/cli/src/util/env/remove-env-record.ts b/packages/cli/src/util/env/remove-env-record.ts index e8c796303..fdd05f508 100644 --- a/packages/cli/src/util/env/remove-env-record.ts +++ b/packages/cli/src/util/env/remove-env-record.ts @@ -10,7 +10,7 @@ export default async function removeEnvRecord( ): Promise { output.debug(`Removing Environment Variable ${env.key}`); - const urlProject = `/v7/projects/${projectId}/env/${env.id}`; + const urlProject = `/v8/projects/${projectId}/env/${env.id}`; await client.fetch(urlProject, { method: 'DELETE', diff --git a/packages/cli/src/util/input/input-project.ts b/packages/cli/src/util/input/input-project.ts index 20e816652..43a76c371 100644 --- a/packages/cli/src/util/input/input-project.ts +++ b/packages/cli/src/util/input/input-project.ts @@ -19,19 +19,24 @@ export default async function inputProject( // attempt to auto-detect a project to link let detectedProject = null; output.spinner('Searching for existing projects…', 1000); - try { - const [project, slugifiedProject] = await Promise.all([ - getProjectByIdOrName(client, detectedProjectName, org.id), - slugifiedName !== detectedProjectName - ? getProjectByIdOrName(client, slugifiedName, org.id) - : null, - ]); - detectedProject = !(project instanceof ProjectNotFound) - ? project - : !(slugifiedProject instanceof ProjectNotFound) - ? slugifiedProject - : null; - } catch (error) {} + + const [project, slugifiedProject] = await Promise.all([ + getProjectByIdOrName(client, detectedProjectName, org.id), + slugifiedName !== detectedProjectName + ? getProjectByIdOrName(client, slugifiedName, org.id) + : null, + ]); + + detectedProject = !(project instanceof ProjectNotFound) + ? project + : !(slugifiedProject instanceof ProjectNotFound) + ? slugifiedProject + : null; + + if (detectedProject && !detectedProject.id) { + throw new Error(`Detected linked project does not have "id".`); + } + output.stopSpinner(); if (autoConfirm) { diff --git a/packages/cli/src/util/projects/get-project-by-id-or-name.ts b/packages/cli/src/util/projects/get-project-by-id-or-name.ts index 5c1b1a944..c2e6950ce 100644 --- a/packages/cli/src/util/projects/get-project-by-id-or-name.ts +++ b/packages/cli/src/util/projects/get-project-by-id-or-name.ts @@ -9,7 +9,7 @@ export default async function getProjectByNameOrId( ) { try { const project = await client.fetch( - `/projects/${encodeURIComponent(projectNameOrId)}`, + `/v8/projects/${encodeURIComponent(projectNameOrId)}`, { accountId } ); return project; diff --git a/packages/cli/test/helpers/prepare.js b/packages/cli/test/helpers/prepare.js index 4bb98bf46..ffed8f380 100644 --- a/packages/cli/test/helpers/prepare.js +++ b/packages/cli/test/helpers/prepare.js @@ -168,6 +168,17 @@ module.exports = async function prepare(session, binaryPath) { 'vercel.json': JSON.stringify({ version: 2 }), 'README.md': 'readme contents', }, + 'deploy-default-with-sub-directory': { + 'vercel.json': JSON.stringify({ version: 2 }), + 'output/README.md': + 'readme contents for deploy-default-with-sub-directory', + }, + 'deploy-default-with-conflicting-sub-directory': { + 'list/vercel.json': JSON.stringify({ version: 2 }), + 'list/list/README.md': 'nested nested readme contents', + 'list/README.md': + 'readme contents for deploy-default-with-conflicting-sub-directory', + }, 'local-config-v2': { [`main-${session}.html`]: '

hello main

', [`test-${session}.html`]: '

hello test

', @@ -192,10 +203,6 @@ module.exports = async function prepare(session, binaryPath) { name: 'nested-level', }), }, - 'subdirectory-secret': { - 'index.html': 'Home page', - 'secret/file.txt': 'my secret', - }, 'build-secret': { 'package.json': JSON.stringify({ private: true, diff --git a/packages/cli/test/integration.js b/packages/cli/test/integration.js index 06a263058..e63993956 100644 --- a/packages/cli/test/integration.js +++ b/packages/cli/test/integration.js @@ -125,6 +125,19 @@ ${stdout} `; } +async function vcLink(t, projectPath) { + const { exitCode, stderr, stdout } = await execa( + binaryPath, + ['link', '--confirm', ...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 = {}; @@ -355,6 +368,8 @@ test('default command should prompt login with empty auth.json', async t => { } }); +// 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')); @@ -378,6 +393,110 @@ test('login', async t => { t.is(auth.token, token); }); +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 = /project +latest deployment +state +age +username/; + t.regex(stdout || '', 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 = /project +latest deployment +state +age +username/; + t.regex(stdout || '', 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('deploy using only now.json with `redirects` defined', async t => { const target = fixture('redirects-v2'); @@ -1088,30 +1207,6 @@ test('output the version', async t => { t.is(version, pkg.version); }); -test('should error with suggestion for secrets subcommand', async t => { - const target = fixture('subdirectory-secret'); - - const { exitCode, stderr, stdout } = await execa( - binaryPath, - ['secret', 'add', 'key', 'value', ...defaultArgs], - { - cwd: target, - reject: false, - } - ); - - console.log(stderr); - console.log(stdout); - console.log(exitCode); - - t.is(exitCode, 1); - t.regex( - stderr, - /secrets/gm, - `Expected "secrets" suggestion but received "${stderr}"` - ); -}); - test('should add secret with hyphen prefix', async t => { const target = fixture('build-secret'); const key = 'mysecret'; diff --git a/packages/cli/test/mocks/project.ts b/packages/cli/test/mocks/project.ts index f192dafd4..559ae09e3 100644 --- a/packages/cli/test/mocks/project.ts +++ b/packages/cli/test/mocks/project.ts @@ -77,13 +77,13 @@ export const defaultProject = { }; export function useProject(project = defaultProject) { - client.scenario.get(`/projects/${project.name}`, (_req, res) => { + client.scenario.get(`/v8/projects/${project.name}`, (_req, res) => { res.json(project); }); - client.scenario.get(`/projects/${project.id}`, (_req, res) => { + client.scenario.get(`/v8/projects/${project.id}`, (_req, res) => { res.json(project); }); - client.scenario.get(`/v7/projects/${project.id}/env`, (_req, res) => { + client.scenario.get(`/v8/projects/${project.id}/env`, (_req, res) => { res.json({ envs }); });