mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-27 03:39:11 +00:00
Compare commits
19 Commits
@vercel/cl
...
@vercel/hy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1edc2d06c9 | ||
|
|
fdb15b2539 | ||
|
|
32ebcd83a7 | ||
|
|
2e43b2b88a | ||
|
|
f83d432fcd | ||
|
|
87fc38e860 | ||
|
|
afc4388fc0 | ||
|
|
3c48b40b43 | ||
|
|
ce89f00328 | ||
|
|
621b53bc49 | ||
|
|
728b620355 | ||
|
|
7d16395038 | ||
|
|
59e1259688 | ||
|
|
169242157e | ||
|
|
db10ffd679 | ||
|
|
c0d0744c4e | ||
|
|
9da67423a5 | ||
|
|
51fe09d5e9 | ||
|
|
695bfbdd60 |
@@ -19,6 +19,9 @@ packages/cli/src/util/dev/templates/*.ts
|
||||
packages/client/tests/fixtures
|
||||
packages/client/lib
|
||||
|
||||
# hydrogen
|
||||
packages/hydrogen/edge-entry.js
|
||||
|
||||
# next
|
||||
packages/next/test/integration/middleware
|
||||
packages/next/test/integration/middleware-eval
|
||||
|
||||
@@ -24,7 +24,6 @@ export function sendToVercelAnalytics(metric) {
|
||||
speed: getConnectionSpeed(),
|
||||
};
|
||||
|
||||
console.log({ body });
|
||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||
// This content type is necessary for `sendBeacon`
|
||||
type: 'application/x-www-form-urlencoded',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "4.2.1",
|
||||
"version": "5.0.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -31,7 +31,6 @@
|
||||
"@types/node-fetch": "^2.1.6",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "2.4.2",
|
||||
"@vercel/frameworks": "1.0.2",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"aggregate-error": "3.0.1",
|
||||
"async-retry": "1.2.3",
|
||||
|
||||
@@ -177,7 +177,7 @@ export async function getNodeBinPath({
|
||||
cwd,
|
||||
}: {
|
||||
cwd: string;
|
||||
}): Promise<string | undefined> {
|
||||
}): Promise<string> {
|
||||
const { code, stdout, stderr } = await execAsync('npm', ['bin'], {
|
||||
cwd,
|
||||
prettyCommand: 'npm bin',
|
||||
@@ -233,10 +233,23 @@ export function getSpawnOptions(
|
||||
};
|
||||
|
||||
if (!meta.isDev) {
|
||||
// Ensure that the selected Node version is at the beginning of the `$PATH`
|
||||
opts.env.PATH = `/node${nodeVersion.major}/bin${path.delimiter}${
|
||||
opts.env.PATH || process.env.PATH
|
||||
}`;
|
||||
let found = false;
|
||||
const oldPath = opts.env.PATH || process.env.PATH || '';
|
||||
|
||||
const pathSegments = oldPath.split(path.delimiter).map(segment => {
|
||||
if (/^\/node[0-9]+\/bin/.test(segment)) {
|
||||
found = true;
|
||||
return `/node${nodeVersion.major}/bin`;
|
||||
}
|
||||
return segment;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
// If we didn't find & replace, prepend at beginning of PATH
|
||||
pathSegments.unshift(`/node${nodeVersion.major}/bin`);
|
||||
}
|
||||
|
||||
opts.env.PATH = pathSegments.filter(Boolean).join(path.delimiter);
|
||||
}
|
||||
|
||||
return opts;
|
||||
@@ -474,20 +487,31 @@ export function getEnvForPackageManager({
|
||||
env: { [x: string]: string | undefined };
|
||||
}) {
|
||||
const newEnv: { [x: string]: string | undefined } = { ...env };
|
||||
const oldPath = env.PATH + '';
|
||||
const npm7 = '/node16/bin-npm7';
|
||||
const pnpm7 = '/pnpm7/node_modules/.bin';
|
||||
const corepackEnabled = env.ENABLE_EXPERIMENTAL_COREPACK === '1';
|
||||
if (cliType === 'npm') {
|
||||
if (
|
||||
typeof lockfileVersion === 'number' &&
|
||||
lockfileVersion >= 2 &&
|
||||
(nodeVersion?.major || 0) < 16
|
||||
(nodeVersion?.major || 0) < 16 &&
|
||||
!oldPath.includes(npm7) &&
|
||||
!corepackEnabled
|
||||
) {
|
||||
// Ensure that npm 7 is at the beginning of the `$PATH`
|
||||
newEnv.PATH = `/node16/bin-npm7${path.delimiter}${env.PATH}`;
|
||||
console.log('Detected `package-lock.json` generated by npm 7...');
|
||||
newEnv.PATH = `${npm7}${path.delimiter}${oldPath}`;
|
||||
console.log('Detected `package-lock.json` generated by npm 7+...');
|
||||
}
|
||||
} else if (cliType === 'pnpm') {
|
||||
if (typeof lockfileVersion === 'number' && lockfileVersion === 5.4) {
|
||||
if (
|
||||
typeof lockfileVersion === 'number' &&
|
||||
lockfileVersion === 5.4 &&
|
||||
!oldPath.includes(pnpm7) &&
|
||||
!corepackEnabled
|
||||
) {
|
||||
// Ensure that pnpm 7 is at the beginning of the `$PATH`
|
||||
newEnv.PATH = `/pnpm7/node_modules/.bin${path.delimiter}${env.PATH}`;
|
||||
newEnv.PATH = `${pnpm7}${path.delimiter}${oldPath}`;
|
||||
console.log('Detected `pnpm-lock.yaml` generated by pnpm 7...');
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -80,16 +80,6 @@ export {
|
||||
};
|
||||
|
||||
export { EdgeFunction } from './edge-function';
|
||||
export {
|
||||
detectBuilders,
|
||||
detectOutputDirectory,
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from './detect-builders';
|
||||
export { detectFileSystemAPI } from './detect-file-system-api';
|
||||
export { detectFramework } from './detect-framework';
|
||||
export { getProjectPaths } from './get-project-paths';
|
||||
export { DetectorFilesystem } from './detectors/filesystem';
|
||||
export { readConfigFile } from './fs/read-config-file';
|
||||
export { normalizePath } from './fs/normalize-path';
|
||||
|
||||
@@ -97,35 +87,3 @@ export * from './should-serve';
|
||||
export * from './schemas';
|
||||
export * from './types';
|
||||
export * from './errors';
|
||||
|
||||
/**
|
||||
* Helper function to support both `@vercel` and legacy `@now` official Runtimes.
|
||||
*/
|
||||
export const isOfficialRuntime = (desired: string, name?: string): boolean => {
|
||||
if (typeof name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
name === `@vercel/${desired}` ||
|
||||
name === `@now/${desired}` ||
|
||||
name.startsWith(`@vercel/${desired}@`) ||
|
||||
name.startsWith(`@now/${desired}@`)
|
||||
);
|
||||
};
|
||||
|
||||
export const isStaticRuntime = (name?: string): boolean => {
|
||||
return isOfficialRuntime('static', name);
|
||||
};
|
||||
|
||||
export { workspaceManagers } from './workspaces/workspace-managers';
|
||||
export {
|
||||
getWorkspaces,
|
||||
GetWorkspaceOptions,
|
||||
Workspace,
|
||||
WorkspaceType,
|
||||
} from './workspaces/get-workspaces';
|
||||
export {
|
||||
getWorkspacePackagePaths,
|
||||
GetWorkspacePackagePathsOptions,
|
||||
} from './workspaces/get-workspace-package-paths';
|
||||
export { monorepoManagers } from './monorepos/monorepo-managers';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# users.rb
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/users.rb": {
|
||||
"memory": 3008
|
||||
},
|
||||
"api/doesnt-exist.rb": {
|
||||
"memory": 768
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# [id].py
|
||||
@@ -1 +0,0 @@
|
||||
# index
|
||||
@@ -1 +0,0 @@
|
||||
# project/[aid]/[bid]/index.py
|
||||
@@ -1 +0,0 @@
|
||||
# get
|
||||
@@ -1 +0,0 @@
|
||||
# post
|
||||
@@ -1 +0,0 @@
|
||||
This file should also be included
|
||||
@@ -1 +0,0 @@
|
||||
# date
|
||||
@@ -1 +0,0 @@
|
||||
# math
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/users/post.py": {
|
||||
"memory": 3008
|
||||
}
|
||||
}
|
||||
}
|
||||
148
packages/build-utils/test/integration.test.ts
vendored
148
packages/build-utils/test/integration.test.ts
vendored
@@ -5,7 +5,6 @@ import {
|
||||
testDeployment,
|
||||
// @ts-ignore
|
||||
} from '../../../test/lib/deployment/test-deployment';
|
||||
import { glob, detectBuilders } from '../src';
|
||||
|
||||
jest.setTimeout(4 * 60 * 1000);
|
||||
|
||||
@@ -32,11 +31,6 @@ const skipFixtures: string[] = [
|
||||
'08-zero-config-middleman',
|
||||
'21-npm-workspaces',
|
||||
'23-pnpm-workspaces',
|
||||
'27-yarn-workspaces',
|
||||
'28-turborepo-with-yarn-workspaces',
|
||||
'29-nested-workspaces',
|
||||
'30-double-nested-workspaces',
|
||||
'31-turborepo-in-package-json',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@@ -83,145 +77,3 @@ for (const builder of buildersToTestWith) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('Test `detectBuilders` and `detectRoutes`', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
|
||||
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
|
||||
const fileList = await glob('**', fixture);
|
||||
const files = Object.keys(fileList);
|
||||
|
||||
const probes = [
|
||||
{
|
||||
path: '/api/my-endpoint',
|
||||
mustContain: 'my-endpoint',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/other-endpoint',
|
||||
mustContain: 'other-endpoint',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/team/zeit',
|
||||
mustContain: 'team/zeit',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/user/myself',
|
||||
mustContain: 'user/myself',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/not-okay/',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api/',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
mustContain: 'hello from index.txt',
|
||||
},
|
||||
];
|
||||
|
||||
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
|
||||
|
||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(fixture, 'now.json'),
|
||||
JSON.stringify(nowConfig, null, 2)
|
||||
);
|
||||
|
||||
const deployment = await testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
fixture
|
||||
);
|
||||
expect(deployment).toBeDefined();
|
||||
});
|
||||
|
||||
it('Test `detectBuilders` with `index` files', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
|
||||
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
|
||||
const fileList = await glob('**', fixture);
|
||||
const files = Object.keys(fileList);
|
||||
|
||||
const probes = [
|
||||
{
|
||||
path: '/api/not-okay',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/index',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/index.js',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date.js',
|
||||
mustContain: 'hello from api/date.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
// Someone might expect this to be `date.js`,
|
||||
// but I doubt that there is any case were both
|
||||
// `date/index.js` and `date.js` exists,
|
||||
// so it is not special cased
|
||||
path: '/api/date',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/index',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/index.js',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
mustContain: 'hello from index.txt',
|
||||
},
|
||||
];
|
||||
|
||||
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
|
||||
|
||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
||||
await fs.writeFile(
|
||||
path.join(fixture, 'now.json'),
|
||||
JSON.stringify(nowConfig, null, 2)
|
||||
);
|
||||
|
||||
const deployment = await testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
fixture
|
||||
);
|
||||
expect(deployment).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -38,6 +38,38 @@ describe('Test `getEnvForPackageManager()`', () => {
|
||||
PATH: `/node16/bin-npm7${delimiter}foo`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not set npm path if corepack enabled',
|
||||
args: {
|
||||
cliType: 'npm',
|
||||
nodeVersion: { major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||
lockfileVersion: 2,
|
||||
env: {
|
||||
FOO: 'bar',
|
||||
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||
},
|
||||
},
|
||||
want: {
|
||||
FOO: 'bar',
|
||||
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not prepend npm path again if already detected',
|
||||
args: {
|
||||
cliType: 'npm',
|
||||
nodeVersion: { major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||
lockfileVersion: 2,
|
||||
env: {
|
||||
FOO: 'bar',
|
||||
PATH: `/node16/bin-npm7${delimiter}foo`,
|
||||
},
|
||||
},
|
||||
want: {
|
||||
FOO: 'bar',
|
||||
PATH: `/node16/bin-npm7${delimiter}foo`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not set path if node is 16 and npm 7+ is detected',
|
||||
args: {
|
||||
@@ -101,6 +133,38 @@ describe('Test `getEnvForPackageManager()`', () => {
|
||||
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not set pnpm path if corepack is enabled',
|
||||
args: {
|
||||
cliType: 'pnpm',
|
||||
nodeVersion: { major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
lockfileVersion: 5.4,
|
||||
env: {
|
||||
FOO: 'bar',
|
||||
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||
},
|
||||
},
|
||||
want: {
|
||||
FOO: 'bar',
|
||||
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not prepend pnpm path again if already detected',
|
||||
args: {
|
||||
cliType: 'pnpm',
|
||||
nodeVersion: { major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
lockfileVersion: 5.4,
|
||||
env: {
|
||||
FOO: 'bar',
|
||||
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
||||
},
|
||||
},
|
||||
want: {
|
||||
FOO: 'bar',
|
||||
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not set path if pnpm 6 is detected',
|
||||
args: {
|
||||
|
||||
111
packages/build-utils/test/unit.get-spawn-options.test.ts
vendored
Normal file
111
packages/build-utils/test/unit.get-spawn-options.test.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
import { delimiter } from 'path';
|
||||
import { getSpawnOptions } from '../src';
|
||||
|
||||
describe('Test `getSpawnOptions()`', () => {
|
||||
const origProcessEnvPath = process.env.PATH;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.PATH = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.PATH = origProcessEnvPath;
|
||||
});
|
||||
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
args: Parameters<typeof getSpawnOptions>;
|
||||
envPath: string | undefined;
|
||||
want: string | undefined;
|
||||
}> = [
|
||||
{
|
||||
name: 'should do nothing when isDev and node14',
|
||||
args: [
|
||||
{ isDev: true },
|
||||
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||
],
|
||||
envPath: '/foo',
|
||||
want: '/foo',
|
||||
},
|
||||
{
|
||||
name: 'should do nothing when isDev and node16',
|
||||
args: [
|
||||
{ isDev: true },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: '/foo',
|
||||
want: '/foo',
|
||||
},
|
||||
{
|
||||
name: 'should replace 14 with 16 when only path',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: '/node14/bin',
|
||||
want: '/node16/bin',
|
||||
},
|
||||
{
|
||||
name: 'should replace 14 with 16 at beginning',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: `/node14/bin${delimiter}/foo`,
|
||||
want: `/node16/bin${delimiter}/foo`,
|
||||
},
|
||||
{
|
||||
name: 'should replace 14 with 16 at end',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: `/foo${delimiter}/node14/bin`,
|
||||
want: `/foo${delimiter}/node16/bin`,
|
||||
},
|
||||
{
|
||||
name: 'should replace 14 with 16 in middle',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: `/foo${delimiter}/node14/bin${delimiter}/bar`,
|
||||
want: `/foo${delimiter}/node16/bin${delimiter}/bar`,
|
||||
},
|
||||
{
|
||||
name: 'should prepend 16 at beginning when nothing to replace',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: `/foo`,
|
||||
want: `/node16/bin${delimiter}/foo`,
|
||||
},
|
||||
{
|
||||
name: 'should prepend 16 at beginning no path input',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
],
|
||||
envPath: '',
|
||||
want: `/node16/bin`,
|
||||
},
|
||||
{
|
||||
name: 'should replace 12 with 14 when only path',
|
||||
args: [
|
||||
{ isDev: false },
|
||||
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||
],
|
||||
envPath: '/node12/bin',
|
||||
want: '/node14/bin',
|
||||
},
|
||||
];
|
||||
|
||||
for (const { name, args, envPath, want } of cases) {
|
||||
it(name, () => {
|
||||
process.env.PATH = envPath;
|
||||
const opts = getSpawnOptions(...args);
|
||||
expect(opts.env?.PATH).toBe(want);
|
||||
});
|
||||
}
|
||||
});
|
||||
2
packages/build-utils/test/unit.test.ts
vendored
2
packages/build-utils/test/unit.test.ts
vendored
@@ -18,6 +18,8 @@ import {
|
||||
Meta,
|
||||
} from '../src';
|
||||
|
||||
jest.setTimeout(7 * 1000);
|
||||
|
||||
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
||||
let result;
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "25.2.3",
|
||||
"version": "26.0.1-canary.1",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -42,15 +42,15 @@
|
||||
"node": ">= 14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "4.2.1",
|
||||
"@vercel/go": "2.0.3",
|
||||
"@vercel/next": "3.1.2",
|
||||
"@vercel/node": "2.3.3",
|
||||
"@vercel/python": "3.0.3",
|
||||
"@vercel/redwood": "1.0.4",
|
||||
"@vercel/remix": "1.0.4",
|
||||
"@vercel/ruby": "1.3.11",
|
||||
"@vercel/static-build": "1.0.3",
|
||||
"@vercel/build-utils": "5.0.1-canary.0",
|
||||
"@vercel/go": "2.0.5-canary.0",
|
||||
"@vercel/next": "3.1.4-canary.1",
|
||||
"@vercel/node": "2.4.1-canary.0",
|
||||
"@vercel/python": "3.0.5-canary.0",
|
||||
"@vercel/redwood": "1.0.6-canary.0",
|
||||
"@vercel/remix": "1.0.6-canary.0",
|
||||
"@vercel/ruby": "1.3.13-canary.0",
|
||||
"@vercel/static-build": "1.0.5-canary.0",
|
||||
"update-notifier": "5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -95,8 +95,9 @@
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel/client": "12.0.3",
|
||||
"@vercel/client": "12.0.5-canary.0",
|
||||
"@vercel/frameworks": "1.0.2",
|
||||
"@vercel/fs-detectors": "1.0.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@zeit/fun": "0.11.2",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
@@ -113,7 +114,6 @@
|
||||
"chalk": "4.1.0",
|
||||
"chance": "1.1.7",
|
||||
"chokidar": "3.3.1",
|
||||
"clipboardy": "2.1.0",
|
||||
"codecov": "3.8.2",
|
||||
"cpy": "7.2.0",
|
||||
"credit-card": "3.0.1",
|
||||
|
||||
@@ -127,7 +127,7 @@ export default async function ({ creditCards, clear = false, contextName }) {
|
||||
}
|
||||
|
||||
console.log(''); // New line
|
||||
const stopSpinner = wait('Saving card');
|
||||
const stopSpinner = wait(process.stderr, 'Saving card');
|
||||
|
||||
try {
|
||||
const res = await creditCards.add({
|
||||
|
||||
@@ -174,7 +174,7 @@ export default async client => {
|
||||
)} ${chalk.gray(`[${elapsed}]`)}`;
|
||||
const choices = buildInquirerChoices(cards);
|
||||
|
||||
cardId = await listInput({
|
||||
cardId = await listInput(client, {
|
||||
message,
|
||||
choices,
|
||||
separator: true,
|
||||
@@ -251,7 +251,7 @@ export default async client => {
|
||||
)} under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`;
|
||||
const choices = buildInquirerChoices(cards);
|
||||
|
||||
cardId = await listInput({
|
||||
cardId = await listInput(client, {
|
||||
message,
|
||||
choices,
|
||||
separator: true,
|
||||
|
||||
@@ -14,7 +14,6 @@ import logo from '../../util/output/logo';
|
||||
import getArgs from '../../util/get-args';
|
||||
import Client from '../../util/client';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import { Output } from '../../util/output';
|
||||
import { Deployment, PaginationOptions } from '../../types';
|
||||
import { normalizeURL } from '../../util/bisect/normalize-url';
|
||||
|
||||
@@ -86,10 +85,10 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
let bad =
|
||||
argv['--bad'] ||
|
||||
(await prompt(output, `Specify a URL where the bug occurs:`));
|
||||
(await prompt(client, `Specify a URL where the bug occurs:`));
|
||||
let good =
|
||||
argv['--good'] ||
|
||||
(await prompt(output, `Specify a URL where the bug does not occur:`));
|
||||
(await prompt(client, `Specify a URL where the bug does not occur:`));
|
||||
let subpath = argv['--path'] || '';
|
||||
let run = argv['--run'] || '';
|
||||
const openEnabled = argv['--open'] || false;
|
||||
@@ -143,7 +142,7 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
if (!subpath) {
|
||||
subpath = await prompt(
|
||||
output,
|
||||
client,
|
||||
`Specify the URL subpath where the bug occurs:`
|
||||
);
|
||||
}
|
||||
@@ -391,10 +390,10 @@ function getCommit(deployment: DeploymentV6) {
|
||||
return { sha, message };
|
||||
}
|
||||
|
||||
async function prompt(output: Output, message: string): Promise<string> {
|
||||
async function prompt(client: Client, message: string): Promise<string> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { val } = await inquirer.prompt({
|
||||
const { val } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'val',
|
||||
message,
|
||||
@@ -402,7 +401,7 @@ async function prompt(output: Output, message: string): Promise<string> {
|
||||
if (val) {
|
||||
return val;
|
||||
} else {
|
||||
output.error('A value must be specified');
|
||||
client.output.error('A value must be specified');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import { join, normalize, relative, resolve } from 'path';
|
||||
import {
|
||||
detectBuilders,
|
||||
normalizePath,
|
||||
Files,
|
||||
FileFsRef,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
BuildResultV3,
|
||||
NowBuildError,
|
||||
} from '@vercel/build-utils';
|
||||
import { detectBuilders } from '@vercel/fs-detectors';
|
||||
import minimatch from 'minimatch';
|
||||
import {
|
||||
appendRoutesToPhase,
|
||||
|
||||
@@ -68,7 +68,6 @@ export const help = () => `
|
||||
-m, --meta Add metadata for the deployment (e.g.: ${chalk.dim(
|
||||
'`-m KEY=value`'
|
||||
)}). Can appear many times.
|
||||
-C, --no-clipboard Do not attempt to copy URL to clipboard
|
||||
-S, --scope Set a custom scope
|
||||
--regions Set default regions to enable the deployment on
|
||||
--prod Create a production deployment
|
||||
|
||||
@@ -10,7 +10,6 @@ import { readLocalConfig } from '../../util/config/files';
|
||||
import getArgs from '../../util/get-args';
|
||||
import { handleError } from '../../util/error';
|
||||
import Client from '../../util/client';
|
||||
import { write as copy } from 'clipboardy';
|
||||
import { getPrettyError } from '@vercel/build-utils';
|
||||
import toHumanPath from '../../util/humanize-path';
|
||||
import Now from '../../util';
|
||||
@@ -65,7 +64,7 @@ import { help } from './args';
|
||||
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
|
||||
import parseTarget from '../../util/deploy/parse-target';
|
||||
import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
|
||||
import { createGitMeta } from '../../util/deploy/create-git-meta';
|
||||
import { createGitMeta } from '../../util/create-git-meta';
|
||||
|
||||
export default async (client: Client) => {
|
||||
const { output } = client;
|
||||
@@ -77,7 +76,6 @@ export default async (client: Client) => {
|
||||
'--force': Boolean,
|
||||
'--with-cache': Boolean,
|
||||
'--public': Boolean,
|
||||
'--no-clipboard': Boolean,
|
||||
'--env': [String],
|
||||
'--build-env': [String],
|
||||
'--meta': [String],
|
||||
@@ -91,7 +89,6 @@ export default async (client: Client) => {
|
||||
'-p': '--public',
|
||||
'-e': '--env',
|
||||
'-b': '--build-env',
|
||||
'-C': '--no-clipboard',
|
||||
'-m': '--meta',
|
||||
'-c': '--confirm',
|
||||
|
||||
@@ -160,9 +157,9 @@ export default async (client: Client) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { log, debug, error, prettyError, isTTY } = output;
|
||||
const { log, debug, error, prettyError } = output;
|
||||
|
||||
const quiet = !isTTY;
|
||||
const quiet = !client.stdout.isTTY;
|
||||
|
||||
// check paths
|
||||
const pathValidation = await validatePaths(client, paths);
|
||||
@@ -686,13 +683,7 @@ export default async (client: Client) => {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return printDeploymentStatus(
|
||||
output,
|
||||
client,
|
||||
deployment,
|
||||
deployStamp,
|
||||
!argv['--no-clipboard']
|
||||
);
|
||||
return printDeploymentStatus(output, client, deployment, deployStamp);
|
||||
};
|
||||
|
||||
function handleCreateDeployError(
|
||||
@@ -825,8 +816,7 @@ const printDeploymentStatus = async (
|
||||
action?: string;
|
||||
};
|
||||
},
|
||||
deployStamp: () => string,
|
||||
isClipboardEnabled: boolean
|
||||
deployStamp: () => string
|
||||
) => {
|
||||
indications = indications || [];
|
||||
const isProdDeployment = target === 'production';
|
||||
@@ -847,40 +837,23 @@ const printDeploymentStatus = async (
|
||||
} else {
|
||||
// print preview/production url
|
||||
let previewUrl: string;
|
||||
let isWildcard: boolean;
|
||||
if (Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||
if (previewUrlInfo) {
|
||||
isWildcard = previewUrlInfo.isWildcard;
|
||||
previewUrl = previewUrlInfo.previewUrl;
|
||||
} else {
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
} else {
|
||||
// fallback to deployment url
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
|
||||
// copy to clipboard
|
||||
let isCopiedToClipboard = false;
|
||||
if (isClipboardEnabled && !isWildcard) {
|
||||
try {
|
||||
await copy(previewUrl);
|
||||
isCopiedToClipboard = true;
|
||||
} catch (err) {
|
||||
output.debug(`Error copyind to clipboard: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
output.print(
|
||||
prependEmoji(
|
||||
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
|
||||
previewUrl
|
||||
)}${
|
||||
isCopiedToClipboard ? chalk.gray(` [copied to clipboard]`) : ''
|
||||
} ${deployStamp()}`,
|
||||
)} ${deployStamp()}`,
|
||||
emoji('success')
|
||||
) + `\n`
|
||||
);
|
||||
|
||||
@@ -46,7 +46,11 @@ export default async function init(
|
||||
const exampleList = examples.filter(x => x.visible).map(x => x.name);
|
||||
|
||||
if (!name) {
|
||||
const chosen = await chooseFromDropdown('Select example:', exampleList);
|
||||
const chosen = await chooseFromDropdown(
|
||||
client,
|
||||
'Select example:',
|
||||
exampleList
|
||||
);
|
||||
|
||||
if (!chosen) {
|
||||
output.log('Aborted');
|
||||
@@ -90,14 +94,18 @@ async function fetchExampleList(client: Client) {
|
||||
/**
|
||||
* Prompt user for choosing which example to init
|
||||
*/
|
||||
async function chooseFromDropdown(message: string, exampleList: string[]) {
|
||||
async function chooseFromDropdown(
|
||||
client: Client,
|
||||
message: string,
|
||||
exampleList: string[]
|
||||
) {
|
||||
const choices = exampleList.map(name => ({
|
||||
name,
|
||||
value: name,
|
||||
short: name,
|
||||
}));
|
||||
|
||||
return listInput({
|
||||
return listInput(client, {
|
||||
message,
|
||||
choices,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import cmd from '../util/output/cmd';
|
||||
import logo from '../util/output/logo';
|
||||
import elapsed from '../util/output/elapsed';
|
||||
import strlen from '../util/strlen';
|
||||
import getScope from '../util/get-scope';
|
||||
import toHost from '../util/to-host';
|
||||
import parseMeta from '../util/parse-meta';
|
||||
import { isValidName } from '../util/is-valid-name';
|
||||
@@ -16,6 +15,10 @@ import getCommandFlags from '../util/get-command-flags';
|
||||
import { getPkgName, getCommandName } from '../util/pkg-name';
|
||||
import Client from '../util/client';
|
||||
import { Deployment } from '../types';
|
||||
import validatePaths from '../util/validate-paths';
|
||||
import { getLinkedProject } from '../util/projects/link';
|
||||
import { ensureLink } from '../util/ensure-link';
|
||||
import getScope from '../util/get-scope';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -31,6 +34,7 @@ const help = () => {
|
||||
'DIR'
|
||||
)} Path to the global ${'`.vercel`'} directory
|
||||
-d, --debug Debug mode [off]
|
||||
--confirm Skip the confirmation prompt
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
@@ -42,12 +46,14 @@ const help = () => {
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} List all deployments
|
||||
${chalk.gray('–')} List all deployments for the currently linked project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} ls`)}
|
||||
|
||||
${chalk.gray('–')} List all deployments for the app ${chalk.dim('`my-app`')}
|
||||
|
||||
${chalk.gray('–')} List all deployments for the project ${chalk.dim(
|
||||
'`my-app`'
|
||||
)} in the team of the currently linked project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} ls my-app`)}
|
||||
|
||||
${chalk.gray('–')} Filter deployments by metadata
|
||||
@@ -71,6 +77,7 @@ export default async function main(client: Client) {
|
||||
'-m': '--meta',
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
'--confirm': Boolean,
|
||||
});
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
@@ -86,18 +93,64 @@ export default async function main(client: Client) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let app: string | undefined = argv._[1];
|
||||
let host: string | undefined = undefined;
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
const meta = parseMeta(argv['--meta']);
|
||||
const { currentTeam, includeScheme } = config;
|
||||
const yes = argv['--confirm'] || false;
|
||||
|
||||
let contextName = null;
|
||||
const meta = parseMeta(argv['--meta']);
|
||||
const { includeScheme } = config;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
const pathValidation = await validatePaths(client, paths);
|
||||
if (!pathValidation.valid) {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
const { path } = pathValidation;
|
||||
|
||||
// retrieve `project` and `org` from .vercel
|
||||
let link = await getLinkedProject(client, path);
|
||||
|
||||
if (link.status === 'error') {
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
let { org, project, status } = link;
|
||||
const appArg: string | undefined = argv._[1];
|
||||
let app: string | undefined = appArg || project?.name;
|
||||
let host: string | undefined = undefined;
|
||||
|
||||
if (app && !isValidName(app)) {
|
||||
error(`The provided argument "${app}" is not a valid project name`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If there's no linked project and user doesn't pass `app` arg,
|
||||
// prompt to link their current directory.
|
||||
if (status === 'not_linked' && !app) {
|
||||
const linkedProject = await ensureLink('list', client, path, yes);
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
link.org = linkedProject.org;
|
||||
link.project = linkedProject.project;
|
||||
}
|
||||
|
||||
let { contextName, team } = await getScope(client);
|
||||
|
||||
// If user passed in a custom scope, update the current team & context name
|
||||
if (argv['--scope']) {
|
||||
client.config.currentTeam = team?.id || undefined;
|
||||
if (team?.slug) contextName = team.slug;
|
||||
} else {
|
||||
client.config.currentTeam = org?.type === 'team' ? org.id : undefined;
|
||||
if (org?.slug) contextName = org.slug;
|
||||
}
|
||||
|
||||
const { currentTeam } = config;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
@@ -152,6 +205,7 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
debug('Fetching deployments');
|
||||
|
||||
const response = await now.list(app, {
|
||||
version: 6,
|
||||
meta,
|
||||
@@ -194,17 +248,18 @@ export default async function main(client: Client) {
|
||||
deployments = deployments.filter(deployment => deployment.url === host);
|
||||
}
|
||||
|
||||
// we don't output the table headers if we have no deployments
|
||||
if (!deployments.length) {
|
||||
log(`No deployments found.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
log(
|
||||
`Deployments under ${chalk.bold(contextName)} ${elapsed(
|
||||
Date.now() - start
|
||||
)}`
|
||||
);
|
||||
|
||||
// we don't output the table headers if we have no deployments
|
||||
if (!deployments.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// information to help the user find other deployments or instances
|
||||
if (app == null) {
|
||||
log(
|
||||
@@ -216,7 +271,7 @@ export default async function main(client: Client) {
|
||||
|
||||
print('\n');
|
||||
|
||||
console.log(
|
||||
client.output.print(
|
||||
`${table(
|
||||
[
|
||||
['project', 'latest deployment', 'state', 'age', 'username'].map(
|
||||
@@ -247,7 +302,7 @@ export default async function main(client: Client) {
|
||||
hsep: ' '.repeat(4),
|
||||
stringLength: strlen,
|
||||
}
|
||||
).replace(/^/gm, ' ')}\n`
|
||||
).replace(/^/gm, ' ')}\n\n`
|
||||
);
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
@@ -270,7 +325,7 @@ function getProjectName(d: Deployment) {
|
||||
}
|
||||
|
||||
// renders the state string
|
||||
function stateString(s: string) {
|
||||
export function stateString(s: string) {
|
||||
switch (s) {
|
||||
case 'INITIALIZING':
|
||||
return chalk.yellow(s);
|
||||
|
||||
@@ -10,6 +10,20 @@ 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';
|
||||
import validatePaths from '../util/validate-paths';
|
||||
import { ensureLink } from '../util/ensure-link';
|
||||
import { parseGitConfig, pluckRemoteUrl } from '../util/create-git-meta';
|
||||
import {
|
||||
connectGitProvider,
|
||||
disconnectGitProvider,
|
||||
formatProvider,
|
||||
parseRepoUrl,
|
||||
} from '../util/projects/connect-git-provider';
|
||||
import { join } from 'path';
|
||||
import { Team, User } from '../types';
|
||||
import confirm from '../util/input/confirm';
|
||||
import { Output } from '../util/output';
|
||||
import link from '../util/output/link';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
@@ -20,6 +34,7 @@ const help = () => {
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
connect Connect a Git provider to your project
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
@@ -54,6 +69,7 @@ const main = async (client: Client) => {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
'--yes': Boolean,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
@@ -71,10 +87,10 @@ const main = async (client: Client) => {
|
||||
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
let scope = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
scope = await getScope(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
@@ -84,17 +100,12 @@ const main = async (client: Client) => {
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await run({ client, contextName });
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
}
|
||||
return await run({ client, scope });
|
||||
};
|
||||
|
||||
export default async (client: Client) => {
|
||||
try {
|
||||
await main(client);
|
||||
return await main(client);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
process.exit(1);
|
||||
@@ -103,16 +114,148 @@ export default async (client: Client) => {
|
||||
|
||||
async function run({
|
||||
client,
|
||||
contextName,
|
||||
scope,
|
||||
}: {
|
||||
client: Client;
|
||||
contextName: string;
|
||||
scope: {
|
||||
contextName: string;
|
||||
team: Team | null;
|
||||
user: User;
|
||||
};
|
||||
}) {
|
||||
const { output } = client;
|
||||
const { contextName, team } = scope;
|
||||
const args = argv._.slice(1);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
if (subcommand === 'connect') {
|
||||
const yes = Boolean(argv['--yes']);
|
||||
if (args.length !== 0) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project connect')}`
|
||||
)}`
|
||||
);
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
let paths = [process.cwd()];
|
||||
|
||||
const validate = await validatePaths(client, paths);
|
||||
if (!validate.valid) {
|
||||
return validate.exitCode;
|
||||
}
|
||||
const { path } = validate;
|
||||
|
||||
const linkedProject = await ensureLink(
|
||||
'project connect',
|
||||
client,
|
||||
path,
|
||||
yes
|
||||
);
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
|
||||
const { project, org } = linkedProject;
|
||||
const gitProviderLink = project.link;
|
||||
|
||||
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
|
||||
// get project from .git
|
||||
const gitConfigPath = join(path, '.git/config');
|
||||
const gitConfig = await parseGitConfig(gitConfigPath, output);
|
||||
if (!gitConfig) {
|
||||
output.error(
|
||||
`No local git repo found. Run ${chalk.cyan(
|
||||
'`git clone <url>`'
|
||||
)} to clone a remote Git repository first.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const remoteUrl = pluckRemoteUrl(gitConfig);
|
||||
if (!remoteUrl) {
|
||||
output.error(
|
||||
`No remote origin URL found in your Git config. Make sure you've connected your local Git repo to a Git provider first.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const parsedUrl = parseRepoUrl(remoteUrl);
|
||||
if (!parsedUrl) {
|
||||
output.error(
|
||||
`Failed to parse Git repo data from the following remote URL in your Git config: ${link(
|
||||
remoteUrl
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const { provider, org: gitOrg, repo } = parsedUrl;
|
||||
const repoPath = `${gitOrg}/${repo}`;
|
||||
let connectedRepoPath;
|
||||
|
||||
if (!gitProviderLink) {
|
||||
const connect = await connectGitProvider(
|
||||
client,
|
||||
team,
|
||||
project.id,
|
||||
provider,
|
||||
repoPath
|
||||
);
|
||||
if (typeof connect === 'number') {
|
||||
return connect;
|
||||
}
|
||||
} else {
|
||||
const connectedProvider = gitProviderLink.type;
|
||||
const connectedOrg = gitProviderLink.org;
|
||||
const connectedRepo = gitProviderLink.repo;
|
||||
connectedRepoPath = `${connectedOrg}/${connectedRepo}`;
|
||||
|
||||
const isSameRepo =
|
||||
connectedProvider === provider &&
|
||||
connectedOrg === gitOrg &&
|
||||
connectedRepo === repo;
|
||||
if (isSameRepo) {
|
||||
output.log(
|
||||
`${chalk.cyan(
|
||||
connectedRepoPath
|
||||
)} is already connected to your project.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const shouldReplaceRepo = await confirmRepoConnect(
|
||||
client,
|
||||
output,
|
||||
yes,
|
||||
connectedRepoPath
|
||||
);
|
||||
if (!shouldReplaceRepo) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
await disconnectGitProvider(client, team, project.id);
|
||||
const connect = await connectGitProvider(
|
||||
client,
|
||||
team,
|
||||
project.id,
|
||||
provider,
|
||||
repoPath
|
||||
);
|
||||
if (typeof connect === 'number') {
|
||||
return connect;
|
||||
}
|
||||
}
|
||||
|
||||
output.log(
|
||||
`Connected ${formatProvider(provider)} repository ${chalk.cyan(
|
||||
repoPath
|
||||
)}!`
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand === 'ls' || subcommand === 'list') {
|
||||
if (args.length !== 0) {
|
||||
console.error(
|
||||
@@ -271,7 +414,7 @@ async function run({
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error('Please specify a valid subcommand: ls | add | rm'));
|
||||
output.error('Please specify a valid subcommand: ls | connect | add | rm');
|
||||
help();
|
||||
exit(2);
|
||||
}
|
||||
@@ -281,6 +424,28 @@ process.on('uncaughtException', err => {
|
||||
exit(1);
|
||||
});
|
||||
|
||||
async function confirmRepoConnect(
|
||||
client: Client,
|
||||
output: Output,
|
||||
yes: boolean,
|
||||
connectedRepoPath: string
|
||||
) {
|
||||
let shouldReplaceProject = yes;
|
||||
if (!shouldReplaceProject) {
|
||||
shouldReplaceProject = await confirm(
|
||||
client,
|
||||
`Looks like you already have a repository connected: ${chalk.cyan(
|
||||
connectedRepoPath
|
||||
)}. Do you want to replace it?`,
|
||||
true
|
||||
);
|
||||
if (!shouldReplaceProject) {
|
||||
output.log(`Aborted. Repo not connected.`);
|
||||
}
|
||||
}
|
||||
return shouldReplaceProject;
|
||||
}
|
||||
|
||||
function readConfirmation(projectName: string) {
|
||||
return new Promise(resolve => {
|
||||
process.stdout.write(
|
||||
|
||||
@@ -83,7 +83,7 @@ export default async function main(client: Client, desiredSlug?: string) {
|
||||
];
|
||||
|
||||
output.stopSpinner();
|
||||
desiredSlug = await listInput({
|
||||
desiredSlug = await listInput(client, {
|
||||
message: 'Switch to:',
|
||||
choices,
|
||||
eraseFinalAnswer: true,
|
||||
|
||||
@@ -54,12 +54,12 @@ export default async (client: Client): Promise<number> => {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (output.isTTY) {
|
||||
if (client.stdout.isTTY) {
|
||||
output.log(contextName);
|
||||
} else {
|
||||
// If stdout is not a TTY, then only print the username
|
||||
// to support piping the output to another file / exe
|
||||
output.print(`${contextName}\n`, { w: process.stdout });
|
||||
client.stdout.write(`${contextName}\n`);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -23,7 +23,7 @@ import * as Sentry from '@sentry/node';
|
||||
import hp from './util/humanize-path';
|
||||
import commands from './commands';
|
||||
import pkg from './util/pkg';
|
||||
import createOutput from './util/output';
|
||||
import { Output } from './util/output';
|
||||
import cmd from './util/output/cmd';
|
||||
import info from './util/output/info';
|
||||
import error from './util/output/error';
|
||||
@@ -109,7 +109,7 @@ const main = async () => {
|
||||
}
|
||||
|
||||
const isDebugging = argv['--debug'];
|
||||
const output = createOutput({ debug: isDebugging });
|
||||
const output = new Output(process.stderr, { debug: isDebugging });
|
||||
|
||||
debug = output.debug;
|
||||
|
||||
@@ -389,6 +389,7 @@ const main = async () => {
|
||||
apiUrl,
|
||||
stdin: process.stdin,
|
||||
stdout: process.stdout,
|
||||
stderr: output.stream,
|
||||
output,
|
||||
config,
|
||||
authConfig,
|
||||
@@ -798,7 +799,5 @@ process.on('uncaughtException', handleUnexpected);
|
||||
main()
|
||||
.then(exitCode => {
|
||||
process.exitCode = exitCode;
|
||||
// @ts-ignore - "nowExit" is a non-standard event name
|
||||
process.emit('nowExit');
|
||||
})
|
||||
.catch(handleUnexpected);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Readable, Writable } from 'stream';
|
||||
|
||||
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
|
||||
|
||||
export type Primitive =
|
||||
@@ -128,6 +130,8 @@ export type Deployment = {
|
||||
version?: number;
|
||||
created: number;
|
||||
createdAt: number;
|
||||
ready?: number;
|
||||
buildingAt?: number;
|
||||
creator: { uid: string; username: string };
|
||||
target: string | null;
|
||||
ownerId: string;
|
||||
@@ -244,12 +248,34 @@ export interface ProjectEnvVariable {
|
||||
gitBranch?: string;
|
||||
}
|
||||
|
||||
export interface DeployHook {
|
||||
createdAt: number;
|
||||
id: string;
|
||||
name: string;
|
||||
ref: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ProjectLinkData {
|
||||
type: string;
|
||||
repo: string;
|
||||
repoId: number;
|
||||
org?: string;
|
||||
gitCredentialId: string;
|
||||
productionBranch?: string | null;
|
||||
sourceless: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
deployHooks?: DeployHook[];
|
||||
}
|
||||
|
||||
export interface Project extends ProjectSettings {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
updatedAt: number;
|
||||
createdAt: number;
|
||||
link?: ProjectLinkData;
|
||||
alias?: ProjectAliasTarget[];
|
||||
latestDeployments?: Partial<Deployment>[];
|
||||
}
|
||||
@@ -442,3 +468,19 @@ export interface BuildOutput {
|
||||
layers?: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ReadableTTY extends Readable {
|
||||
isTTY?: boolean;
|
||||
isRaw?: boolean;
|
||||
setRawMode?: (mode: boolean) => void;
|
||||
}
|
||||
|
||||
export interface WritableTTY extends Writable {
|
||||
isTTY?: boolean;
|
||||
}
|
||||
|
||||
export interface Stdio {
|
||||
stdin: ReadableTTY;
|
||||
stdout: WritableTTY;
|
||||
stderr: WritableTTY;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { bold } from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { EventEmitter } from 'events';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { parse as parseUrl } from 'url';
|
||||
@@ -11,10 +13,16 @@ import printIndications from './print-indications';
|
||||
import reauthenticate from './login/reauthenticate';
|
||||
import { SAMLError } from './login/types';
|
||||
import { writeToAuthConfigFile } from './config/files';
|
||||
import { AuthConfig, GlobalConfig, JSONObject } from '../types';
|
||||
import type {
|
||||
AuthConfig,
|
||||
GlobalConfig,
|
||||
JSONObject,
|
||||
Stdio,
|
||||
ReadableTTY,
|
||||
WritableTTY,
|
||||
} from '../types';
|
||||
import { sharedPromise } from './promise';
|
||||
import { APIError } from './errors-ts';
|
||||
import { bold } from 'chalk';
|
||||
|
||||
const isSAMLError = (v: any): v is SAMLError => {
|
||||
return v && v.saml;
|
||||
@@ -28,12 +36,10 @@ export interface FetchOptions extends Omit<RequestInit, 'body'> {
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
export interface ClientOptions extends Stdio {
|
||||
argv: string[];
|
||||
apiUrl: string;
|
||||
authConfig: AuthConfig;
|
||||
stdin: NodeJS.ReadStream;
|
||||
stdout: NodeJS.WriteStream;
|
||||
output: Output;
|
||||
config: GlobalConfig;
|
||||
localConfig?: VercelConfig;
|
||||
@@ -43,15 +49,17 @@ export const isJSONObject = (v: any): v is JSONObject => {
|
||||
return v && typeof v == 'object' && v.constructor === Object;
|
||||
};
|
||||
|
||||
export default class Client extends EventEmitter {
|
||||
export default class Client extends EventEmitter implements Stdio {
|
||||
argv: string[];
|
||||
apiUrl: string;
|
||||
authConfig: AuthConfig;
|
||||
stdin: NodeJS.ReadStream;
|
||||
stdout: NodeJS.WriteStream;
|
||||
stdin: ReadableTTY;
|
||||
stdout: WritableTTY;
|
||||
stderr: WritableTTY;
|
||||
output: Output;
|
||||
config: GlobalConfig;
|
||||
localConfig?: VercelConfig;
|
||||
prompt!: inquirer.PromptModule;
|
||||
private requestIdCounter: number;
|
||||
|
||||
constructor(opts: ClientOptions) {
|
||||
@@ -61,10 +69,12 @@ export default class Client extends EventEmitter {
|
||||
this.authConfig = opts.authConfig;
|
||||
this.stdin = opts.stdin;
|
||||
this.stdout = opts.stdout;
|
||||
this.stderr = opts.stderr;
|
||||
this.output = opts.output;
|
||||
this.config = opts.config;
|
||||
this.localConfig = opts.localConfig;
|
||||
this.requestIdCounter = 1;
|
||||
this._createPromptModule();
|
||||
}
|
||||
|
||||
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
|
||||
@@ -130,7 +140,7 @@ export default class Client extends EventEmitter {
|
||||
return this.retry(async bail => {
|
||||
const res = await this._fetch(url, opts);
|
||||
|
||||
printIndications(res);
|
||||
printIndications(this, res);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await responseError(res);
|
||||
@@ -186,4 +196,11 @@ export default class Client extends EventEmitter {
|
||||
_onRetry = (error: Error) => {
|
||||
this.output.debug(`Retrying: ${error}\n${error.stack}`);
|
||||
};
|
||||
|
||||
_createPromptModule() {
|
||||
this.prompt = inquirer.createPromptModule({
|
||||
input: this.stdin as NodeJS.ReadStream,
|
||||
output: this.stderr as NodeJS.WriteStream,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { join } from 'path';
|
||||
import ini from 'ini';
|
||||
import git from 'git-last-commit';
|
||||
import { exec } from 'child_process';
|
||||
import { GitMetadata } from '../../types';
|
||||
import { Output } from '../output';
|
||||
import { GitMetadata } from '../types';
|
||||
import { Output } from './output';
|
||||
|
||||
export function isDirty(directory: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -33,21 +33,31 @@ function getLastCommit(directory: string): Promise<git.Commit> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function parseGitConfig(configPath: string, output: Output) {
|
||||
try {
|
||||
return ini.parse(await fs.readFile(configPath, 'utf-8'));
|
||||
} catch (error) {
|
||||
output.debug(`Error while parsing repo data: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function pluckRemoteUrl(gitConfig: {
|
||||
[key: string]: any;
|
||||
}): string | undefined {
|
||||
// Assuming "origin" is the remote url that the user would want to use
|
||||
return gitConfig['remote "origin"']?.url;
|
||||
}
|
||||
|
||||
export async function getRemoteUrl(
|
||||
configPath: string,
|
||||
output: Output
|
||||
): Promise<string | null> {
|
||||
let gitConfig;
|
||||
try {
|
||||
gitConfig = ini.parse(await fs.readFile(configPath, 'utf-8'));
|
||||
} catch (error) {
|
||||
output.debug(`Error while parsing repo data: ${error.message}`);
|
||||
}
|
||||
let gitConfig = await parseGitConfig(configPath, output);
|
||||
if (!gitConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originUrl: string = gitConfig['remote "origin"']?.url;
|
||||
const originUrl = pluckRemoteUrl(gitConfig);
|
||||
if (originUrl) {
|
||||
return originUrl;
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
Lambda,
|
||||
FileBlob,
|
||||
FileFsRef,
|
||||
isOfficialRuntime,
|
||||
} from '@vercel/build-utils';
|
||||
import { isOfficialRuntime } from '@vercel/fs-detectors';
|
||||
import plural from 'pluralize';
|
||||
import minimatch from 'minimatch';
|
||||
|
||||
|
||||
@@ -36,12 +36,14 @@ import {
|
||||
StartDevServerResult,
|
||||
FileFsRef,
|
||||
PackageJson,
|
||||
spawnCommand,
|
||||
} from '@vercel/build-utils';
|
||||
import {
|
||||
detectBuilders,
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
spawnCommand,
|
||||
isOfficialRuntime,
|
||||
} from '@vercel/build-utils';
|
||||
} from '@vercel/fs-detectors';
|
||||
import frameworkList from '@vercel/frameworks';
|
||||
|
||||
import cmd from '../output/cmd';
|
||||
|
||||
44
packages/cli/src/util/ensure-link.ts
Normal file
44
packages/cli/src/util/ensure-link.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Org, Project } from '../types';
|
||||
import Client from './client';
|
||||
import setupAndLink from './link/setup-and-link';
|
||||
import param from './output/param';
|
||||
import { getCommandName } from './pkg-name';
|
||||
import { getLinkedProject } from './projects/link';
|
||||
|
||||
type LinkResult = {
|
||||
org: Org;
|
||||
project: Project;
|
||||
};
|
||||
export async function ensureLink(
|
||||
commandName: string,
|
||||
client: Client,
|
||||
cwd: string,
|
||||
yes: boolean
|
||||
): Promise<LinkResult | number> {
|
||||
let link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'not_linked') {
|
||||
link = await setupAndLink(client, cwd, {
|
||||
autoConfirm: yes,
|
||||
successEmoji: 'link',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
commandName
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
return { org: link.org, project: link.project };
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolve } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { getVercelIgnore } from '@vercel/client';
|
||||
import uniqueStrings from './unique-strings';
|
||||
import { Output } from './output/create-output';
|
||||
|
||||
@@ -530,7 +530,7 @@ export default class Now extends EventEmitter {
|
||||
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
|
||||
fetch(`${this._apiUrl}${_url}`, { ...opts, body })
|
||||
);
|
||||
printIndications(res);
|
||||
printIndications(this._client, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import inquirer from 'inquirer';
|
||||
import Client from '../client';
|
||||
|
||||
export default async function confirm(
|
||||
@@ -8,12 +7,7 @@ export default async function confirm(
|
||||
): Promise<boolean> {
|
||||
require('./patch-inquirer');
|
||||
|
||||
const prompt = inquirer.createPromptModule({
|
||||
input: client.stdin,
|
||||
output: client.stdout,
|
||||
});
|
||||
|
||||
const answers = await prompt({
|
||||
const answers = await client.prompt({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Client from '../client';
|
||||
import inquirer from 'inquirer';
|
||||
import confirm from './confirm';
|
||||
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
|
||||
import chalk from 'chalk';
|
||||
@@ -79,11 +78,7 @@ export default async function inputProject(
|
||||
let project: Project | ProjectNotFound | null = null;
|
||||
|
||||
while (!project || project instanceof ProjectNotFound) {
|
||||
const prompt = inquirer.createPromptModule({
|
||||
input: client.stdin,
|
||||
output: client.stdout,
|
||||
});
|
||||
const answers = await prompt({
|
||||
const answers = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'existingProjectName',
|
||||
message: `What’s the name of your existing project?`,
|
||||
@@ -114,7 +109,7 @@ export default async function inputProject(
|
||||
let newProjectName: string | null = null;
|
||||
|
||||
while (!newProjectName) {
|
||||
const answers = await inquirer.prompt({
|
||||
const answers = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'newProjectName',
|
||||
message: `What’s your project’s name?`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { validateRootDirectory } from '../validate-paths';
|
||||
import Client from '../client';
|
||||
|
||||
@@ -15,11 +14,7 @@ export async function inputRootDirectory(
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const prompt = inquirer.createPromptModule({
|
||||
input: client.stdin,
|
||||
output: client.stdout,
|
||||
});
|
||||
const { rootDirectory } = await prompt({
|
||||
const { rootDirectory } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'rootDirectory',
|
||||
message: `In which directory is your code located?`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import inquirer from 'inquirer';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import Client from '../client';
|
||||
import eraseLines from '../output/erase-lines';
|
||||
|
||||
interface ListEntry {
|
||||
@@ -35,21 +36,24 @@ function getLength(input: string): number {
|
||||
return biggestLength;
|
||||
}
|
||||
|
||||
export default async function list({
|
||||
message = 'the question',
|
||||
// eslint-disable-line no-unused-vars
|
||||
choices: _choices = [
|
||||
{
|
||||
name: 'something\ndescription\ndetails\netc',
|
||||
value: 'something unique',
|
||||
short: 'generally the first line of `name`',
|
||||
},
|
||||
],
|
||||
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
|
||||
separator = false, // Puts a blank separator between each choice
|
||||
abort = 'end', // Whether the `abort` option will be at the `start` or the `end`,
|
||||
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
|
||||
}: ListOptions): Promise<string> {
|
||||
export default async function list(
|
||||
client: Client,
|
||||
{
|
||||
message = 'the question',
|
||||
// eslint-disable-line no-unused-vars
|
||||
choices: _choices = [
|
||||
{
|
||||
name: 'something\ndescription\ndetails\netc',
|
||||
value: 'something unique',
|
||||
short: 'generally the first line of `name`',
|
||||
},
|
||||
],
|
||||
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
|
||||
separator = false, // Puts a blank separator between each choice
|
||||
abort = 'end', // Whether the `abort` option will be at the `start` or the `end`,
|
||||
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
|
||||
}: ListOptions
|
||||
): Promise<string> {
|
||||
require('./patch-inquirer-legacy');
|
||||
|
||||
let biggestLength = 0;
|
||||
@@ -106,7 +110,7 @@ export default async function list({
|
||||
choices.push(abortSeparator, _abort);
|
||||
}
|
||||
|
||||
const answer = await inquirer.prompt({
|
||||
const answer = await client.prompt({
|
||||
name: 'value',
|
||||
type: 'list',
|
||||
default: selected,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import chalk from 'chalk';
|
||||
import type { ReadableTTY, WritableTTY } from '../../types';
|
||||
|
||||
type Options = {
|
||||
abortSequences?: Set<string>;
|
||||
defaultValue?: boolean;
|
||||
noChar?: string;
|
||||
resolveChars?: Set<string>;
|
||||
stdin: NodeJS.ReadStream;
|
||||
stdout: NodeJS.WriteStream;
|
||||
stdin: ReadableTTY;
|
||||
stdout: WritableTTY;
|
||||
trailing?: string;
|
||||
yesChar?: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ReadableTTY } from '../../types';
|
||||
|
||||
export default async function readStandardInput(
|
||||
stdin: NodeJS.ReadStream
|
||||
stdin: ReadableTTY
|
||||
): Promise<string> {
|
||||
return new Promise<string>(resolve => {
|
||||
setTimeout(() => resolve(''), 500);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import inquirer from 'inquirer';
|
||||
import Client from '../client';
|
||||
import getUser from '../get-user';
|
||||
import getTeams from '../teams/get-teams';
|
||||
@@ -43,7 +42,7 @@ export default async function selectOrg(
|
||||
return choices[defaultOrgIndex].value;
|
||||
}
|
||||
|
||||
const answers = await inquirer.prompt({
|
||||
const answers = await client.prompt({
|
||||
type: 'list',
|
||||
name: 'org',
|
||||
message: question,
|
||||
|
||||
@@ -56,7 +56,7 @@ export default async function setupAndLink(
|
||||
return { status: 'error', exitCode: 1, reason: 'PATH_IS_FILE' };
|
||||
}
|
||||
const link = await getLinkedProject(client, path);
|
||||
const isTTY = process.stdout.isTTY;
|
||||
const isTTY = client.stdin.isTTY;
|
||||
const quiet = !isTTY;
|
||||
let rootDirectory: string | null = null;
|
||||
let sourceFilesOutsideRootDirectory = true;
|
||||
|
||||
@@ -176,7 +176,7 @@ async function getVerificationTokenOutOfBand(client: Client, url: URL) {
|
||||
output.log(
|
||||
`After login is complete, enter the verification code printed in your browser.`
|
||||
);
|
||||
const verificationToken = await readInput('Verification code:');
|
||||
const verificationToken = await readInput(client, 'Verification code:');
|
||||
output.print(eraseLines(6));
|
||||
|
||||
// If the pasted token begins with "saml_", then the `ssoUserId` was returned.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import inquirer from 'inquirer';
|
||||
import Client from '../client';
|
||||
import error from '../output/error';
|
||||
import listInput from '../input/list';
|
||||
@@ -32,7 +31,7 @@ export default async function prompt(
|
||||
choices.pop();
|
||||
}
|
||||
|
||||
const choice = await listInput({
|
||||
const choice = await listInput(client, {
|
||||
message: 'Log in to Vercel',
|
||||
choices,
|
||||
});
|
||||
@@ -44,22 +43,26 @@ export default async function prompt(
|
||||
} else if (choice === 'bitbucket') {
|
||||
result = await doBitbucketLogin(client, outOfBand, ssoUserId);
|
||||
} else if (choice === 'email') {
|
||||
const email = await readInput('Enter your email address:');
|
||||
const email = await readInput(client, 'Enter your email address:');
|
||||
result = await doEmailLogin(client, email, ssoUserId);
|
||||
} else if (choice === 'saml') {
|
||||
const slug = error?.teamId || (await readInput('Enter your Team slug:'));
|
||||
const slug =
|
||||
error?.teamId || (await readInput(client, 'Enter your Team slug:'));
|
||||
result = await doSamlLogin(client, slug, outOfBand, ssoUserId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function readInput(message: string): Promise<string> {
|
||||
export async function readInput(
|
||||
client: Client,
|
||||
message: string
|
||||
): Promise<string> {
|
||||
let input;
|
||||
|
||||
while (!input) {
|
||||
try {
|
||||
const { val } = await inquirer.prompt({
|
||||
const { val } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'val',
|
||||
message,
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
import chalk from 'chalk';
|
||||
import renderLink from './link';
|
||||
import wait, { StopSpinner } from './wait';
|
||||
import { Writable } from 'stream';
|
||||
import type { WritableTTY } from '../../types';
|
||||
|
||||
export interface OutputOptions {
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface PrintOptions {
|
||||
w?: Writable;
|
||||
}
|
||||
|
||||
export interface LogOptions extends PrintOptions {
|
||||
export interface LogOptions {
|
||||
color?: typeof chalk;
|
||||
}
|
||||
|
||||
export class Output {
|
||||
stream: WritableTTY;
|
||||
debugEnabled: boolean;
|
||||
private spinnerMessage: string;
|
||||
private _spinner: StopSpinner | null;
|
||||
isTTY: boolean;
|
||||
|
||||
constructor({ debug: debugEnabled = false }: OutputOptions = {}) {
|
||||
constructor(
|
||||
stream: WritableTTY,
|
||||
{ debug: debugEnabled = false }: OutputOptions = {}
|
||||
) {
|
||||
this.stream = stream;
|
||||
this.debugEnabled = debugEnabled;
|
||||
this.spinnerMessage = '';
|
||||
this._spinner = null;
|
||||
this.isTTY = process.stdout.isTTY || false;
|
||||
}
|
||||
|
||||
isDebugEnabled = () => {
|
||||
return this.debugEnabled;
|
||||
};
|
||||
|
||||
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
|
||||
print = (str: string) => {
|
||||
this.stopSpinner();
|
||||
const stream: Writable = w || process.stderr;
|
||||
stream.write(str);
|
||||
this.stream.write(str);
|
||||
};
|
||||
|
||||
log = (str: string, color = chalk.grey) => {
|
||||
@@ -111,11 +109,17 @@ export class Output {
|
||||
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
|
||||
return;
|
||||
}
|
||||
if (this.isTTY) {
|
||||
if (this.stream.isTTY) {
|
||||
if (this._spinner) {
|
||||
this._spinner.text = message;
|
||||
} else {
|
||||
this._spinner = wait(message, delay);
|
||||
this._spinner = wait(
|
||||
{
|
||||
text: message,
|
||||
stream: this.stream,
|
||||
},
|
||||
delay
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.print(`${message}\n`);
|
||||
@@ -157,7 +161,3 @@ export class Output {
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
export default function createOutput(opts?: OutputOptions) {
|
||||
return new Output(opts);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default, Output } from './create-output';
|
||||
export { Output } from './create-output';
|
||||
export { StopSpinner } from './wait';
|
||||
|
||||
@@ -8,14 +8,19 @@ export interface StopSpinner {
|
||||
}
|
||||
|
||||
export default function wait(
|
||||
msg: string,
|
||||
delay: number = 300,
|
||||
_ora = ora
|
||||
opts: ora.Options,
|
||||
delay: number = 300
|
||||
): StopSpinner {
|
||||
let spinner: ReturnType<typeof _ora> | null = null;
|
||||
let text = opts.text;
|
||||
let spinner: ora.Ora | null = null;
|
||||
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error(`"text" is required for Ora spinner`);
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
spinner = _ora(chalk.gray(msg));
|
||||
spinner = ora(opts);
|
||||
spinner.text = chalk.gray(text);
|
||||
spinner.color = 'gray';
|
||||
spinner.start();
|
||||
}, delay);
|
||||
@@ -29,23 +34,21 @@ export default function wait(
|
||||
}
|
||||
};
|
||||
|
||||
stop.text = msg;
|
||||
stop.text = text;
|
||||
|
||||
// Allow `text` property to update the text while the spinner is in action
|
||||
Object.defineProperty(stop, 'text', {
|
||||
get() {
|
||||
return msg;
|
||||
return text;
|
||||
},
|
||||
|
||||
set(v: string) {
|
||||
msg = v;
|
||||
text = v;
|
||||
if (spinner) {
|
||||
spinner.text = chalk.gray(v);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
process.once('nowExit', stop);
|
||||
return stop;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import { Response } from 'node-fetch';
|
||||
import { emoji, EmojiLabel, prependEmoji } from './emoji';
|
||||
import createOutput from './output';
|
||||
import Client from './client';
|
||||
import linkStyle from './output/link';
|
||||
import { emoji, EmojiLabel, prependEmoji } from './emoji';
|
||||
|
||||
export default function printIndications(res: Response) {
|
||||
const _output = createOutput();
|
||||
export default function printIndications(client: Client, res: Response) {
|
||||
const indications = new Set(['warning', 'notice', 'tip']);
|
||||
const regex = /^x-(?:vercel|now)-(warning|notice|tip)-(.*)$/;
|
||||
|
||||
@@ -25,7 +24,7 @@ export default function printIndications(res: Response) {
|
||||
chalk.dim(`${action || 'Learn More'}: ${linkStyle(link)}`) +
|
||||
newline;
|
||||
}
|
||||
_output.print(message + finalLink);
|
||||
client.output.print(message + finalLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
117
packages/cli/src/util/projects/connect-git-provider.ts
Normal file
117
packages/cli/src/util/projects/connect-git-provider.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import Client from '../client';
|
||||
import { stringify } from 'qs';
|
||||
import { Team } from '../../types';
|
||||
import chalk from 'chalk';
|
||||
import link from '../output/link';
|
||||
|
||||
export async function disconnectGitProvider(
|
||||
client: Client,
|
||||
team: Team | null,
|
||||
projectId: string
|
||||
) {
|
||||
const fetchUrl = `/v4/projects/${projectId}/link?${stringify({
|
||||
teamId: team?.id,
|
||||
})}`;
|
||||
return client.fetch(fetchUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function connectGitProvider(
|
||||
client: Client,
|
||||
team: Team | null,
|
||||
projectId: string,
|
||||
type: string,
|
||||
repo: string
|
||||
) {
|
||||
const fetchUrl = `/v4/projects/${projectId}/link?${stringify({
|
||||
teamId: team?.id,
|
||||
})}`;
|
||||
return client
|
||||
.fetch(fetchUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
repo,
|
||||
}),
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err.meta?.action === 'Install GitHub App' ||
|
||||
err.code === 'repo_not_found'
|
||||
) {
|
||||
client.output.error(
|
||||
`Failed to link ${chalk.cyan(
|
||||
repo
|
||||
)}. Make sure there aren't any typos and that you have access to the repository if it's private.`
|
||||
);
|
||||
} else if (err.action === 'Add a Login Connection') {
|
||||
client.output.error(
|
||||
err.message.replace(repo, chalk.cyan(repo)) +
|
||||
`\nVisit ${link(err.link)} for more information.`
|
||||
);
|
||||
} else {
|
||||
client.output.error(
|
||||
`Failed to connect the ${formatProvider(
|
||||
type
|
||||
)} repository ${repo}.\n${err}`
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
export function formatProvider(type: string): string {
|
||||
switch (type) {
|
||||
case 'github':
|
||||
return 'GitHub';
|
||||
case 'gitlab':
|
||||
return 'GitLab';
|
||||
case 'bitbucket':
|
||||
return 'Bitbucket';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRepoUrl(originUrl: string): {
|
||||
provider: string;
|
||||
org: string;
|
||||
repo: string;
|
||||
} | null {
|
||||
const isSSH = originUrl.startsWith('git@');
|
||||
// Matches all characters between (// or @) and (.com or .org)
|
||||
// eslint-disable-next-line prefer-named-capture-group
|
||||
const provider = /(?<=(\/\/|@)).*(?=(\.com|\.org))/.exec(originUrl);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let org;
|
||||
let repo;
|
||||
|
||||
if (isSSH) {
|
||||
org = originUrl.split(':')[1].split('/')[0];
|
||||
repo = originUrl.split('/')[1]?.replace('.git', '');
|
||||
} else {
|
||||
// Assume https:// or git://
|
||||
org = originUrl.split('/')[3];
|
||||
repo = originUrl.split('/')[4]?.replace('.git', '');
|
||||
}
|
||||
|
||||
if (!org || !repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: provider[0],
|
||||
org,
|
||||
repo,
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,15 @@ export const config = {
|
||||
};
|
||||
|
||||
export default function middleware(request, _event) {
|
||||
const response = new Response('middleware response');
|
||||
const url = new URL(request.url);
|
||||
|
||||
const response = new Response(
|
||||
JSON.stringify({
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
fromMiddleware: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Set custom header
|
||||
response.headers.set('x-modified-edge', 'true');
|
||||
|
||||
@@ -511,7 +511,20 @@ test(
|
||||
testFixtureStdio('middleware-matchers', async (testPath: any) => {
|
||||
await testPath(404, '/');
|
||||
await testPath(404, '/another');
|
||||
await testPath(200, '/about/page', 'middleware response');
|
||||
await testPath(200, '/dashboard/home', 'middleware response');
|
||||
await testPath(
|
||||
200,
|
||||
'/about/page',
|
||||
'{"pathname":"/about/page","search":"","fromMiddleware":true}'
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
'/dashboard/home',
|
||||
'{"pathname":"/dashboard/home","search":"","fromMiddleware":true}'
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
'/dashboard/home?a=b',
|
||||
'{"pathname":"/dashboard/home","search":"?a=b","fromMiddleware":true}'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -348,7 +348,6 @@ function testFixtureStdio(
|
||||
: []),
|
||||
'deploy',
|
||||
'--public',
|
||||
'--no-clipboard',
|
||||
'--debug',
|
||||
],
|
||||
{ cwd, stdio: 'pipe', reject: false }
|
||||
|
||||
1
packages/cli/test/fixtures/unit/commands/list/with-team/.gitignore
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/list/with-team/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.vercel
|
||||
4
packages/cli/test/fixtures/unit/commands/list/with-team/.vercel/project.json
vendored
Normal file
4
packages/cli/test/fixtures/unit/commands/list/with-team/.vercel/project.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"projectId": "with-team",
|
||||
"orgId": "team_dummy"
|
||||
}
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/bad-remote-url/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/bad-remote-url/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
10
packages/cli/test/fixtures/unit/commands/projects/connect/bad-remote-url/git/config
generated
vendored
Normal file
10
packages/cli/test/fixtures/unit/commands/projects/connect/bad-remote-url/git/config
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[remote "origin"]
|
||||
url = bababooey
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/bad-remote-url/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/bad-remote-url/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/existing-connection/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/existing-connection/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
10
packages/cli/test/fixtures/unit/commands/projects/connect/existing-connection/git/config
generated
vendored
Normal file
10
packages/cli/test/fixtures/unit/commands/projects/connect/existing-connection/git/config
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/user2/repo2
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/existing-connection/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/existing-connection/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/invalid-repo/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/invalid-repo/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
10
packages/cli/test/fixtures/unit/commands/projects/connect/invalid-repo/git/config
generated
vendored
Normal file
10
packages/cli/test/fixtures/unit/commands/projects/connect/invalid-repo/git/config
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/laksfj/asdgklsadkl
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/invalid-repo/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/invalid-repo/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/new-connection/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/new-connection/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
10
packages/cli/test/fixtures/unit/commands/projects/connect/new-connection/git/config
generated
vendored
Normal file
10
packages/cli/test/fixtures/unit/commands/projects/connect/new-connection/git/config
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/user/repo
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/new-connection/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/new-connection/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/no-git-config/file.txt
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/no-git-config/file.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hi
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/no-remote-url/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/no-remote-url/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
7
packages/cli/test/fixtures/unit/commands/projects/connect/no-remote-url/git/config
generated
vendored
Normal file
7
packages/cli/test/fixtures/unit/commands/projects/connect/no-remote-url/git/config
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/no-remote-url/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/no-remote-url/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/same-repo-connection/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/same-repo-connection/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
10
packages/cli/test/fixtures/unit/commands/projects/connect/same-repo-connection/git/config
generated
vendored
Normal file
10
packages/cli/test/fixtures/unit/commands/projects/connect/same-repo-connection/git/config
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/user/repo
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/same-repo-connection/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/same-repo-connection/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/.gitignore
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vercel
|
||||
1
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/git/HEAD
generated
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/git/HEAD
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
10
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/git/config
generated
vendored
Normal file
10
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/git/config
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/user/repo.git
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
6
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/git/info/exclude
generated
vendored
Normal file
6
packages/cli/test/fixtures/unit/commands/projects/connect/unlinked/git/info/exclude
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
35
packages/cli/test/integration.js
vendored
35
packages/cli/test/integration.js
vendored
@@ -553,8 +553,8 @@ test('default command should warn when deploying with conflicting subdirectory',
|
||||
/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
|
||||
const listHeader = /No deployments found/;
|
||||
t.regex(stderr || '', listHeader); // ensure `list` command still ran
|
||||
});
|
||||
|
||||
test('deploy command should not warn when deploying with conflicting subdirectory and using --cwd', async t => {
|
||||
@@ -577,8 +577,8 @@ test('deploy command should not warn when deploying with conflicting subdirector
|
||||
/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
|
||||
const listHeader = /No deployments found/;
|
||||
t.regex(stderr || '', listHeader); // ensure `list` command still ran
|
||||
});
|
||||
|
||||
test('default command should work with --cwd option', async t => {
|
||||
@@ -1813,31 +1813,6 @@ test('remove the wildcard alias', async t => {
|
||||
});
|
||||
*/
|
||||
|
||||
test('ensure username in list is right', async t => {
|
||||
const { stdout, stderr, exitCode } = await execa(
|
||||
binaryPath,
|
||||
['ls', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(exitCode);
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(exitCode, 0);
|
||||
|
||||
const line = stdout
|
||||
.split('\n')
|
||||
.find(line => line.includes('.now.sh') || line.includes('.vercel.app'));
|
||||
const columns = line.split(/\s+/);
|
||||
|
||||
// Ensure username column have username
|
||||
t.truthy(columns.pop().includes(contextName));
|
||||
});
|
||||
|
||||
test('ensure we render a warning for deployments with no files', async t => {
|
||||
const directory = fixture('empty-directory');
|
||||
|
||||
@@ -1958,7 +1933,7 @@ test('ensure we render a prompt when deploying home directory', async t => {
|
||||
t.is(exitCode, 0);
|
||||
|
||||
t.true(
|
||||
stdout.includes(
|
||||
stderr.includes(
|
||||
'You are deploying your home directory. Do you want to continue? [y/N]'
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Register Jest matcher extensions for CLI unit tests
|
||||
import './matchers';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { PassThrough } from 'stream';
|
||||
import { createServer, Server } from 'http';
|
||||
@@ -12,27 +15,43 @@ chalk.level = 0;
|
||||
|
||||
export type Scenario = Router;
|
||||
|
||||
class MockStream extends PassThrough {
|
||||
isTTY: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.isTTY = true;
|
||||
}
|
||||
|
||||
// These is for the `ora` module
|
||||
clearLine() {}
|
||||
cursorTo() {}
|
||||
}
|
||||
|
||||
export class MockClient extends Client {
|
||||
mockServer?: Server;
|
||||
mockOutput: jest.Mock<void, Parameters<Output['print']>>;
|
||||
private app: Express;
|
||||
stdin!: MockStream;
|
||||
stdout!: MockStream;
|
||||
stderr!: MockStream;
|
||||
scenario: Scenario;
|
||||
mockServer?: Server;
|
||||
private app: Express;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
argv: [],
|
||||
// Gets populated in `startMockServer()`
|
||||
apiUrl: '',
|
||||
|
||||
// Gets re-initialized for every test in `reset()`
|
||||
argv: [],
|
||||
authConfig: {},
|
||||
stdin: new PassThrough(),
|
||||
stdout: new PassThrough(),
|
||||
output: new Output(),
|
||||
config: {},
|
||||
localConfig: {},
|
||||
stdin: new PassThrough(),
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
output: new Output(new PassThrough()),
|
||||
});
|
||||
|
||||
this.mockOutput = jest.fn();
|
||||
|
||||
this.app = express();
|
||||
this.app.use(express.json());
|
||||
|
||||
@@ -57,33 +76,29 @@ export class MockClient extends Client {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.stdin = new PassThrough();
|
||||
this.stdin.isTTY = true;
|
||||
this.stdin = new MockStream();
|
||||
|
||||
this.stdout = new PassThrough();
|
||||
this.stdout.isTTY = true;
|
||||
this.stdout = new MockStream();
|
||||
this.stdout.setEncoding('utf8');
|
||||
this.stdout.end = () => {};
|
||||
this.stdout.pause();
|
||||
|
||||
this.output = new Output();
|
||||
this.mockOutput = jest.fn();
|
||||
this.output.print = s => {
|
||||
return this.mockOutput(s);
|
||||
};
|
||||
this.stderr = new MockStream();
|
||||
this.stderr.setEncoding('utf8');
|
||||
this.stderr.end = () => {};
|
||||
this.stderr.pause();
|
||||
this.stderr.isTTY = true;
|
||||
|
||||
this._createPromptModule();
|
||||
|
||||
this.output = new Output(this.stderr);
|
||||
|
||||
this.argv = [];
|
||||
this.authConfig = {};
|
||||
this.config = {};
|
||||
this.localConfig = {};
|
||||
|
||||
// Just make this one silent
|
||||
this.output.spinner = () => {};
|
||||
|
||||
this.scenario = Router();
|
||||
|
||||
this.output.isTTY = true;
|
||||
}
|
||||
|
||||
get outputBuffer() {
|
||||
return this.mockOutput.mock.calls.map(c => c[0]).join('');
|
||||
}
|
||||
|
||||
async startMockServer() {
|
||||
|
||||
@@ -7,7 +7,11 @@ import { Build, User } from '../../src/types';
|
||||
let deployments = new Map<string, Deployment>();
|
||||
let deploymentBuilds = new Map<Deployment, Build[]>();
|
||||
|
||||
export function useDeployment({ creator }: { creator: Pick<User, 'id'> }) {
|
||||
export function useDeployment({
|
||||
creator,
|
||||
}: {
|
||||
creator: Pick<User, 'id' | 'email' | 'name'>;
|
||||
}) {
|
||||
const createdAt = Date.now();
|
||||
const url = new URL(chance().url());
|
||||
const deployment: Deployment = {
|
||||
@@ -23,6 +27,11 @@ export function useDeployment({ creator }: { creator: Pick<User, 'id'> }) {
|
||||
createdAt,
|
||||
createdIn: 'sfo1',
|
||||
ownerId: creator.id,
|
||||
creator: {
|
||||
uid: creator.id,
|
||||
email: creator.email,
|
||||
username: creator.name,
|
||||
},
|
||||
readyState: 'READY',
|
||||
env: {},
|
||||
build: { env: {} },
|
||||
@@ -77,4 +86,9 @@ beforeEach(() => {
|
||||
const builds = deploymentBuilds.get(deployment);
|
||||
res.json({ builds });
|
||||
});
|
||||
|
||||
client.scenario.get('/:version/now/deployments', (req, res) => {
|
||||
const deploymentsList = Array.from(deployments.values());
|
||||
res.json({ deployments: deploymentsList });
|
||||
});
|
||||
});
|
||||
|
||||
65
packages/cli/test/mocks/matchers/index.ts
Normal file
65
packages/cli/test/mocks/matchers/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* This file registers the custom Jest "matchers" that are useful for
|
||||
* writing CLI unit tests, and sets them up to be recognized by TypeScript.
|
||||
*
|
||||
* References:
|
||||
* - https://haspar.us/notes/adding-jest-custom-matchers-in-typescript
|
||||
* - https://gist.github.com/hasparus/4ebaa17ec5d3d44607f522bcb1cda9fb
|
||||
*/
|
||||
|
||||
/// <reference types="@types/jest" />
|
||||
|
||||
import * as matchers from './matchers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Tail<T extends unknown[]> = T extends [infer _Head, ...infer Tail]
|
||||
? Tail
|
||||
: never;
|
||||
|
||||
type AnyFunction = (...args: any[]) => any;
|
||||
type PromiseFunction = (...args: any[]) => Promise<any>;
|
||||
|
||||
type GetMatcherType<TP, TResult> = TP extends PromiseFunction
|
||||
? (...args: Tail<Parameters<TP>>) => Promise<TResult>
|
||||
: TP extends AnyFunction
|
||||
? (...args: Tail<Parameters<TP>>) => TResult
|
||||
: TP;
|
||||
|
||||
//type T = GetMatcherType<typeof matchers['toOutput'], void>;
|
||||
|
||||
type GetMatchersType<TMatchers, TResult> = {
|
||||
[P in keyof TMatchers]: GetMatcherType<TMatchers[P], TResult>;
|
||||
};
|
||||
|
||||
type FirstParam<T extends AnyFunction> = Parameters<T>[0];
|
||||
|
||||
type OnlyMethodsWhereFirstArgIsOfType<TObject, TWantedFirstArg> = {
|
||||
[P in keyof TObject]: TObject[P] extends AnyFunction
|
||||
? TWantedFirstArg extends FirstParam<TObject[P]>
|
||||
? TObject[P]
|
||||
: [
|
||||
`Error: this function is present only when received is:`,
|
||||
FirstParam<TObject[P]>
|
||||
]
|
||||
: TObject[P];
|
||||
};
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Matchers<R, T = {}>
|
||||
extends GetMatchersType<
|
||||
OnlyMethodsWhereFirstArgIsOfType<typeof matchers, T>,
|
||||
R
|
||||
> {}
|
||||
}
|
||||
}
|
||||
|
||||
const jestExpect = (global as any).expect;
|
||||
|
||||
if (jestExpect !== undefined) {
|
||||
jestExpect.extend(matchers);
|
||||
} else {
|
||||
console.error("Couldn't find Jest's global expect.");
|
||||
}
|
||||
1
packages/cli/test/mocks/matchers/matchers.ts
Normal file
1
packages/cli/test/mocks/matchers/matchers.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './to-output';
|
||||
67
packages/cli/test/mocks/matchers/to-output.ts
Normal file
67
packages/cli/test/mocks/matchers/to-output.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
getLabelPrinter,
|
||||
matcherHint,
|
||||
printExpected,
|
||||
printReceived,
|
||||
} from 'jest-matcher-utils';
|
||||
import type { Readable } from 'stream';
|
||||
import type { MatcherState } from 'expect';
|
||||
import type { MatcherHintOptions } from 'jest-matcher-utils';
|
||||
|
||||
export async function toOutput(
|
||||
this: MatcherState,
|
||||
stream: Readable,
|
||||
test: string,
|
||||
timeout = 3000
|
||||
) {
|
||||
const { isNot } = this;
|
||||
const matcherName = 'toOutput';
|
||||
const matcherHintOptions: MatcherHintOptions = {
|
||||
isNot,
|
||||
promise: this.promise,
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
let output = '';
|
||||
let timeoutId = setTimeout(onTimeout, timeout);
|
||||
|
||||
const message = () => {
|
||||
const labelExpected = 'Expected output';
|
||||
const labelReceived = 'Received output';
|
||||
const printLabel = getLabelPrinter(labelExpected, labelReceived);
|
||||
const hint =
|
||||
matcherHint(matcherName, 'stream', 'test', matcherHintOptions) + '\n\n';
|
||||
return (
|
||||
hint +
|
||||
printLabel(labelExpected) +
|
||||
(isNot ? 'not ' : '') +
|
||||
printExpected(test) +
|
||||
'\n' +
|
||||
printLabel(labelReceived) +
|
||||
(isNot ? ' ' : '') +
|
||||
printReceived(output)
|
||||
);
|
||||
};
|
||||
|
||||
function onData(data: string) {
|
||||
output += data;
|
||||
if (output.includes(test)) {
|
||||
cleanup();
|
||||
resolve({ pass: true, message });
|
||||
}
|
||||
}
|
||||
|
||||
function onTimeout() {
|
||||
cleanup();
|
||||
resolve({ pass: false, message });
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
clearTimeout(timeoutId);
|
||||
stream.removeListener('data', onData);
|
||||
stream.pause();
|
||||
}
|
||||
|
||||
stream.on('data', onData);
|
||||
stream.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { client } from './client';
|
||||
import { Project } from '../../src/types';
|
||||
import { formatProvider } from '../../src/util/projects/connect-git-provider';
|
||||
|
||||
const envs = [
|
||||
{
|
||||
@@ -157,6 +158,49 @@ 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);
|
||||
});
|
||||
|
||||
return { project, envs };
|
||||
}
|
||||
|
||||
@@ -10,38 +10,38 @@ import { useUser } from '../../mocks/user';
|
||||
describe('deploy', () => {
|
||||
it('should reject deploying a single file', async () => {
|
||||
client.setArgv('deploy', __filename);
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! Support for single file deployments has been removed.\nLearn More: https://vercel.link/no-single-file-deployments\n`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying multiple files', async () => {
|
||||
client.setArgv('deploy', __filename, join(__dirname, 'inspect.test.ts'));
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! Can't deploy more than one path.\n`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying a directory that does not exist', async () => {
|
||||
client.setArgv('deploy', 'does-not-exists');
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! The specified file or directory "does-not-exists" does not exist.\n`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying a directory that does not contain ".vercel/output" when `--prebuilt` is used', async () => {
|
||||
client.setArgv('deploy', __dirname, '--prebuilt');
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Error! The "--prebuilt" option was used, but no prebuilt output found in ".vercel/output". Run `vercel build` to generate a local build.\n'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying a directory that was built with a different target environment when `--prebuilt --prod` is used on "preview" output', async () => {
|
||||
@@ -56,14 +56,14 @@ describe('deploy', () => {
|
||||
});
|
||||
|
||||
client.setArgv('deploy', cwd, '--prebuilt', '--prod');
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Error! The "--prebuilt" option was used with the target environment "production",' +
|
||||
' but the prebuilt output found in ".vercel/output" was built with target environment "preview".' +
|
||||
' Please run `vercel --prebuilt`.\n' +
|
||||
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying a directory that was built with a different target environment when `--prebuilt` is used on "production" output', async () => {
|
||||
@@ -78,14 +78,14 @@ describe('deploy', () => {
|
||||
});
|
||||
|
||||
client.setArgv('deploy', cwd, '--prebuilt');
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Error! The "--prebuilt" option was used with the target environment "preview",' +
|
||||
' but the prebuilt output found in ".vercel/output" was built with target environment "production".' +
|
||||
' Please run `vercel --prebuilt --prod`.\n' +
|
||||
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying "version: 1"', async () => {
|
||||
@@ -94,11 +94,11 @@ describe('deploy', () => {
|
||||
[fileNameSymbol]: 'vercel.json',
|
||||
version: 1,
|
||||
};
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Error! The value of the `version` property within vercel.json can only be `2`.\n'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should reject deploying "version: {}"', async () => {
|
||||
@@ -108,10 +108,10 @@ describe('deploy', () => {
|
||||
// @ts-ignore
|
||||
version: {},
|
||||
};
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = deploy(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Error! The `version` property inside your vercel.json file must be a number.\n'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,8 +19,12 @@ describe('env', () => {
|
||||
name: 'vercel-env-pull',
|
||||
});
|
||||
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
|
||||
const exitCode = await env(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
||||
const exitCodePromise = env(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "development" Environment Variables for Project vercel-env-pull'
|
||||
);
|
||||
await expect(client.stderr).toOutput('Created .env file');
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const rawDevEnv = await fs.readFile(path.join(cwd, '.env'));
|
||||
|
||||
@@ -39,8 +43,12 @@ describe('env', () => {
|
||||
name: 'vercel-env-pull',
|
||||
});
|
||||
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
|
||||
const exitCode = await env(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
||||
const exitCodePromise = env(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "development" Environment Variables for Project vercel-env-pull'
|
||||
);
|
||||
await expect(client.stderr).toOutput('Created other.env file');
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const rawDevEnv = await fs.readFile(path.join(cwd, 'other.env'));
|
||||
|
||||
@@ -61,8 +69,12 @@ describe('env', () => {
|
||||
});
|
||||
|
||||
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
|
||||
const exitCode = await env(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
||||
const exitCodePromise = env(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "development" Environment Variables for Project vercel-env-pull'
|
||||
);
|
||||
await expect(client.stderr).toOutput('Created other.env file');
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const rawDevEnv = await fs.readFile(path.join(cwd, 'other.env'));
|
||||
|
||||
|
||||
@@ -10,11 +10,9 @@ describe('inspect', () => {
|
||||
client.setArgv('inspect', deployment.url);
|
||||
const exitCode = await inspect(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
expect(
|
||||
client.mockOutput.mock.calls[0][0].startsWith(
|
||||
`> Fetched deployment "${deployment.url}" in ${user.username}`
|
||||
)
|
||||
).toBeTruthy();
|
||||
await expect(client.stderr).toOutput(
|
||||
`> Fetched deployment "${deployment.url}" in ${user.username}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should print error when deployment not found', async () => {
|
||||
@@ -23,7 +21,7 @@ describe('inspect', () => {
|
||||
client.setArgv('inspect', 'bad.com');
|
||||
const exitCode = await inspect(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! Failed to find deployment "bad.com" in ${user.username}\n`
|
||||
);
|
||||
});
|
||||
|
||||
139
packages/cli/test/unit/commands/list.test.ts
Normal file
139
packages/cli/test/unit/commands/list.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { client, MockClient } 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';
|
||||
|
||||
const fixture = (name: string) =>
|
||||
join(__dirname, '../../fixtures/unit/commands/list', name);
|
||||
|
||||
describe('list', () => {
|
||||
const originalCwd = process.cwd();
|
||||
let teamSlug: string = '';
|
||||
|
||||
it('should get deployments from a project linked by a directory', async () => {
|
||||
const cwd = fixture('with-team');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
|
||||
const user = useUser();
|
||||
const team = useTeams('team_dummy');
|
||||
teamSlug = team[0].slug;
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'with-team',
|
||||
name: 'with-team',
|
||||
});
|
||||
const deployment = useDeployment({ creator: user });
|
||||
|
||||
await list(client);
|
||||
|
||||
const output = await readOutputStream(client);
|
||||
|
||||
const { org } = getDataFromIntro(output.split('\n')[0]);
|
||||
const header: string[] = parseTable(output.split('\n')[2]);
|
||||
const data: string[] = parseTable(output.split('\n')[3]);
|
||||
data.splice(2, 1);
|
||||
|
||||
expect(org).toEqual(team[0].slug);
|
||||
expect(header).toEqual([
|
||||
'project',
|
||||
'latest deployment',
|
||||
'state',
|
||||
'age',
|
||||
'username',
|
||||
]);
|
||||
|
||||
expect(data).toEqual([
|
||||
deployment.url,
|
||||
stateString(deployment.state || ''),
|
||||
user.name,
|
||||
]);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should get the deployments for a specified project', async () => {
|
||||
const cwd = fixture('with-team');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
|
||||
const user = useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'with-team',
|
||||
name: 'with-team',
|
||||
});
|
||||
const deployment = useDeployment({ creator: user });
|
||||
|
||||
client.setArgv(deployment.name);
|
||||
await list(client);
|
||||
|
||||
const output = await readOutputStream(client);
|
||||
|
||||
const { org } = getDataFromIntro(output.split('\n')[0]);
|
||||
const header: string[] = parseTable(output.split('\n')[2]);
|
||||
const data: string[] = parseTable(output.split('\n')[3]);
|
||||
data.splice(2, 1);
|
||||
|
||||
expect(org).toEqual(teamSlug);
|
||||
|
||||
expect(header).toEqual([
|
||||
'project',
|
||||
'latest deployment',
|
||||
'state',
|
||||
'age',
|
||||
'username',
|
||||
]);
|
||||
expect(data).toEqual([
|
||||
deployment.url,
|
||||
stateString(deployment.state || ''),
|
||||
user.name,
|
||||
]);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -5,22 +5,45 @@ import { useUser } from '../../mocks/user';
|
||||
describe('login', () => {
|
||||
it('should not allow the `--token` flag', async () => {
|
||||
client.setArgv('login', '--token', 'foo');
|
||||
const exitCode = await login(client);
|
||||
expect(exitCode).toEqual(2);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
const exitCodePromise = login(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Error! `--token` may not be used with the "login" command\n'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(2);
|
||||
});
|
||||
|
||||
it('should allow login via email as argument', async () => {
|
||||
const user = useUser();
|
||||
client.setArgv('login', user.email);
|
||||
const exitCode = await login(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
expect(
|
||||
client.outputBuffer.includes(
|
||||
const exitCodePromise = login(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Success! Email authentication complete for ${user.email}`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
});
|
||||
|
||||
describe('interactive', () => {
|
||||
it('should allow login via email', async () => {
|
||||
const user = useUser();
|
||||
client.setArgv('login');
|
||||
const exitCodePromise = login(client);
|
||||
await expect(client.stderr).toOutput(`> Log in to Vercel`);
|
||||
|
||||
// Move down to "Email" option
|
||||
client.stdin.write('\x1B[B'); // Down arrow
|
||||
client.stdin.write('\x1B[B'); // Down arrow
|
||||
client.stdin.write('\x1B[B'); // Down arrow
|
||||
client.stdin.write('\r'); // Return key
|
||||
|
||||
await expect(client.stderr).toOutput('> Enter your email address:');
|
||||
|
||||
client.stdin.write(`${user.email}\n`);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Success! Email authentication complete for ${user.email}`
|
||||
)
|
||||
).toEqual(true);
|
||||
);
|
||||
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
272
packages/cli/test/unit/commands/projects.test.ts
Normal file
272
packages/cli/test/unit/commands/projects.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { join } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import projects from '../../../src/commands/projects';
|
||||
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';
|
||||
|
||||
describe('projects', () => {
|
||||
describe('connect', () => {
|
||||
const originalCwd = process.cwd();
|
||||
const fixture = (name: string) =>
|
||||
join(__dirname, '../../fixtures/unit/commands/projects/connect', name);
|
||||
|
||||
it('connects an unlinked project', async () => {
|
||||
const cwd = fixture('unlinked');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'unlinked',
|
||||
name: 'unlinked',
|
||||
});
|
||||
client.setArgv('projects', 'connect');
|
||||
const projectsPromise = projects(client);
|
||||
|
||||
await expect(client.stderr).toOutput('Set up');
|
||||
client.stdin.write('y\n');
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
'Which scope should contain your project?'
|
||||
);
|
||||
client.stdin.write('\r');
|
||||
|
||||
await expect(client.stderr).toOutput('Found project');
|
||||
client.stdin.write('y\n');
|
||||
|
||||
const exitCode = await projectsPromise;
|
||||
await expect(client.stderr).toOutput(
|
||||
'Connected GitHub repository user/repo!'
|
||||
);
|
||||
|
||||
expect(exitCode).toEqual(0);
|
||||
|
||||
const project: Project = await client.fetch(`/v8/projects/unlinked`);
|
||||
expect(project.link).toMatchObject({
|
||||
type: 'github',
|
||||
repo: 'user/repo',
|
||||
repoId: 1010,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
});
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should fail when there is no git config', async () => {
|
||||
const cwd = fixture('no-git-config');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'no-git-config',
|
||||
name: 'no-git-config',
|
||||
});
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! No local git repo found. Run \`git clone <url>\` to clone a remote Git repository first.\n`
|
||||
);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should fail when there is no remote url', async () => {
|
||||
const cwd = fixture('no-remote-url');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'no-remote-url',
|
||||
name: 'no-remote-url',
|
||||
});
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! No remote origin URL found in your Git config. Make sure you've connected your local Git repo to a Git provider first.\n`
|
||||
);
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should fail when the remote url is bad', async () => {
|
||||
const cwd = fixture('bad-remote-url');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'bad-remote-url',
|
||||
name: 'bad-remote-url',
|
||||
});
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Error! Failed to parse Git repo data from the following remote URL in your Git config: bababooey\n`
|
||||
);
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should connect a repo to a project that is not already connected', async () => {
|
||||
const cwd = fixture('new-connection');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'new-connection',
|
||||
name: 'new-connection',
|
||||
});
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
|
||||
const project: Project = await client.fetch(
|
||||
`/v8/projects/new-connection`
|
||||
);
|
||||
expect(project.link).toMatchObject({
|
||||
type: 'github',
|
||||
repo: 'user/repo',
|
||||
repoId: 1010,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
});
|
||||
expect(client.stderr).toOutput(
|
||||
`> Connected GitHub repository user/repo!\n`
|
||||
);
|
||||
expect(exitCode).toEqual(0);
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should replace an old connection with a new one', async () => {
|
||||
const cwd = fixture('existing-connection');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
id: 'existing-connection',
|
||||
name: 'existing-connection',
|
||||
});
|
||||
project.project.link = {
|
||||
type: 'github',
|
||||
repo: 'repo',
|
||||
org: 'user',
|
||||
repoId: 1010,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
};
|
||||
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
|
||||
const newProjectData: Project = await client.fetch(
|
||||
`/v8/projects/existing-connection`
|
||||
);
|
||||
expect(newProjectData.link).toMatchObject({
|
||||
type: 'github',
|
||||
repo: 'user2/repo2',
|
||||
repoId: 1010,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
});
|
||||
await expect(client.stderr).toOutput(
|
||||
`> Connected GitHub repository user2/repo2!\n`
|
||||
);
|
||||
expect(exitCode).toEqual(0);
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should exit when an already-connected repo is connected', async () => {
|
||||
const cwd = fixture('new-connection');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
id: 'new-connection',
|
||||
name: 'new-connection',
|
||||
});
|
||||
project.project.link = {
|
||||
type: 'github',
|
||||
repo: 'repo',
|
||||
org: 'user',
|
||||
repoId: 1010,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
};
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
await expect(client.stderr).toOutput(
|
||||
`> user/repo is already connected to your project.\n`
|
||||
);
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
it('should fail when it cannot find the repository', async () => {
|
||||
const cwd = fixture('invalid-repo');
|
||||
try {
|
||||
process.chdir(cwd);
|
||||
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'invalid-repo',
|
||||
name: 'invalid-repo',
|
||||
});
|
||||
|
||||
client.setArgv('projects', 'connect', '--yes');
|
||||
const exitCode = await projects(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Failed to link laksfj/asdgklsadkl. Make sure there aren't any typos and that you have access to the repository if it's private.`
|
||||
);
|
||||
} finally {
|
||||
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,17 @@ describe('pull', () => {
|
||||
name: 'vercel-pull-next',
|
||||
});
|
||||
client.setArgv('pull', cwd);
|
||||
const exitCode = await pull(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
||||
const exitCodePromise = pull(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "development" Environment Variables for Project vercel-pull-next'
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Created .vercel${path.sep}.env.development.local file`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const rawDevEnv = await fs.readFile(
|
||||
path.join(cwd, '.vercel', '.env.development.local')
|
||||
@@ -29,23 +38,18 @@ describe('pull', () => {
|
||||
});
|
||||
|
||||
it('should fail with message to pull without a link and without --env', async () => {
|
||||
try {
|
||||
process.stdout.isTTY = undefined;
|
||||
client.stdin.isTTY = false;
|
||||
|
||||
const cwd = setupFixture('vercel-pull-unlinked');
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
const cwd = setupFixture('vercel-pull-unlinked');
|
||||
useUser();
|
||||
useTeams('team_dummy');
|
||||
|
||||
client.setArgv('pull', cwd);
|
||||
const exitCode = await pull(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(1);
|
||||
|
||||
expect(client.outputBuffer).toMatch(
|
||||
/Command `vercel pull` requires confirmation. Use option "--yes" to confirm./gm
|
||||
);
|
||||
} finally {
|
||||
process.stdout.isTTY = true;
|
||||
}
|
||||
client.setArgv('pull', cwd);
|
||||
const exitCodePromise = pull(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should fail without message to pull without a link and with --env', async () => {
|
||||
@@ -54,12 +58,11 @@ describe('pull', () => {
|
||||
useTeams('team_dummy');
|
||||
|
||||
client.setArgv('pull', cwd, '--yes');
|
||||
const exitCode = await pull(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(1);
|
||||
|
||||
expect(client.outputBuffer).not.toMatch(
|
||||
/Command `vercel pull` requires confirmation. Use option "--yes" to confirm./gm
|
||||
const exitCodePromise = pull(client);
|
||||
await expect(client.stderr).not.toOutput(
|
||||
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
|
||||
it('should handle pulling with env vars (headless mode)', async () => {
|
||||
@@ -81,8 +84,17 @@ describe('pull', () => {
|
||||
name: 'vercel-pull-next',
|
||||
});
|
||||
client.setArgv('pull', cwd);
|
||||
const exitCode = await pull(client);
|
||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
||||
const exitCodePromise = pull(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "development" Environment Variables for Project vercel-pull-next'
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Created .vercel${path.sep}.env.development.local file`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const config = await fs.readJSON(path.join(cwd, '.vercel/project.json'));
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
@@ -108,8 +120,17 @@ describe('pull', () => {
|
||||
name: 'vercel-pull-next',
|
||||
});
|
||||
client.setArgv('pull', '--environment=preview', cwd);
|
||||
const exitCode = await pull(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
const exitCodePromise = pull(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "preview" Environment Variables for Project vercel-pull-next'
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Created .vercel${path.sep}.env.preview.local file`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const rawPreviewEnv = await fs.readFile(
|
||||
path.join(cwd, '.vercel', '.env.preview.local')
|
||||
@@ -130,8 +151,17 @@ describe('pull', () => {
|
||||
name: 'vercel-pull-next',
|
||||
});
|
||||
client.setArgv('pull', '--environment=production', cwd);
|
||||
const exitCode = await pull(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
const exitCodePromise = pull(client);
|
||||
await expect(client.stderr).toOutput(
|
||||
'Downloading "production" Environment Variables for Project vercel-pull-next'
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Created .vercel${path.sep}.env.production.local file`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||
);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
|
||||
const rawProdEnv = await fs.readFile(
|
||||
path.join(cwd, '.vercel', '.env.production.local')
|
||||
|
||||
@@ -14,14 +14,14 @@ describe('whoami', () => {
|
||||
const user = useUser();
|
||||
const exitCode = await whoami(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
expect(client.outputBuffer).toEqual(`> ${user.username}\n`);
|
||||
await expect(client.stderr).toOutput(`> ${user.username}\n`);
|
||||
});
|
||||
|
||||
it('should print only the Vercel username when output is not a TTY', async () => {
|
||||
const user = useUser();
|
||||
client.output.isTTY = false;
|
||||
client.stdout.isTTY = false;
|
||||
const exitCode = await whoami(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
expect(client.outputBuffer).toEqual(`${user.username}\n`);
|
||||
await expect(client.stdout).toOutput(`${user.username}\n`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import confirm from '../../src/util/input/confirm';
|
||||
import { client } from '../mocks/client';
|
||||
|
||||
describe('MockClient', () => {
|
||||
it('should mock `confirm()`', async () => {
|
||||
// true
|
||||
let confirmedPromise = confirm(client, 'Do the thing?', false);
|
||||
|
||||
client.stdin.write('yes\n');
|
||||
|
||||
client.stdout.setEncoding('utf8');
|
||||
client.stdout.on('data', d => console.log({ d }));
|
||||
|
||||
let confirmed = await confirmedPromise;
|
||||
expect(confirmed).toEqual(true);
|
||||
|
||||
// false
|
||||
confirmedPromise = confirm(client, 'Do the thing?', false);
|
||||
|
||||
client.stdin.write('no\n');
|
||||
|
||||
confirmed = await confirmedPromise;
|
||||
expect(confirmed).toEqual(false);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user