mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-07 12:57:47 +00:00
[cli] Add support for vc --cwd <dir> (#7577)
`vc`'s default command is `deploy`, which can lead to ambiguous cli invocations when running `vc dir-or-command` like: ``` $ vc list Vercel CLI 23.1.2 Error! The supplied argument "list" is ambiguous. If you wish to deploy the subdirectory "list", first run "cd list". ``` when run in a directory that contains a subdirectory "list". This conflict will happen with any current and future commands, like `vc build`. In order to make sure the CLI can be invoked either way, this PR deprecates the default behavior. Going forward, a user would see the following. **Conflicting Command, Run Command** ```bash $ vc list # warning: Did you mean to deploy the subdirectory "list"? Use `vc --cwd list` instead. # ... runs the `list` command ``` **Conflicting Command, Deploy Directory** ```bash $ vc --cwd list # ... deploy as normal ``` --- Card: https://linear.app/vercel/issue/BUI-33/prevent-ambiguous-vc-command-oror-dir
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -25,7 +25,9 @@ const ARG_COMMON = {
|
||||
|
||||
'--api': String,
|
||||
|
||||
'--target': String
|
||||
'--target': String,
|
||||
|
||||
'--cwd': String,
|
||||
};
|
||||
|
||||
export default () => ARG_COMMON;
|
||||
|
||||
2
packages/cli/src/util/env/add-env-record.ts
vendored
2
packages/cli/src/util/env/add-env-record.ts
vendored
@@ -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),
|
||||
|
||||
2
packages/cli/src/util/env/get-env-records.ts
vendored
2
packages/cli/src/util/env/get-env-records.ts
vendored
@@ -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);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function removeEnvRecord(
|
||||
): Promise<void> {
|
||||
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<ProjectEnvVariable>(urlProject, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
if (detectedProject && !detectedProject.id) {
|
||||
throw new Error(`Detected linked project does not have "id".`);
|
||||
}
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
if (autoConfirm) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async function getProjectByNameOrId(
|
||||
) {
|
||||
try {
|
||||
const project = await client.fetch<Project>(
|
||||
`/projects/${encodeURIComponent(projectNameOrId)}`,
|
||||
`/v8/projects/${encodeURIComponent(projectNameOrId)}`,
|
||||
{ accountId }
|
||||
);
|
||||
return project;
|
||||
|
||||
@@ -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`]: '<h1>hello main</h1>',
|
||||
[`test-${session}.html`]: '<h1>hello test</h1>',
|
||||
@@ -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,
|
||||
|
||||
143
packages/cli/test/integration.js
vendored
143
packages/cli/test/integration.js
vendored
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user