mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 12:57:47 +00:00
[cli] Refactor vc project (#8091)
Since the `vc project` command is about to be expanded with 2 new subcommands, I think it makes sense to do a little bit of refactoring. The current `vc project` command is all in one file, with the logic for subcommands nested within if statements. I think the structure of `vc project` should look more like `vc env`, which is consistent with how commands with subcommands look throughout the rest of the codebase.
This PR moves the logic for the `project` subcommands into their own files.
### 📋 Checklist
<!--
Please keep your PR as a Draft until the checklist is complete
-->
#### Tests
- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`
#### Code Review
- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
This commit is contained in:
55
packages/cli/src/commands/project/add.ts
Normal file
55
packages/cli/src/commands/project/add.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
export default async function add(
|
||||
client: Client,
|
||||
args: string[],
|
||||
contextName: string
|
||||
) {
|
||||
const { output } = client;
|
||||
if (args.length !== 1) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project add <name>')}`
|
||||
)}`
|
||||
);
|
||||
|
||||
if (args.length > 1) {
|
||||
const example = chalk.cyan(
|
||||
`${getCommandName(`project add "${args.join(' ')}"`)}`
|
||||
);
|
||||
output.log(
|
||||
`If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const [name] = args;
|
||||
try {
|
||||
await client.fetch('/projects', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
output.log(
|
||||
`${chalk.cyan('Success!')} Project ${chalk.bold(
|
||||
name.toLowerCase()
|
||||
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
111
packages/cli/src/commands/project/index.ts
Normal file
111
packages/cli/src/commands/project/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../../util/client';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import getScope from '../../util/get-scope';
|
||||
import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import validatePaths from '../../util/validate-paths';
|
||||
import add from './add';
|
||||
import list from './list';
|
||||
import rm from './rm';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} project`)} [options] <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
-S, --scope Set a custom scope
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Add a new project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} project add my-project`)}
|
||||
|
||||
${chalk.gray('–')} Paginate projects, where ${chalk.dim(
|
||||
'`1584722256178`'
|
||||
)} is the time in milliseconds since the UNIX epoch.
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} project ls --next 1584722256178`)}
|
||||
`);
|
||||
};
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
ls: ['ls', 'list'],
|
||||
add: ['add'],
|
||||
rm: ['rm', 'remove'],
|
||||
connect: ['connect'],
|
||||
};
|
||||
|
||||
export default async function main(client: Client) {
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
'--yes': Boolean,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
subcommand = argv._[0] || 'list';
|
||||
const args = argv._.slice(1);
|
||||
const { output } = client;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
const pathValidation = await validatePaths(client, paths);
|
||||
if (!pathValidation.valid) {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
let contextName = '';
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (error) {
|
||||
if (error.code === 'NOT_AUTHORIZED' || error.code === 'TEAM_DELETED') {
|
||||
output.error(error.message);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
switch (subcommand) {
|
||||
case 'ls':
|
||||
case 'list':
|
||||
return await list(client, argv, args, contextName);
|
||||
case 'add':
|
||||
return await add(client, args, contextName);
|
||||
case 'rm':
|
||||
case 'remove':
|
||||
return await rm(client, args);
|
||||
default:
|
||||
output.error(getInvalidSubcommand(COMMAND_CONFIG));
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
86
packages/cli/src/commands/project/list.ts
Normal file
86
packages/cli/src/commands/project/list.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import Client from '../../util/client';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import strlen from '../../util/strlen';
|
||||
|
||||
export default async function list(
|
||||
client: Client,
|
||||
argv: any,
|
||||
args: string[],
|
||||
contextName: string
|
||||
) {
|
||||
const { output } = client;
|
||||
if (args.length !== 0) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project ls')}`
|
||||
)}`
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
|
||||
|
||||
let projectsUrl = '/v4/projects/?limit=20';
|
||||
|
||||
const next = argv['--next'] || false;
|
||||
if (next) {
|
||||
projectsUrl += `&until=${next}`;
|
||||
}
|
||||
|
||||
const {
|
||||
projects: list,
|
||||
pagination,
|
||||
}: {
|
||||
projects: [{ name: string; updatedAt: number }];
|
||||
pagination: { count: number; next: number };
|
||||
} = await client.fetch(projectsUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
output.log(
|
||||
`${list.length > 0 ? 'Projects' : 'No projects'} found under ${chalk.bold(
|
||||
contextName
|
||||
)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
const cur = Date.now();
|
||||
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
|
||||
|
||||
const out = table(
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
output.print(`\n${out}\n\n`);
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
|
||||
const nextCmd = `project ls${flags} --next ${pagination.next}`;
|
||||
output.log(`To display the next page run ${getCommandName(nextCmd)}`);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
63
packages/cli/src/commands/project/rm.ts
Normal file
63
packages/cli/src/commands/project/rm.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
export default async function rm(client: Client, args: string[]) {
|
||||
if (args.length !== 1) {
|
||||
client.output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project rm <name>')}`
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const yes = await readConfirmation(client, name);
|
||||
|
||||
if (!yes) {
|
||||
client.output.log('User abort');
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
client.output.error('No such project exists');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
client.output.log(
|
||||
`${chalk.cyan('Success!')} Project ${chalk.bold(name)} removed ${chalk.gray(
|
||||
`[${elapsed}]`
|
||||
)}`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function readConfirmation(
|
||||
client: Client,
|
||||
projectName: string
|
||||
): Promise<boolean> {
|
||||
client.output.print(
|
||||
prependEmoji(
|
||||
`The project ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`,
|
||||
emoji('warning')
|
||||
)
|
||||
);
|
||||
|
||||
return await confirm(client, `${chalk.bold.red('Are you sure?')}`, false);
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import strlen from '../util/strlen';
|
||||
import getArgs from '../util/get-args';
|
||||
import { handleError, error } from '../util/error';
|
||||
import exit from '../util/exit';
|
||||
import logo from '../util/output/logo';
|
||||
import getScope from '../util/get-scope';
|
||||
import getCommandFlags from '../util/get-command-flags';
|
||||
import { getPkgName, getCommandName } from '../util/pkg-name';
|
||||
import Client from '../util/client';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} projects`)} [options] <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
-S, --scope Set a custom scope
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Add a new project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} projects add my-project`)}
|
||||
|
||||
${chalk.gray('–')} Paginate projects, where ${chalk.dim(
|
||||
'`1584722256178`'
|
||||
)} is the time in milliseconds since the UNIX epoch.
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} projects ls --next 1584722256178`)}
|
||||
`);
|
||||
};
|
||||
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
const main = async (client: Client) => {
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
|
||||
subcommand = argv._[0] || 'list';
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await run({ client, contextName });
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export default async (client: Client) => {
|
||||
try {
|
||||
await main(client);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
async function run({
|
||||
client,
|
||||
contextName,
|
||||
}: {
|
||||
client: Client;
|
||||
contextName: string;
|
||||
}) {
|
||||
const { output } = client;
|
||||
const args = argv._.slice(1);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
if (subcommand === 'ls' || subcommand === 'list') {
|
||||
if (args.length !== 0) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('projects ls')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
|
||||
|
||||
let projectsUrl = '/v4/projects/?limit=20';
|
||||
|
||||
const next = argv['--next'];
|
||||
if (next) {
|
||||
projectsUrl += `&until=${next}`;
|
||||
}
|
||||
|
||||
const {
|
||||
projects: list,
|
||||
pagination,
|
||||
}: {
|
||||
projects: [{ name: string; updatedAt: number }];
|
||||
pagination: { count: number; next: number };
|
||||
} = await client.fetch(projectsUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
console.log(
|
||||
`> ${
|
||||
list.length > 0 ? 'Projects' : 'No projects'
|
||||
} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
const cur = Date.now();
|
||||
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
|
||||
|
||||
const out = table(
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
console.log(`\n${out}\n`);
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
|
||||
const nextCmd = `projects ls${flags} --next ${pagination.next}`;
|
||||
console.log(`To display the next page run ${getCommandName(nextCmd)}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'rm' || subcommand === 'remove') {
|
||||
if (args.length !== 1) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project rm <name>')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const yes = await readConfirmation(name);
|
||||
if (!yes) {
|
||||
console.error(error('User abort'));
|
||||
return exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
console.error(error('No such project exists'));
|
||||
return exit(1);
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
name
|
||||
)} removed ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
if (args.length !== 1) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('projects add <name>')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
||||
if (args.length > 1) {
|
||||
const example = chalk.cyan(
|
||||
`${getCommandName(`projects add "${args.join(' ')}"`)}`
|
||||
);
|
||||
console.log(
|
||||
`> If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
|
||||
);
|
||||
}
|
||||
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const [name] = args;
|
||||
try {
|
||||
await client.fetch('/projects', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
name.toLowerCase()
|
||||
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error('Please specify a valid subcommand: ls | add | rm'));
|
||||
help();
|
||||
exit(2);
|
||||
}
|
||||
|
||||
process.on('uncaughtException', err => {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
function readConfirmation(projectName: string) {
|
||||
return new Promise(resolve => {
|
||||
process.stdout.write(
|
||||
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`
|
||||
);
|
||||
|
||||
process.stdout.write(
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
|
||||
);
|
||||
|
||||
process.stdin
|
||||
.on('data', d => {
|
||||
process.stdin.pause();
|
||||
resolve(d.toString().trim().toLowerCase() === 'y');
|
||||
})
|
||||
.resume();
|
||||
});
|
||||
}
|
||||
@@ -653,7 +653,7 @@ const main = async () => {
|
||||
func = require('./commands/logout').default;
|
||||
break;
|
||||
case 'projects':
|
||||
func = require('./commands/projects').default;
|
||||
func = require('./commands/project').default;
|
||||
break;
|
||||
case 'pull':
|
||||
func = require('./commands/pull').default;
|
||||
|
||||
19
packages/cli/test/helpers/parse-table.ts
Normal file
19
packages/cli/test/helpers/parse-table.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function getDataFromIntro(output: string): {
|
||||
project: string | undefined;
|
||||
org: string | undefined;
|
||||
} {
|
||||
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
|
||||
const org = output.match(/(?<=under )(.*)(?= \[)/);
|
||||
|
||||
return {
|
||||
project: project?.[0],
|
||||
org: org?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
export function parseTable(output: string): string[] {
|
||||
return output
|
||||
.trim()
|
||||
.replace(/ {1} +/g, ',')
|
||||
.split(',');
|
||||
}
|
||||
23
packages/cli/test/helpers/read-output-stream.ts
Normal file
23
packages/cli/test/helpers/read-output-stream.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { MockClient } from '../mocks/client';
|
||||
|
||||
export function readOutputStream(
|
||||
client: MockClient,
|
||||
length: number = 3
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const timeout = setTimeout(() => {
|
||||
reject();
|
||||
}, 3000);
|
||||
|
||||
client.stderr.resume();
|
||||
client.stderr.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
if (chunks.length === length) {
|
||||
clearTimeout(timeout);
|
||||
resolve(chunks.toString().replace(/,/g, ''));
|
||||
}
|
||||
});
|
||||
client.stderr.on('error', reject);
|
||||
});
|
||||
}
|
||||
2
packages/cli/test/integration.js
vendored
2
packages/cli/test/integration.js
vendored
@@ -3517,7 +3517,7 @@ test('`vc --debug project ls` should output the projects listing', async t => {
|
||||
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
t.true(
|
||||
stdout.includes('> Projects found under'),
|
||||
stderr.includes('> Projects found under'),
|
||||
formatOutput({ stderr, stdout })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -157,6 +157,64 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
||||
|
||||
res.json({ envs });
|
||||
});
|
||||
client.scenario.post(`/v4/projects/${project.id}/link`, (req, res) => {
|
||||
const { type, repo, org } = req.body;
|
||||
if (
|
||||
(type === 'github' || type === 'gitlab' || type === 'bitbucket') &&
|
||||
(repo === 'user/repo' || repo === 'user2/repo2')
|
||||
) {
|
||||
project.link = {
|
||||
type,
|
||||
repo,
|
||||
repoId: 1010,
|
||||
org,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
};
|
||||
res.json(project);
|
||||
} else {
|
||||
if (type === 'github') {
|
||||
res.status(400).json({
|
||||
message: `To link a GitHub repository, you need to install the GitHub integration first. (400)\nInstall GitHub App: https://github.com/apps/vercel`,
|
||||
meta: {
|
||||
action: 'Install GitHub App',
|
||||
link: 'https://github.com/apps/vercel',
|
||||
repo,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
code: 'repo_not_found',
|
||||
message: `The repository "${repo}" couldn't be found in your linked ${formatProvider(
|
||||
type
|
||||
)} account.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
client.scenario.delete(`/v4/projects/${project.id}/link`, (_req, res) => {
|
||||
if (project.link) {
|
||||
project.link = undefined;
|
||||
}
|
||||
res.json(project);
|
||||
});
|
||||
client.scenario.get(`/v4/projects`, (req, res) => {
|
||||
res.json({
|
||||
projects: [defaultProject],
|
||||
pagination: null,
|
||||
});
|
||||
});
|
||||
client.scenario.post(`/projects`, (req, res) => {
|
||||
const { name } = req.body;
|
||||
if (name === project.name) {
|
||||
res.json(project);
|
||||
}
|
||||
});
|
||||
client.scenario.delete(`/:version/projects/${project.id}`, (_req, res) => {
|
||||
res.json({});
|
||||
});
|
||||
|
||||
return { project, envs };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { client, MockClient } from '../../mocks/client';
|
||||
import { client } from '../../mocks/client';
|
||||
import { useUser } from '../../mocks/user';
|
||||
import list, { stateString } from '../../../src/commands/list';
|
||||
import { join } from 'path';
|
||||
import { useTeams } from '../../mocks/team';
|
||||
import { defaultProject, useProject } from '../../mocks/project';
|
||||
import { useDeployment } from '../../mocks/deployment';
|
||||
import { readOutputStream } from '../../helpers/read-output-stream';
|
||||
import { parseTable, getDataFromIntro } from '../../helpers/parse-table';
|
||||
|
||||
const fixture = (name: string) =>
|
||||
join(__dirname, '../../fixtures/unit/commands/list', name);
|
||||
@@ -98,42 +100,3 @@ describe('list', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getDataFromIntro(output: string): {
|
||||
project: string | undefined;
|
||||
org: string | undefined;
|
||||
} {
|
||||
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
|
||||
const org = output.match(/(?<=under )(.*)(?= \[)/);
|
||||
|
||||
return {
|
||||
project: project?.[0],
|
||||
org: org?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function parseTable(output: string): string[] {
|
||||
return output
|
||||
.trim()
|
||||
.replace(/ {3} +/g, ',')
|
||||
.split(',');
|
||||
}
|
||||
|
||||
function readOutputStream(client: MockClient): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const timeout = setTimeout(() => {
|
||||
reject();
|
||||
}, 3000);
|
||||
|
||||
client.stderr.resume();
|
||||
client.stderr.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
if (chunks.length === 3) {
|
||||
clearTimeout(timeout);
|
||||
resolve(chunks.toString().replace(/,/g, ''));
|
||||
}
|
||||
});
|
||||
client.stderr.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
94
packages/cli/test/unit/commands/project.test.ts
Normal file
94
packages/cli/test/unit/commands/project.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import projects from '../../../src/commands/project';
|
||||
import { useUser } from '../../mocks/user';
|
||||
import { useTeams } from '../../mocks/team';
|
||||
import { defaultProject, useProject } from '../../mocks/project';
|
||||
import { client } from '../../mocks/client';
|
||||
import { Project } from '../../../src/types';
|
||||
import { readOutputStream } from '../../helpers/read-output-stream';
|
||||
import { getDataFromIntro, parseTable } from '../../helpers/parse-table';
|
||||
|
||||
describe('projects', () => {
|
||||
describe('list', () => {
|
||||
it('should list deployments under a user', async () => {
|
||||
const user = useUser();
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
});
|
||||
|
||||
client.setArgv('project', 'ls');
|
||||
await projects(client);
|
||||
|
||||
const output = await readOutputStream(client, 2);
|
||||
const { org } = getDataFromIntro(output.split('\n')[0]);
|
||||
const header: string[] = parseTable(output.split('\n')[2]);
|
||||
const data: string[] = parseTable(output.split('\n')[3]);
|
||||
data.pop();
|
||||
|
||||
expect(org).toEqual(user.username);
|
||||
expect(header).toEqual(['name', 'updated']);
|
||||
expect(data).toEqual([project.project.name]);
|
||||
});
|
||||
it('should list deployments for a team', async () => {
|
||||
useUser();
|
||||
const team = useTeams('team_dummy');
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
});
|
||||
|
||||
client.config.currentTeam = team[0].id;
|
||||
client.setArgv('project', 'ls');
|
||||
await projects(client);
|
||||
|
||||
const output = await readOutputStream(client, 2);
|
||||
const { org } = getDataFromIntro(output.split('\n')[0]);
|
||||
const header: string[] = parseTable(output.split('\n')[2]);
|
||||
const data: string[] = parseTable(output.split('\n')[3]);
|
||||
data.pop();
|
||||
|
||||
expect(org).toEqual(team[0].slug);
|
||||
expect(header).toEqual(['name', 'updated']);
|
||||
expect(data).toEqual([project.project.name]);
|
||||
});
|
||||
});
|
||||
describe('add', () => {
|
||||
it('should add a project', async () => {
|
||||
const user = useUser();
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'test-project',
|
||||
name: 'test-project',
|
||||
});
|
||||
|
||||
client.setArgv('project', 'add', 'test-project');
|
||||
await projects(client);
|
||||
|
||||
const project: Project = await client.fetch(`/v8/projects/test-project`);
|
||||
expect(project).toBeDefined();
|
||||
|
||||
expect(client.stderr).toOutput(
|
||||
`Success! Project test-project added (${user.username})`
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('rm', () => {
|
||||
it('should remove a project', async () => {
|
||||
useUser();
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'test-project',
|
||||
name: 'test-project',
|
||||
});
|
||||
|
||||
client.setArgv('project', 'rm', 'test-project');
|
||||
const projectsPromise = projects(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`The project test-project will be removed permanently.`
|
||||
);
|
||||
client.stdin.write('y\n');
|
||||
|
||||
const exitCode = await projectsPromise;
|
||||
expect(exitCode).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user