Compare commits

...

35 Commits

Author SHA1 Message Date
Matthew Stanciu
8ebe21ec09 Merge branch 'main' into add/vc-open 2022-07-08 10:31:04 -07:00
Matthew Stanciu
5e25067cac Remove test for no longer existing code 2022-07-08 10:27:34 -07:00
Matthew Stanciu
c76d787693 Remove "open a specific deployment url" 2022-07-08 10:21:15 -07:00
Matthew Stanciu
33c9f8a5df Fix alignment & use open 2022-07-08 09:58:44 -07:00
Matthew Stanciu
cdd1ce903f Merge branch 'main' into add/vc-open 2022-07-07 21:28:59 -07:00
Matthew Stanciu
2252e423d2 Merge branch 'main' into add/vc-open 2022-07-07 15:41:29 -07:00
Matthew Stanciu
78a96169b0 prod argument –> --prod flag 2022-07-07 15:40:32 -07:00
Matthew Stanciu
862f0bf9b6 Merge branch 'main' into add/vc-open 2022-07-06 16:15:29 -07:00
Matthew Stanciu
8044c492d8 Alias prod –> production, dash –> dashboard 2022-07-06 16:15:19 -07:00
Matthew Stanciu
14f6b4fc36 Merge branch 'main' into add/vc-open 2022-07-06 12:51:04 -07:00
Matthew Stanciu
e5faa2d61f Remove more reundant 'not_found's 2022-07-06 11:15:48 -07:00
Sean Massa
5160e357d7 Merge branch 'main' into add/vc-open 2022-07-06 13:12:53 -05:00
Matthew Stanciu
e56858ed2b Refactor
- Extract parts of the command into separate functions
- Defer requests until they are needed
- Small cleanup
2022-07-06 10:42:53 -07:00
Matthew Stanciu
b93d5dacd3 Fix tests depending on first one 2022-07-06 10:18:41 -07:00
Matthew Stanciu
2e1b19e54f project –> Project 2022-07-06 09:45:48 -07:00
Matthew Stanciu
1588a2c3ba Merge branch 'main' into add/vc-open 2022-07-05 14:47:08 -07:00
Matthew Stanciu
38e193617d Merge branch 'main' into add/vc-open 2022-07-05 12:26:04 -07:00
Matthew Stanciu
d799c3bb55 link-project –> ensure-link 2022-07-05 12:13:33 -07:00
Matthew Stanciu
9b070359fc Merge branch 'main' into add/vc-open 2022-07-05 11:52:23 -07:00
Matthew Stanciu
11f5b296e4 huh 2022-07-05 11:46:30 -07:00
Matthew Stanciu
a22e47c9f2 almonk suggestions
- Remove `--prod` and `inspect`
- Add `latest`, `prod`, and `[url]`
2022-07-05 11:34:14 -07:00
Matthew Stanciu
1ac1d73834 Add example to help 2022-07-02 00:06:53 -07:00
Matthew Stanciu
d2bc2a084d Fix spacing in help 2022-07-02 00:05:47 -07:00
Matthew Stanciu
968d832855 Combine 2 similar tests into 1 2022-07-01 23:58:41 -07:00
Matthew Stanciu
39cccaf0d4 Small refactoring + show commands in list 2022-07-01 20:18:13 -07:00
Matthew Stanciu
1fc799b801 Fix dumb typo 2022-07-01 17:31:32 -07:00
Matthew Stanciu
96772ad9c0 Accidentally put gitignore in the ignored folder 2022-07-01 17:20:55 -07:00
Matthew Stanciu
0f6d02f285 Track fixtures 2022-07-01 17:16:23 -07:00
Matthew Stanciu
fddb0e899e Merge branch 'main' into add/vc-open 2022-07-01 17:12:47 -07:00
Matthew Stanciu
b40c0bb07c Add tests 2022-07-01 17:09:00 -07:00
Matthew Stanciu
8402013311 Add open to global help 2022-07-01 16:21:52 -07:00
Matthew Stanciu
ec0fbda657 Add help page 2022-07-01 16:20:26 -07:00
Matthew Stanciu
23cccf0310 Add args 2022-07-01 16:14:05 -07:00
Matthew Stanciu
3ffee4e999 Add prod 2022-07-01 16:02:31 -07:00
Matthew Stanciu
2010f884ca Add basic command 2022-07-01 13:21:30 -07:00
25 changed files with 557 additions and 3 deletions

View File

@@ -52,6 +52,7 @@
"@vercel/remix": "1.0.6",
"@vercel/ruby": "1.3.13",
"@vercel/static-build": "1.0.5",
"open": "8.4.0",
"update-notifier": "5.1.0"
},
"devDependencies": {

View File

@@ -15,7 +15,7 @@ import getArgs from '../../util/get-args';
import Client from '../../util/client';
import { getPkgName } from '../../util/pkg-name';
import { Deployment, PaginationOptions } from '../../types';
import { normalizeURL } from '../../util/bisect/normalize-url';
import { normalizeURL } from '../../util/normalize-url';
interface DeploymentV6
extends Pick<

View File

@@ -21,6 +21,7 @@ export const help = () => `
link [path] Link local directory to a Vercel Project
login [email] Logs into your account or creates a new one
logout Logs out of your account
open [options] Opens a Project URL in your browser
pull [path] Pull your Project Settings from the cloud
switch [scope] Switches between teams and your personal account
help [cmd] Displays complete help for [cmd]

View File

@@ -25,6 +25,7 @@ export default new Map([
['logout', 'logout'],
['logs', 'logs'],
['ls', 'list'],
['open', 'open'],
['project', 'projects'],
['projects', 'projects'],
['pull', 'pull'],

View File

@@ -0,0 +1,319 @@
import chalk from 'chalk';
import Client from '../util/client';
import getArgs from '../util/get-args';
import getScope from '../util/get-scope';
import handleError from '../util/handle-error';
import logo from '../util/output/logo';
import { getCommandName, getPkgName } from '../util/pkg-name';
import validatePaths from '../util/validate-paths';
import { ensureLink } from '../util/ensure-link';
import list from '../util/input/list';
import { Org, Project, Team } from '../types';
import { stringify } from 'querystring';
import openUrl from 'open';
import link from '../util/output/link';
import { getDeployment } from '../util/get-deployment';
import { normalizeURL } from '../util/normalize-url';
import { emoji, prependEmoji } from '../util/emoji';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} open`)} [options]
${chalk.dim('Options:')}
-h, --help Output usage information
--confirm Skip confirmation prompts
--prod Filter for production deployments
dash Open the dashboard in a browser
latest Open the latest preview deployment URL in a browser
[url] Open the specified deployment URL in a browser
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
${chalk.dim('Examples:')}
${chalk.gray('')} View all options
${chalk.cyan(`$ ${getPkgName()} open`)}
${chalk.gray('')} Open the dashboard for the current project
${chalk.cyan(`$ ${getPkgName()} open dash`)}
${chalk.gray('')} Open the latest preview deployment URL
${chalk.cyan(`$ ${getPkgName()} open latest`)}
${chalk.gray('')} Open the latest production deployment URL
${chalk.cyan(`$ ${getPkgName()} open latest --prod`)}
${chalk.gray('')} Open the dashboard for the latest preview deployment
${chalk.cyan(`$ ${getPkgName()} open dash latest`)}
${chalk.gray('')} Open the dashboard for the latest production deployment
${chalk.cyan(`$ ${getPkgName()} open dash latest --prod`)}
${chalk.gray('')} Open the dashboard for a specific deployment URL
${chalk.cyan(`$ ${getPkgName()} open dash [url]`)}
`);
};
export default async function open(
client: Client,
test: Boolean
): Promise<number> {
const { output } = client;
let argv;
let subcommand: string | string[];
let narrow: string | string[];
try {
argv = getArgs(client.argv.slice(2), {
'--confirm': Boolean,
'--prod': Boolean,
});
} catch (error) {
handleError(error);
return 1;
}
argv._ = argv._.slice(1);
subcommand = argv._[0];
narrow = argv._[1];
if (argv['--help']) {
help();
return 2;
}
const confirm = argv['--confirm'] || false;
const prod = argv['--prod'] || false;
let scope = null;
try {
scope = await getScope(client);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const { team, contextName } = scope;
let paths = [process.cwd()];
const validate = await validatePaths(client, paths);
if (!validate.valid) {
return validate.exitCode;
}
const { path } = validate;
const linkedProject = await ensureLink('open', client, path, confirm);
if (typeof linkedProject === 'number') {
return linkedProject;
}
const { project, org } = linkedProject;
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
const choice = await getChoice(
subcommand,
narrow,
contextName,
client,
project,
org,
team,
prod
);
if (typeof choice === 'number') {
return choice;
}
if (choice === 'not_found') {
output.log(
`No deployments found. Run ${chalk.cyan(
getCommandName('deploy')
)} to create a deployment.`
);
return 1;
}
if (choice === '') {
// User aborted
return 0;
}
if (!test) openUrl(choice);
output.log(`🪄 Opened ${link(choice)}`);
return 0;
}
async function getChoice(
subcommand: string,
narrow: string,
contextName: string,
client: Client,
project: Project,
org: Org,
team: Team | null,
prod: Boolean
): Promise<string | number> {
if (subcommand === 'dash' || subcommand === 'dashboard') {
if (narrow === 'latest') {
return await getInspectorUrl(client, project, org, team, prod);
} else if (narrow) {
// Assume they're trying to pass in a deployment URL
const deployment = await verifyDeployment(client, narrow, contextName);
if (typeof deployment === 'number') {
return deployment;
}
return deployment.inspectorUrl;
} else {
return getDashboardUrl(org, project);
}
} else if (subcommand === 'latest') {
return await getLatestDeploymentUrl(client, project, team, prod);
} else {
if (subcommand) {
client.output.print(
prependEmoji('Unknown subcommand.\n', emoji('warning'))
);
}
return await listOptions(client, project, org, team);
}
}
async function listOptions(
client: Client,
project: Project,
org: Org,
team: Team | null
): Promise<string> {
return await list(client, {
message: 'What do you want to open?',
choices: [
{
name: `Dashboard ${chalk.gray('(vc open dash)')}`,
value: getDashboardUrl(org, project),
short: 'Dashboard',
},
{
name: `Latest Preview Deployment ${chalk.gray('(vc open latest)')}`,
value: await getLatestDeploymentUrl(client, project, team),
short: 'Latest Preview Deployment',
},
{
name: `Inspect Latest Preview Deployment ${chalk.gray(
'(vc open dash latest)'
)}`,
value: await getInspectorUrl(client, project, org, team),
short: 'Deployment Inspector',
},
{
name: `Latest Production Deployment ${chalk.gray(
'(vc open latest --prod)'
)}`,
value: await getLatestDeploymentUrl(client, project, team, true),
short: 'Latest Production Deployment',
},
{
name: `Inspect Latest Production Deployment ${chalk.gray(
'(vc open dash latest --prod)'
)}`,
value: await getInspectorUrl(client, project, org, team, true),
short: 'Latest Production Deployment Inspector',
},
],
});
}
async function verifyDeployment(
client: Client,
url: string,
contextName: string
) {
try {
const deployment = await getDeployment(client, url);
return {
url: normalizeURL(deployment.url),
inspectorUrl: deployment.inspectorUrl || '',
};
} catch (err) {
if (err.status === 404) {
client.output.error(
`Could not find a deployment with URL ${link(url)} in ${contextName}.`
);
}
return 1;
}
}
function getDashboardUrl(org: Org, project: Project): string {
return `https://vercel.com/${org.slug}/${project.name}`;
}
async function getInspectorUrl(
client: Client,
project: Project,
org: Org,
team: Team | null,
prod: Boolean = false
): Promise<string> {
const proj = await getProject(client, project, team);
if (proj) {
let latestDeploymentId = (
prod ? proj?.targets?.production?.id : proj.latestDeployments?.[0]?.id
)?.replace('dpl_', '');
if (latestDeploymentId) {
return `https://vercel.com/${org.slug}/${project.name}/${latestDeploymentId}`;
}
}
return 'not_found';
}
async function getLatestDeploymentUrl(
client: Client,
project: Project,
team: Team | null,
prod: Boolean = false
): Promise<string> {
const proj = await getProject(client, project, team);
if (prod && proj?.targets?.production) {
return `https://${proj.targets.production.url}`;
} else if (proj?.latestDeployments?.[0]?.url) {
return `https://${proj.latestDeployments[0].url}`;
}
return 'not_found';
}
async function getProject(
client: Client,
project: Project,
team: Team | null
): Promise<Project> {
const proj = await client
.fetch(
`/v9/projects/${project.name}?${stringify({
teamId: team?.id,
})}`
)
.catch(err => {
client.output.error(err.message);
return;
});
return proj as Project;
}

View File

@@ -653,6 +653,9 @@ const main = async () => {
case 'logout':
func = require('./commands/logout').default;
break;
case 'open':
func = require('./commands/open').default;
break;
case 'projects':
func = require('./commands/project').default;
break;

View File

@@ -117,6 +117,7 @@ export type Cert = {
export type Deployment = {
uid: string;
id?: string;
url: string;
name: string;
type: 'LAMBDAS';
@@ -256,6 +257,9 @@ export interface Project extends ProjectSettings {
createdAt: number;
alias?: ProjectAliasTarget[];
latestDeployments?: Partial<Deployment>[];
targets?: {
production?: Partial<Deployment>;
};
}
export interface Org {

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1 @@
{ "projectId": "test-project", "orgId": "team_dashboard" }

View File

@@ -0,0 +1 @@
<h1>hi</h1>

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1 @@
{ "projectId": "no-deployments", "orgId": "team_dummy" }

View File

@@ -0,0 +1 @@
<h1>hi</h1>

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1 @@
{ "projectId": "test-project", "orgId": "team_preview" }

View File

@@ -0,0 +1 @@
<h1>hi</h1>

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1 @@
{ "projectId": "test-project", "orgId": "team_prod" }

View File

@@ -0,0 +1 @@
<h1>hi</h1>

View File

@@ -24,6 +24,7 @@ export function useDeployment({
plan: 'hobby',
public: false,
version: 2,
buildingAt: Date.now(),
createdAt,
createdIn: 'sfo1',
ownerId: creator.id,

View File

@@ -96,7 +96,7 @@ export const defaultProject = {
requestedAt: 1571239348998,
target: 'production',
teamId: null,
type: 'LAMBDAS',
type: undefined,
url: 'a-project-name-rjtr4pz3f.vercel.app',
userId: 'K4amb7K9dAt5R2vBJWF32bmY',
},
@@ -131,6 +131,9 @@ export function useProject(project: Partial<Project> = defaultProject) {
client.scenario.get(`/v8/projects/${project.id}`, (_req, res) => {
res.json(project);
});
client.scenario.get(`/v9/projects/${project.id}`, (_req, res) => {
res.json(project);
});
client.scenario.get(
`/v6/projects/${project.id}/system-env-values`,
(_req, res) => {

View File

@@ -0,0 +1,208 @@
import { join } from 'path';
import { defaultProject, useProject } from '../../mocks/project';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
import open from '../../../src/commands/open';
import { client } from '../../mocks/client';
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/open', name);
describe('open', () => {
const originalCwd = process.cwd();
it('should open the dashboard', async () => {
const cwd = fixture('default');
try {
process.chdir(cwd);
useUser();
const team = useTeams('team_dashboard');
useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
client.setArgv('open', 'dash');
const openPromise = open(client, true);
await expect(client.stderr).toOutput(
`Opened https://vercel.com/${team[0].slug}/test-project`
);
const exitCode = await openPromise;
expect(exitCode).toEqual(0);
} finally {
process.chdir(originalCwd);
}
});
it('should open the preview inspect url', async () => {
const cwd = fixture('preview');
try {
process.chdir(cwd);
useUser();
const team = useTeams('team_preview');
const project = useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
const deploymentId = project?.project?.latestDeployments?.[0].id?.replace(
'dpl_',
''
);
client.setArgv('open', 'dash', 'latest');
const openPromise = open(client, true);
await expect(client.stderr).toOutput(
`Opened https://vercel.com/${team[0].slug}/test-project/${deploymentId}`
);
const exitCode = await openPromise;
expect(exitCode).toEqual(0);
} finally {
process.chdir(originalCwd);
}
});
it('should open the production inspect url', async () => {
const cwd = fixture('prod');
try {
process.chdir(cwd);
useUser();
const team = useTeams('team_prod');
const project = useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
const deploymentId = project?.project?.targets?.production?.id?.replace(
'dpl_',
''
);
client.setArgv('open', 'dash', 'latest', '--prod');
const openPromise = open(client, true);
await expect(client.stderr).toOutput(
`Opened https://vercel.com/${team[0].slug}/test-project/${deploymentId}`
);
const exitCode = await openPromise;
expect(exitCode).toEqual(0);
} finally {
process.chdir(originalCwd);
}
});
it('should open the preview deploy url', async () => {
const cwd = fixture('default');
try {
process.chdir(cwd);
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
const url = project?.project?.latestDeployments?.[0]?.url;
client.setArgv('open', 'latest');
const openPromise = open(client, true);
await expect(client.stderr).toOutput(`Opened https://${url}`);
const exitCode = await openPromise;
expect(exitCode).toEqual(0);
} finally {
process.chdir(originalCwd);
}
});
it('should open the production deploy url', async () => {
const cwd = fixture('default');
try {
process.chdir(cwd);
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
const url = project?.project?.targets?.production?.url;
client.setArgv('open', 'latest', '--prod');
const openPromise = open(client, true);
await expect(client.stderr).toOutput(`Opened https://${url}`);
const exitCode = await openPromise;
expect(exitCode).toEqual(0);
} finally {
process.chdir(originalCwd);
}
});
it('should open the latest preview deploy url from dropdown', async () => {
const cwd = fixture('default');
try {
process.chdir(cwd);
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
const url = project?.project?.latestDeployments?.[0]?.url;
client.setArgv('open');
const openPromise = open(client, true);
await expect(client.stderr).toOutput('What do you want to open?');
client.stdin.write('\x1B[B'); // down arrow
client.stdin.write('\r'); // return
await expect(client.stderr).toOutput(`Opened https://${url}`);
const exitCode = await openPromise;
expect(exitCode).toEqual(0);
} finally {
process.chdir(originalCwd);
}
});
it('should fail when there are no deployments', async () => {
const cwd = fixture('no-deployments');
try {
process.chdir(cwd);
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'no-deployments',
name: 'no-deployments',
});
project.project.latestDeployments = undefined;
client.setArgv('open', 'dash', 'latest');
const openPromiseInspect = open(client, true);
await expect(client.stderr).toOutput(
'No deployments found. Run `vercel deploy` to create a deployment.'
);
const exitCodeInspect = await openPromiseInspect;
expect(exitCodeInspect).toEqual(1);
client.setArgv('open', 'latest');
const openPromiseDeploy = open(client, true);
await expect(client.stderr).toOutput(
'No deployments found. Run `vercel deploy` to create a deployment.'
);
const exitCodeDeploy = await openPromiseDeploy;
expect(exitCodeDeploy).toEqual(1);
} finally {
process.chdir(originalCwd);
}
});
});

View File

@@ -1,4 +1,4 @@
import { normalizeURL } from '../../../../src/util/bisect/normalize-url';
import { normalizeURL } from '../../../../src/util/normalize-url';
describe('normalize-url', () => {
it('should add https to url without scheme', () => {

View File

@@ -63,6 +63,7 @@ export interface Deployment {
| 'ERROR';
createdAt: number;
createdIn: string;
inspectorUrl?: string;
buildingAt?: number;
creator?: {
uid?: string;