mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
41 Commits
@vercel/py
...
@vercel/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4773ff5efd | ||
|
|
d8c7308eb6 | ||
|
|
5df1c89138 | ||
|
|
f5d879143c | ||
|
|
9a55809515 | ||
|
|
56adf15823 | ||
|
|
1acab3d06c | ||
|
|
081b38466b | ||
|
|
c397fd1856 | ||
|
|
afd303b94a | ||
|
|
b12387034a | ||
|
|
5af65d5a24 | ||
|
|
1ee9a96a62 | ||
|
|
76130faf26 | ||
|
|
fb3601d178 | ||
|
|
aebfb6812d | ||
|
|
73999e7253 | ||
|
|
989dad5570 | ||
|
|
68c2dea601 | ||
|
|
63f2da2f68 | ||
|
|
57e5f81361 | ||
|
|
fd5e440533 | ||
|
|
2a45805b26 | ||
|
|
5523383e50 | ||
|
|
0ecbb24cab | ||
|
|
922223bd19 | ||
|
|
0ad7fd34f4 | ||
|
|
3d3774ee7e | ||
|
|
50f8eec7cb | ||
|
|
45374e2f90 | ||
|
|
fd9142b6f3 | ||
|
|
8cf67b549b | ||
|
|
5dc6f48e44 | ||
|
|
66c8544e8f | ||
|
|
0140db38fa | ||
|
|
e5421c27e8 | ||
|
|
5afc527233 | ||
|
|
de9518b010 | ||
|
|
c322d1dbba | ||
|
|
18c19ead76 | ||
|
|
9d80c27382 |
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
@@ -12,6 +12,7 @@ To get started, execute the following:
|
||||
|
||||
```
|
||||
git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
yarn install
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@sentry/node": "5.11.1",
|
||||
"got": "10.2.1",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"parse-github-url": "1.0.2",
|
||||
"tar-fs": "2.0.0",
|
||||
"unzip-stream": "0.3.0"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"jest": "28.0.2",
|
||||
"json5": "2.1.1",
|
||||
"lint-staged": "9.2.5",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"npm-package-arg": "6.1.0",
|
||||
"prettier": "2.6.2",
|
||||
"ts-eager": "2.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "5.0.3",
|
||||
"version": "5.0.5",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -31,11 +31,10 @@
|
||||
"@types/node-fetch": "^2.1.6",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "2.4.2",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/ncc": "0.34.0",
|
||||
"aggregate-error": "3.0.1",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "2.1.4",
|
||||
"boxen": "4.2.0",
|
||||
"cross-spawn": "6.0.5",
|
||||
"end-of-stream": "1.4.1",
|
||||
"fs-extra": "10.0.0",
|
||||
@@ -44,7 +43,7 @@
|
||||
"js-yaml": "3.13.1",
|
||||
"minimatch": "3.0.4",
|
||||
"multistream": "2.1.1",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"semver": "6.1.1",
|
||||
"typescript": "4.3.4",
|
||||
"yazl": "2.5.1"
|
||||
|
||||
@@ -305,43 +305,34 @@ export async function scanParentDirs(
|
||||
): Promise<ScanParentDirsResult> {
|
||||
assert(path.isAbsolute(destPath));
|
||||
|
||||
let cliType: CliType = 'yarn';
|
||||
let packageJson: PackageJson | undefined;
|
||||
let packageJsonPath: string | undefined;
|
||||
let currentDestPath = destPath;
|
||||
const pkgJsonPath = await walkParentDirs({
|
||||
base: '/',
|
||||
start: destPath,
|
||||
filename: 'package.json',
|
||||
});
|
||||
const packageJson: PackageJson | undefined =
|
||||
readPackageJson && pkgJsonPath
|
||||
? JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'))
|
||||
: undefined;
|
||||
const [yarnLockPath, npmLockPath, pnpmLockPath] = await walkParentDirsMulti({
|
||||
base: '/',
|
||||
start: destPath,
|
||||
filenames: ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'],
|
||||
});
|
||||
let lockfileVersion: number | undefined;
|
||||
let cliType: CliType = 'yarn';
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
packageJsonPath = path.join(currentDestPath, 'package.json');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
// Only read the contents of the *first* `package.json` file found,
|
||||
// since that's the one related to this installation.
|
||||
if (readPackageJson && !packageJson) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [packageLockJson, hasYarnLock, pnpmLockYaml] = await Promise.all([
|
||||
fs
|
||||
.readJson(path.join(currentDestPath, 'package-lock.json'))
|
||||
.catch(error => {
|
||||
// If the file doesn't exist, fail gracefully otherwise error
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
fs.pathExists(path.join(currentDestPath, 'yarn.lock')),
|
||||
readConfigFile<{ lockfileVersion: number }>(
|
||||
path.join(currentDestPath, 'pnpm-lock.yaml')
|
||||
),
|
||||
const [hasYarnLock, packageLockJson, pnpmLockYaml] = await Promise.all([
|
||||
Boolean(yarnLockPath),
|
||||
npmLockPath
|
||||
? readConfigFile<{ lockfileVersion: number }>(npmLockPath)
|
||||
: null,
|
||||
pnpmLockPath
|
||||
? readConfigFile<{ lockfileVersion: number }>(pnpmLockPath)
|
||||
: null,
|
||||
]);
|
||||
|
||||
// Priority order is Yarn > pnpm > npm
|
||||
// - find highest priority lock file and use that
|
||||
if (hasYarnLock) {
|
||||
cliType = 'yarn';
|
||||
} else if (pnpmLockYaml) {
|
||||
@@ -353,19 +344,7 @@ export async function scanParentDirs(
|
||||
lockfileVersion = packageLockJson.lockfileVersion;
|
||||
}
|
||||
|
||||
// Only stop iterating if a lockfile was found, because it's possible
|
||||
// that the lockfile is in a higher path than where the `package.json`
|
||||
// file was found.
|
||||
if (packageLockJson || hasYarnLock || pnpmLockYaml) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newDestPath = path.dirname(currentDestPath);
|
||||
if (currentDestPath === newDestPath) break;
|
||||
currentDestPath = newDestPath;
|
||||
}
|
||||
|
||||
const packageJsonPath = pkgJsonPath || undefined;
|
||||
return { cliType, packageJson, lockfileVersion, packageJsonPath };
|
||||
}
|
||||
|
||||
@@ -387,11 +366,48 @@ export async function walkParentDirs({
|
||||
}
|
||||
|
||||
parent = path.dirname(current);
|
||||
|
||||
if (parent === current) {
|
||||
// Reached root directory of the filesystem
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function walkParentDirsMulti({
|
||||
base,
|
||||
start,
|
||||
filenames,
|
||||
}: {
|
||||
base: string;
|
||||
start: string;
|
||||
filenames: string[];
|
||||
}): Promise<(string | undefined)[]> {
|
||||
let parent = '';
|
||||
for (let current = start; base.length <= current.length; current = parent) {
|
||||
const fullPaths = filenames.map(f => path.join(current, f));
|
||||
const existResults = await Promise.all(
|
||||
fullPaths.map(f => fs.pathExists(f))
|
||||
);
|
||||
const foundOneOrMore = existResults.some(b => b);
|
||||
|
||||
if (foundOneOrMore) {
|
||||
return fullPaths.map((f, i) => (existResults[i] ? f : undefined));
|
||||
}
|
||||
|
||||
parent = path.dirname(current);
|
||||
|
||||
if (parent === current) {
|
||||
// Reached root directory of the filesystem
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function isSet<T>(v: any): v is Set<T> {
|
||||
return v?.constructor?.name === 'Set';
|
||||
}
|
||||
|
||||
36
packages/build-utils/test/unit.test.ts
vendored
36
packages/build-utils/test/unit.test.ts
vendored
@@ -19,7 +19,7 @@ import {
|
||||
Meta,
|
||||
} from '../src';
|
||||
|
||||
jest.setTimeout(7 * 1000);
|
||||
jest.setTimeout(10 * 1000);
|
||||
|
||||
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
||||
let result;
|
||||
@@ -454,6 +454,7 @@ it('should return lockfileVersion 2 with npm7', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(2);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should not return lockfileVersion with yarn', async () => {
|
||||
@@ -461,6 +462,7 @@ it('should not return lockfileVersion with yarn', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should return lockfileVersion 1 with older versions of npm', async () => {
|
||||
@@ -468,6 +470,7 @@ it('should return lockfileVersion 1 with older versions of npm', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(1);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect npm Workspaces', async () => {
|
||||
@@ -475,20 +478,45 @@ it('should detect npm Workspaces', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(2);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect pnpm', async () => {
|
||||
it('should detect pnpm without workspace', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '22-pnpm');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('pnpm');
|
||||
expect(result.lockfileVersion).toEqual(5.3);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect pnpm Workspaces', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '23-pnpm-workspaces/a');
|
||||
it('should detect pnpm with workspaces', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '23-pnpm-workspaces/c');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('pnpm');
|
||||
expect(result.lockfileVersion).toEqual(5.3);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect package.json in nested backend', async () => {
|
||||
const fixture = path.join(
|
||||
__dirname,
|
||||
'../../node/test/fixtures/18.1-nested-packagejson/backend'
|
||||
);
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect package.json in nested frontend', async () => {
|
||||
const fixture = path.join(
|
||||
__dirname,
|
||||
'../../node/test/fixtures/18.1-nested-packagejson/frontend'
|
||||
);
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should only invoke `runNpmInstall()` once per `package.json` file (serial)', async () => {
|
||||
|
||||
@@ -53,13 +53,13 @@ At this point you can make modifications to the CLI source code and test them ou
|
||||
cd packages/cli
|
||||
```
|
||||
|
||||
From within the `packages/cli` directory, you can use the `ts-eager` command line tool to quickly excute Vercel CLI from its TypeScript source code directly (without having to manually compile first). For example:
|
||||
From within the `packages/cli` directory, you can use the "dev" script to quickly execute Vercel CLI from its TypeScript source code directly (without having to manually compile first). For example:
|
||||
|
||||
```bash
|
||||
npx ts-eager src
|
||||
npx ts-eager src login
|
||||
npx ts-eager src switch --debug
|
||||
npx ts-eager src dev
|
||||
yarn dev deploy
|
||||
yarn dev whoami
|
||||
yarn dev login
|
||||
yarn dev switch --debug
|
||||
```
|
||||
|
||||
When you are satisfied with your changes, make a commit and create a pull request!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "27.1.5",
|
||||
"version": "27.3.0",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -18,8 +18,8 @@
|
||||
"test-integration-dev": "yarn test test/dev/",
|
||||
"prepublishOnly": "yarn build",
|
||||
"coverage": "codecov",
|
||||
"build": "node -r ts-eager/register ./scripts/build.ts",
|
||||
"build-dev": "node -r ts-eager/register ./scripts/build.ts --dev"
|
||||
"build": "ts-node ./scripts/build.ts",
|
||||
"dev": "ts-node ./src/index.ts"
|
||||
},
|
||||
"bin": {
|
||||
"vc": "./dist/index.js",
|
||||
@@ -42,16 +42,16 @@
|
||||
"node": ">= 14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "5.0.3",
|
||||
"@vercel/go": "2.0.7",
|
||||
"@vercel/hydrogen": "0.0.4",
|
||||
"@vercel/next": "3.1.7",
|
||||
"@vercel/node": "2.4.4",
|
||||
"@vercel/python": "3.0.7",
|
||||
"@vercel/redwood": "1.0.8",
|
||||
"@vercel/remix": "1.0.9",
|
||||
"@vercel/ruby": "1.3.15",
|
||||
"@vercel/static-build": "1.0.8",
|
||||
"@vercel/build-utils": "5.0.5",
|
||||
"@vercel/go": "2.0.9",
|
||||
"@vercel/hydrogen": "0.0.6",
|
||||
"@vercel/next": "3.1.9",
|
||||
"@vercel/node": "2.5.0",
|
||||
"@vercel/python": "3.1.1",
|
||||
"@vercel/redwood": "1.0.10",
|
||||
"@vercel/remix": "1.0.11",
|
||||
"@vercel/ruby": "1.3.17",
|
||||
"@vercel/static-build": "1.0.10",
|
||||
"update-notifier": "5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -59,6 +59,7 @@
|
||||
"@next/env": "11.1.2",
|
||||
"@sentry/node": "5.5.0",
|
||||
"@sindresorhus/slugify": "0.11.0",
|
||||
"@swc/core": "1.2.218",
|
||||
"@tootallnate/once": "1.1.2",
|
||||
"@types/ansi-escapes": "3.0.0",
|
||||
"@types/ansi-regex": "4.0.0",
|
||||
@@ -96,11 +97,11 @@
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel/client": "12.1.2",
|
||||
"@vercel/client": "12.1.4",
|
||||
"@vercel/frameworks": "1.1.1",
|
||||
"@vercel/fs-detectors": "2.0.1",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@zeit/fun": "0.11.2",
|
||||
"@vercel/fun": "1.0.4",
|
||||
"@vercel/ncc": "0.34.0",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
"ajv": "6.12.2",
|
||||
"alpha-sort": "2.0.1",
|
||||
@@ -111,6 +112,7 @@
|
||||
"async-retry": "1.1.3",
|
||||
"async-sema": "2.1.4",
|
||||
"ava": "2.2.0",
|
||||
"boxen": "4.2.0",
|
||||
"bytes": "3.0.0",
|
||||
"chalk": "4.1.0",
|
||||
"chance": "1.1.7",
|
||||
@@ -147,7 +149,7 @@
|
||||
"minimatch": "3.0.4",
|
||||
"mri": "1.1.5",
|
||||
"ms": "2.1.2",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"npm-package-arg": "6.1.0",
|
||||
"open": "8.4.0",
|
||||
"ora": "3.4.0",
|
||||
@@ -169,8 +171,8 @@
|
||||
"title": "3.4.1",
|
||||
"tmp-promise": "1.0.3",
|
||||
"tree-kill": "1.2.2",
|
||||
"ts-node": "8.3.0",
|
||||
"typescript": "4.3.4",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.7.4",
|
||||
"universal-analytics": "0.4.20",
|
||||
"utility-types": "2.1.0",
|
||||
"which": "2.0.2",
|
||||
|
||||
@@ -27,9 +27,6 @@ function envToString(key: string) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isDev = process.argv[2] === '--dev';
|
||||
|
||||
if (!isDev) {
|
||||
// Read the secrets from GitHub Actions and generate a file.
|
||||
// During local development, these secrets will be empty.
|
||||
await createConstants();
|
||||
@@ -45,22 +42,23 @@ async function main() {
|
||||
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
// Do the initial `ncc` build
|
||||
console.log();
|
||||
const args = ['ncc', 'build', '--external', 'update-notifier'];
|
||||
if (isDev) {
|
||||
args.push('--source-map');
|
||||
}
|
||||
args.push('src/index.ts');
|
||||
const args = [
|
||||
'ncc',
|
||||
'build',
|
||||
'--external',
|
||||
'update-notifier',
|
||||
'src/index.ts',
|
||||
];
|
||||
await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot });
|
||||
|
||||
// `ncc` has some issues with `@zeit/fun`'s runtime files:
|
||||
// `ncc` has some issues with `@vercel/fun`'s runtime files:
|
||||
// - Executable bits on the `bootstrap` files appear to be lost:
|
||||
// https://github.com/zeit/ncc/pull/182
|
||||
// https://github.com/vercel/ncc/pull/182
|
||||
// - The `bootstrap.js` asset does not get copied into the output dir:
|
||||
// https://github.com/zeit/ncc/issues/278
|
||||
// https://github.com/vercel/ncc/issues/278
|
||||
//
|
||||
// Aside from those issues, all the same files from the `runtimes` directory
|
||||
// should be copied into the output runtimes dir, specifically the `index.js`
|
||||
@@ -70,7 +68,7 @@ async function main() {
|
||||
// with `fun`'s cache invalidation mechanism and they need to be shasum'd.
|
||||
const runtimes = join(
|
||||
dirRoot,
|
||||
'../../node_modules/@zeit/fun/dist/src/runtimes'
|
||||
'../../node_modules/@vercel/fun/dist/src/runtimes'
|
||||
);
|
||||
await cpy('**/*', join(distRoot, 'runtimes'), {
|
||||
parents: true,
|
||||
@@ -79,6 +77,7 @@ async function main() {
|
||||
|
||||
// Band-aid to bundle stuff that `ncc` neglects to bundle
|
||||
await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot);
|
||||
await cpy(join(dirRoot, 'src/util/dev/builder-worker.js'), distRoot);
|
||||
|
||||
console.log('Finished building Vercel CLI');
|
||||
}
|
||||
|
||||
@@ -22,19 +22,7 @@ export default async function ls(
|
||||
) {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
|
||||
output.error('Please provide a number for flag --next');
|
||||
|
||||
@@ -23,19 +23,7 @@ export default async function rm(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [aliasOrId] = args;
|
||||
|
||||
|
||||
@@ -15,10 +15,9 @@ import { isValidName } from '../../util/is-valid-name';
|
||||
import handleCertError from '../../util/certs/handle-cert-error';
|
||||
import isWildcardAlias from '../../util/alias/is-wildcard-alias';
|
||||
import link from '../../util/output/link';
|
||||
import { User } from '../../types';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import toHost from '../../util/to-host';
|
||||
import { VercelConfig } from '../../util/dev/types';
|
||||
import type { VercelConfig } from '@vercel/client';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -30,23 +29,9 @@ export default async function set(
|
||||
opts: Partial<Options>,
|
||||
args: string[]
|
||||
) {
|
||||
const { output, localConfig } = client;
|
||||
|
||||
const setStamp = stamp();
|
||||
|
||||
let user: User;
|
||||
let contextName: string | null = null;
|
||||
|
||||
try {
|
||||
({ contextName, user } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { output, localConfig } = client;
|
||||
const { contextName, user } = await getScope(client);
|
||||
|
||||
// If there are more than two args we have to error
|
||||
if (args.length > 2) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
MergeRoutesProps,
|
||||
Route,
|
||||
} from '@vercel/routing-utils';
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
import type { VercelConfig } from '@vercel/client';
|
||||
|
||||
import pull from './pull';
|
||||
import { staticFiles as getFiles } from '../util/get-files';
|
||||
@@ -36,7 +36,10 @@ import * as cli from '../util/pkg-name';
|
||||
import cliPkg from '../util/pkg';
|
||||
import readJSONFile from '../util/read-json-file';
|
||||
import { CantParseJSONFile } from '../util/errors-ts';
|
||||
import { readProjectSettings } from '../util/projects/project-settings';
|
||||
import {
|
||||
ProjectLinkAndSettings,
|
||||
readProjectSettings,
|
||||
} from '../util/projects/project-settings';
|
||||
import { VERCEL_DIR } from '../util/projects/link';
|
||||
import confirm from '../util/input/confirm';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
@@ -46,19 +49,31 @@ import {
|
||||
PathOverride,
|
||||
writeBuildResult,
|
||||
} from '../util/build/write-build-result';
|
||||
import { importBuilders, BuilderWithPkg } from '../util/build/import-builders';
|
||||
import { importBuilders } from '../util/build/import-builders';
|
||||
import { initCorepack, cleanupCorepack } from '../util/build/corepack';
|
||||
import { sortBuilders } from '../util/build/sort-builders';
|
||||
import { toEnumerableError } from '../util/error';
|
||||
|
||||
type BuildResult = BuildResultV2 | BuildResultV3;
|
||||
|
||||
interface SerializedBuilder extends Builder {
|
||||
error?: Error;
|
||||
error?: any;
|
||||
require?: string;
|
||||
requirePath?: string;
|
||||
apiVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contents of the `builds.json` file.
|
||||
*/
|
||||
export interface BuildsManifest {
|
||||
'//': string;
|
||||
target: string;
|
||||
argv: string[];
|
||||
error?: any;
|
||||
builds?: SerializedBuilder[];
|
||||
}
|
||||
|
||||
const help = () => {
|
||||
return console.log(`
|
||||
${chalk.bold(`${cli.logo} ${cli.name} build`)}
|
||||
@@ -174,21 +189,83 @@ export default async function main(client: Client): Promise<number> {
|
||||
project = await readProjectSettings(join(cwd, VERCEL_DIR));
|
||||
}
|
||||
|
||||
// TODO: load env vars from the API, fall back to local files if that fails
|
||||
// Delete output directory from potential previous build
|
||||
const outputDir = argv['--output']
|
||||
? resolve(argv['--output'])
|
||||
: join(cwd, OUTPUT_DIR);
|
||||
await fs.remove(outputDir);
|
||||
|
||||
const envPath = await checkExists([
|
||||
join(cwd, VERCEL_DIR, `.env.${target}.local`),
|
||||
join(cwd, `.env`),
|
||||
]);
|
||||
if (envPath) {
|
||||
dotenv.config({ path: envPath, debug: client.output.isDebugEnabled() });
|
||||
output.log(`Loaded env from "${relative(cwd, envPath)}"`);
|
||||
const buildsJson: BuildsManifest = {
|
||||
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
|
||||
target,
|
||||
argv: process.argv,
|
||||
};
|
||||
|
||||
const envToUnset = new Set<string>(['VERCEL', 'NOW_BUILDER']);
|
||||
|
||||
try {
|
||||
const envPath = join(cwd, VERCEL_DIR, `.env.${target}.local`);
|
||||
// TODO (maybe?): load env vars from the API, fall back to the local file if that fails
|
||||
const dotenvResult = dotenv.config({
|
||||
path: envPath,
|
||||
debug: client.output.isDebugEnabled(),
|
||||
});
|
||||
if (dotenvResult.error) {
|
||||
output.debug(
|
||||
`Failed loading environment variables: ${dotenvResult.error}`
|
||||
);
|
||||
} else if (dotenvResult.parsed) {
|
||||
for (const key of Object.keys(dotenvResult.parsed)) {
|
||||
envToUnset.add(key);
|
||||
}
|
||||
output.debug(`Loaded environment variables from "${envPath}"`);
|
||||
}
|
||||
|
||||
// For Vercel Analytics support
|
||||
if (project.settings.analyticsId) {
|
||||
envToUnset.add('VERCEL_ANALYTICS_ID');
|
||||
process.env.VERCEL_ANALYTICS_ID = project.settings.analyticsId;
|
||||
}
|
||||
|
||||
// Some build processes use these env vars to platform detect Vercel
|
||||
process.env.VERCEL = '1';
|
||||
process.env.NOW_BUILDER = '1';
|
||||
|
||||
return await doBuild(client, project, buildsJson, cwd, outputDir);
|
||||
} catch (err: any) {
|
||||
output.prettyError(err);
|
||||
|
||||
// Write error to `builds.json` file
|
||||
buildsJson.error = toEnumerableError(err);
|
||||
const buildsJsonPath = join(outputDir, 'builds.json');
|
||||
const configJsonPath = join(outputDir, 'config.json');
|
||||
await fs.outputJSON(buildsJsonPath, buildsJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
await fs.writeJSON(configJsonPath, { version: 3 }, { spaces: 2 });
|
||||
|
||||
return 1;
|
||||
} finally {
|
||||
// Unset environment variables that were added by dotenv
|
||||
// (this is mostly for the unit tests)
|
||||
for (const key of envToUnset) {
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the Project's builders. If this function throws an error,
|
||||
* then it will be serialized into the `builds.json` manifest file.
|
||||
*/
|
||||
async function doBuild(
|
||||
client: Client,
|
||||
project: ProjectLinkAndSettings,
|
||||
buildsJson: BuildsManifest,
|
||||
cwd: string,
|
||||
outputDir: string
|
||||
): Promise<number> {
|
||||
const { output } = client;
|
||||
const workPath = join(cwd, project.settings.rootDirectory || '.');
|
||||
|
||||
// Load `package.json` and `vercel.json` files
|
||||
@@ -208,21 +285,21 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
const routesResult = getTransformedRoutes(vercelConfig || {});
|
||||
if (routesResult.error) {
|
||||
output.prettyError(routesResult.error);
|
||||
return 1;
|
||||
throw routesResult.error;
|
||||
}
|
||||
|
||||
if (vercelConfig?.builds && vercelConfig.functions) {
|
||||
output.prettyError({
|
||||
throw new NowBuildError({
|
||||
code: 'bad_request',
|
||||
message:
|
||||
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.',
|
||||
link: 'https://vercel.link/functions-and-builds',
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
let builds = vercelConfig?.builds || [];
|
||||
let zeroConfigRoutes: Route[] = [];
|
||||
let isZeroConfig = false;
|
||||
|
||||
if (builds.length > 0) {
|
||||
output.warn(
|
||||
@@ -231,17 +308,18 @@ export default async function main(client: Client): Promise<number> {
|
||||
builds = builds.map(b => expandBuild(files, b)).flat();
|
||||
} else {
|
||||
// Zero config
|
||||
isZeroConfig = true;
|
||||
|
||||
// Detect the Vercel Builders that will need to be invoked
|
||||
const detectedBuilders = await detectBuilders(files, pkg, {
|
||||
...vercelConfig,
|
||||
projectSettings: project.settings,
|
||||
ignoreBuildScript: true,
|
||||
featHandleMiss: true,
|
||||
});
|
||||
|
||||
if (detectedBuilders.errors && detectedBuilders.errors.length > 0) {
|
||||
output.prettyError(detectedBuilders.errors[0]);
|
||||
return 1;
|
||||
throw detectedBuilders.errors[0];
|
||||
}
|
||||
|
||||
for (const w of detectedBuilders.warnings) {
|
||||
@@ -274,13 +352,7 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
const builderSpecs = new Set(builds.map(b => b.use));
|
||||
|
||||
let buildersWithPkgs: Map<string, BuilderWithPkg>;
|
||||
try {
|
||||
buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
|
||||
} catch (err: any) {
|
||||
output.prettyError(err);
|
||||
return 1;
|
||||
}
|
||||
const buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
|
||||
|
||||
// Populate Files -> FileFsRef mapping
|
||||
const filesMap: Files = {};
|
||||
@@ -290,12 +362,6 @@ export default async function main(client: Client): Promise<number> {
|
||||
filesMap[path] = new FileFsRef({ mode, fsPath });
|
||||
}
|
||||
|
||||
// Delete output directory from potential previous build
|
||||
const outputDir = argv['--output']
|
||||
? resolve(argv['--output'])
|
||||
: join(cwd, OUTPUT_DIR);
|
||||
await fs.remove(outputDir);
|
||||
|
||||
const buildStamp = stamp();
|
||||
|
||||
// Create fresh new output directory
|
||||
@@ -322,12 +388,7 @@ export default async function main(client: Client): Promise<number> {
|
||||
];
|
||||
})
|
||||
);
|
||||
const buildsJson = {
|
||||
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
|
||||
target,
|
||||
argv: process.argv,
|
||||
builds: Array.from(buildsJsonBuilds.values()),
|
||||
};
|
||||
buildsJson.builds = Array.from(buildsJsonBuilds.values());
|
||||
const buildsJsonPath = join(outputDir, 'builds.json');
|
||||
const writeBuildsJsonPromise = fs.writeJSON(buildsJsonPath, buildsJson, {
|
||||
spaces: 2,
|
||||
@@ -362,7 +423,8 @@ export default async function main(client: Client): Promise<number> {
|
||||
try {
|
||||
const { builder, pkg: builderPkg } = builderWithPkg;
|
||||
|
||||
const buildConfig: Config = {
|
||||
const buildConfig: Config = isZeroConfig
|
||||
? {
|
||||
outputDirectory: project.settings.outputDirectory ?? undefined,
|
||||
...build.config,
|
||||
projectSettings: project.settings,
|
||||
@@ -371,7 +433,8 @@ export default async function main(client: Client): Promise<number> {
|
||||
buildCommand: project.settings.buildCommand ?? undefined,
|
||||
framework: project.settings.framework,
|
||||
nodeVersion: project.settings.nodeVersion,
|
||||
};
|
||||
}
|
||||
: build.config || {};
|
||||
const buildOptions: BuildOptions = {
|
||||
files: filesMap,
|
||||
entrypoint: build.src,
|
||||
@@ -397,7 +460,7 @@ export default async function main(client: Client): Promise<number> {
|
||||
build,
|
||||
builder,
|
||||
builderPkg,
|
||||
vercelConfig?.cleanUrls
|
||||
vercelConfig
|
||||
).then(
|
||||
override => {
|
||||
if (override) overrides.push(override);
|
||||
@@ -406,25 +469,17 @@ export default async function main(client: Client): Promise<number> {
|
||||
)
|
||||
);
|
||||
} catch (err: any) {
|
||||
const configJson = {
|
||||
version: 3,
|
||||
};
|
||||
const configJsonPromise = fs.writeJSON(
|
||||
const writeConfigJsonPromise = fs.writeJSON(
|
||||
join(outputDir, 'config.json'),
|
||||
configJson,
|
||||
{ version: 3 },
|
||||
{ spaces: 2 }
|
||||
);
|
||||
|
||||
await Promise.all([writeBuildsJsonPromise, configJsonPromise]);
|
||||
await Promise.all([writeBuildsJsonPromise, writeConfigJsonPromise]);
|
||||
|
||||
const buildJsonBuild = buildsJsonBuilds.get(build);
|
||||
if (buildJsonBuild) {
|
||||
buildJsonBuild.error = {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
...err,
|
||||
};
|
||||
buildJsonBuild.error = toEnumerableError(err);
|
||||
|
||||
await fs.writeJSON(buildsJsonPath, buildsJson, {
|
||||
spaces: 2,
|
||||
@@ -441,15 +496,12 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
// Wait for filesystem operations to complete
|
||||
// TODO render progress bar?
|
||||
let hadError = false;
|
||||
const errors = await Promise.all(ops);
|
||||
for (const error of errors) {
|
||||
if (error) {
|
||||
hadError = true;
|
||||
output.prettyError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (hadError) return 1;
|
||||
|
||||
// Merge existing `config.json` file into the one that will be produced
|
||||
const configPath = join(outputDir, 'config.json');
|
||||
@@ -569,7 +621,7 @@ function mergeImages(
|
||||
let images: BuildResultV2Typical['images'] = undefined;
|
||||
for (const result of buildResults) {
|
||||
if ('images' in result && result.images) {
|
||||
images = Object.assign({} || images, result.images);
|
||||
images = Object.assign({}, images, result.images);
|
||||
}
|
||||
}
|
||||
return images;
|
||||
@@ -587,14 +639,3 @@ function mergeWildcard(
|
||||
}
|
||||
return wildcard;
|
||||
}
|
||||
|
||||
async function checkExists(paths: Iterable<string>) {
|
||||
for (const path of paths) {
|
||||
try {
|
||||
await fs.stat(path);
|
||||
return path;
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import stamp from '../../util/output/stamp';
|
||||
import createCertFromFile from '../../util/certs/create-cert-from-file';
|
||||
import createCertForCns from '../../util/certs/create-cert-for-cns';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { Cert } from '../../types';
|
||||
|
||||
interface Options {
|
||||
'--overwrite'?: boolean;
|
||||
@@ -21,7 +22,7 @@ async function add(
|
||||
const { output } = client;
|
||||
const addStamp = stamp();
|
||||
|
||||
let cert;
|
||||
let cert: Cert | Error;
|
||||
|
||||
const {
|
||||
'--overwrite': overwite,
|
||||
@@ -30,18 +31,7 @@ async function add(
|
||||
'--ca': caPath,
|
||||
} = opts;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (overwite) {
|
||||
output.error('Overwrite option is deprecated');
|
||||
|
||||
@@ -39,18 +39,7 @@ export default async function issue(
|
||||
'--ca': caPath,
|
||||
} = opts;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (overwite) {
|
||||
output.error('Overwrite option is deprecated');
|
||||
|
||||
@@ -21,18 +21,8 @@ async function ls(
|
||||
): Promise<number> {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
let contextName = null;
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
if (typeof nextTimestamp !== 'undefined' && Number.isNaN(nextTimestamp)) {
|
||||
output.error('Please provide a number for flag --next');
|
||||
return 1;
|
||||
|
||||
@@ -17,21 +17,9 @@ import { getCommandName } from '../../util/pkg-name';
|
||||
type Options = {};
|
||||
|
||||
async function rm(client: Client, opts: Options, args: string[]) {
|
||||
const { output } = client;
|
||||
const rmStamp = stamp();
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { output } = client;
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (args.length !== 1) {
|
||||
output.error(
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
||||
import bytes from 'bytes';
|
||||
import chalk from 'chalk';
|
||||
import { join, resolve, basename } from 'path';
|
||||
import { Dictionary, fileNameSymbol, VercelConfig } from '@vercel/client';
|
||||
import { fileNameSymbol, VercelConfig } from '@vercel/client';
|
||||
import code from '../../util/output/code';
|
||||
import highlight from '../../util/output/highlight';
|
||||
import { readLocalConfig } from '../../util/config/files';
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
ConflictingPathSegment,
|
||||
BuildError,
|
||||
NotDomainOwner,
|
||||
isAPIError,
|
||||
} from '../../util/errors-ts';
|
||||
import { SchemaValidationFailed } from '../../util/errors';
|
||||
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
|
||||
@@ -65,6 +66,8 @@ 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/create-git-meta';
|
||||
import { parseEnv } from '../../util/parse-env';
|
||||
import { errorToString, isErrnoException, isError } from '../../util/is-error';
|
||||
|
||||
export default async (client: Client) => {
|
||||
const { output } = client;
|
||||
@@ -216,6 +219,22 @@ export default async (client: Client) => {
|
||||
}
|
||||
|
||||
const prebuiltBuild = await getPrebuiltJson(path);
|
||||
|
||||
// Ensure that there was not a build error
|
||||
const prebuiltError =
|
||||
prebuiltBuild?.error ||
|
||||
prebuiltBuild?.builds?.find(build => 'error' in build)?.error;
|
||||
if (prebuiltError) {
|
||||
output.log(
|
||||
`Prebuilt deployment cannot be created because ${getCommandName(
|
||||
'build'
|
||||
)} failed with error:\n`
|
||||
);
|
||||
prettyError(prebuiltError);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Ensure that the deploy target matches the build target
|
||||
const assumedTarget = target || 'preview';
|
||||
if (prebuiltBuild?.target && prebuiltBuild.target !== assumedTarget) {
|
||||
let specifyTarget = '';
|
||||
@@ -268,8 +287,11 @@ export default async (client: Client) => {
|
||||
'Which scope do you want to deploy to?',
|
||||
autoConfirm
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
) {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
@@ -428,7 +450,7 @@ export default async (client: Client) => {
|
||||
parseMeta(argv['--meta'])
|
||||
);
|
||||
|
||||
const gitMetadata = await createGitMeta(path, output);
|
||||
const gitMetadata = await createGitMeta(path, output, project);
|
||||
|
||||
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
|
||||
const deploymentEnv = Object.assign(
|
||||
@@ -448,8 +470,8 @@ export default async (client: Client) => {
|
||||
try {
|
||||
await addProcessEnv(log, deploymentEnv);
|
||||
await addProcessEnv(log, deploymentBuildEnv);
|
||||
} catch (err) {
|
||||
error(err.message);
|
||||
} catch (err: unknown) {
|
||||
error(errorToString(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -610,8 +632,10 @@ export default async (client: Client) => {
|
||||
error('Uploading failed. Please try again.');
|
||||
return 1;
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isError(err)) {
|
||||
debug(`Error: ${err}\n${err.stack}`);
|
||||
}
|
||||
|
||||
if (err instanceof NotDomainOwner) {
|
||||
output.error(err.message);
|
||||
@@ -678,13 +702,7 @@ export default async (client: Client) => {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') {
|
||||
const { additionalProperty = '' } = err.params || {};
|
||||
const message = `Invalid DC name for the scale option: ${additionalProperty}`;
|
||||
error(message);
|
||||
}
|
||||
|
||||
if (err.code === 'size_limit_exceeded') {
|
||||
if (isAPIError(err) && err.code === 'size_limit_exceeded') {
|
||||
const { sizeLimit = 0 } = err;
|
||||
const message = `File size limit exceeded (${bytes(sizeLimit)})`;
|
||||
error(message);
|
||||
@@ -894,36 +912,3 @@ const printDeploymentStatus = async (
|
||||
output.print(message + link);
|
||||
}
|
||||
};
|
||||
|
||||
// Converts `env` Arrays, Strings and Objects into env Objects.
|
||||
const parseEnv = (env?: string[] | Dictionary<string>) => {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof env === 'string') {
|
||||
// a single `--env` arg comes in as a String
|
||||
env = [env];
|
||||
}
|
||||
|
||||
if (Array.isArray(env)) {
|
||||
return env.reduce((o, e) => {
|
||||
let key;
|
||||
let value;
|
||||
const equalsSign = e.indexOf('=');
|
||||
|
||||
if (equalsSign === -1) {
|
||||
key = e;
|
||||
} else {
|
||||
key = e.slice(0, equalsSign);
|
||||
value = e.slice(equalsSign + 1);
|
||||
}
|
||||
|
||||
o[key] = value;
|
||||
return o;
|
||||
}, {} as Dictionary<string | undefined>);
|
||||
}
|
||||
|
||||
// assume it's already an Object
|
||||
return env;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import readConfig from '../../util/config/read-config';
|
||||
import readJSONFile from '../../util/read-json-file';
|
||||
import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import { CantParseJSONFile } from '../../util/errors-ts';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
dev: ['dev'],
|
||||
@@ -136,7 +137,7 @@ export default async function main(client: Client) {
|
||||
try {
|
||||
return await dev(client, argv, args);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
if (isErrnoException(err) && err.code === 'ENOTFOUND') {
|
||||
// Error message will look like the following:
|
||||
// "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
|
||||
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
|
||||
@@ -148,7 +149,9 @@ export default async function main(client: Client) {
|
||||
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
if (typeof err.stack === 'string') {
|
||||
output.debug(err.stack);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
output.prettyError(err);
|
||||
|
||||
@@ -21,18 +21,7 @@ export default async function add(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const parsedParams = parseAddDNSRecordArgs(args);
|
||||
if (!parsedParams) {
|
||||
|
||||
@@ -14,18 +14,7 @@ export default async function add(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (args.length !== 2) {
|
||||
output.error(
|
||||
|
||||
@@ -24,18 +24,7 @@ export default async function ls(
|
||||
) {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
const lsStamp = stamp();
|
||||
|
||||
@@ -14,21 +14,11 @@ type Options = {};
|
||||
|
||||
export default async function rm(
|
||||
client: Client,
|
||||
opts: Options,
|
||||
_opts: Options,
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
try {
|
||||
await getScope(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const [recordId] = args;
|
||||
if (args.length !== 1) {
|
||||
|
||||
@@ -26,18 +26,7 @@ export default async function add(
|
||||
) {
|
||||
const { output } = client;
|
||||
const force = opts['--force'];
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const project = await getLinkedProject(client).then(result => {
|
||||
if (result.status === 'linked') {
|
||||
|
||||
@@ -11,6 +11,7 @@ import promptBool from '../../util/input/prompt-bool';
|
||||
import purchaseDomain from '../../util/domains/purchase-domain';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { errorToString } from '../../util/is-error';
|
||||
|
||||
type Options = {};
|
||||
|
||||
@@ -20,18 +21,7 @@ export default async function buy(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
if (!domainName) {
|
||||
@@ -68,6 +58,11 @@ export default async function buy(
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (renewalPrice instanceof Error) {
|
||||
output.prettyError(renewalPrice);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(await getDomainStatus(client, domainName)).available) {
|
||||
output.error(
|
||||
`The domain ${param(domainName)} is ${chalk.underline(
|
||||
@@ -109,11 +104,11 @@ export default async function buy(
|
||||
|
||||
try {
|
||||
buyResult = await purchaseDomain(client, domainName, price, autoRenew);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.error(
|
||||
'An unexpected error occurred while purchasing your domain. Please try again later.'
|
||||
);
|
||||
output.debug(`Server response: ${err.message}`);
|
||||
output.debug(`Server response: ${errorToString(err)}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,19 +23,7 @@ export default async function inspect(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
const inspectStamp = stamp();
|
||||
|
||||
@@ -25,23 +25,13 @@ export default async function ls(
|
||||
) {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
let contextName = null;
|
||||
|
||||
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
|
||||
output.error('Please provide a number for flag --next');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const lsStamp = stamp();
|
||||
|
||||
|
||||
@@ -25,20 +25,7 @@ export default async function move(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
let user = null;
|
||||
|
||||
try {
|
||||
({ contextName, user } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { contextName, user } = await getScope(client);
|
||||
const { domainName, destination } = await getArgs(args);
|
||||
if (!isRootDomain(domainName)) {
|
||||
output.error(
|
||||
|
||||
@@ -29,18 +29,7 @@ export default async function rm(
|
||||
) {
|
||||
const { output } = client;
|
||||
const [domainName] = args;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (!domainName) {
|
||||
output.error(
|
||||
@@ -122,10 +111,10 @@ async function removeDomain(
|
||||
output.debug(`Removing alias ${id}`);
|
||||
try {
|
||||
await removeAliasById(client, id);
|
||||
} catch (error) {
|
||||
} catch (err: unknown) {
|
||||
// Ignore if the alias does not exist anymore
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
if (!ERRORS.isAPIError(err) || err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,10 +123,10 @@ async function removeDomain(
|
||||
output.debug(`Removing cert ${id}`);
|
||||
try {
|
||||
await deleteCertById(output, client, id);
|
||||
} catch (error) {
|
||||
} catch (err: unknown) {
|
||||
// Ignore if the cert does not exist anymore
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
if (!ERRORS.isAPIError(err) || err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,7 @@ export default async function transferIn(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
if (!domainName) {
|
||||
|
||||
18
packages/cli/src/commands/env/add.ts
vendored
18
packages/cli/src/commands/env/add.ts
vendored
@@ -1,5 +1,4 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { ProjectEnvTarget, Project, ProjectEnvType } from '../../types';
|
||||
import { Output } from '../../util/output';
|
||||
import Client from '../../util/client';
|
||||
@@ -16,6 +15,7 @@ import param from '../../util/output/param';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isKnownError } from '../../util/env/known-error';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -66,7 +66,7 @@ export default async function add(
|
||||
}
|
||||
|
||||
while (!envName) {
|
||||
const { inputName } = await inquirer.prompt({
|
||||
const { inputName } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputName',
|
||||
message: `What’s the name of the variable?`,
|
||||
@@ -106,7 +106,7 @@ export default async function add(
|
||||
if (stdInput) {
|
||||
envValue = stdInput;
|
||||
} else {
|
||||
const { inputValue } = await inquirer.prompt({
|
||||
const { inputValue } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputValue',
|
||||
message: `What’s the value of ${envName}?`,
|
||||
@@ -116,7 +116,7 @@ export default async function add(
|
||||
}
|
||||
|
||||
while (envTargets.length === 0) {
|
||||
const { inputTargets } = await inquirer.prompt({
|
||||
const { inputTargets } = await client.prompt({
|
||||
name: 'inputTargets',
|
||||
type: 'checkbox',
|
||||
message: `Add ${envName} to which Environments (select multiple)?`,
|
||||
@@ -136,7 +136,7 @@ export default async function add(
|
||||
envTargets.length === 1 &&
|
||||
envTargets[0] === ProjectEnvTarget.Preview
|
||||
) {
|
||||
const { inputValue } = await inquirer.prompt({
|
||||
const { inputValue } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputValue',
|
||||
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
|
||||
@@ -157,12 +157,12 @@ export default async function add(
|
||||
envTargets,
|
||||
envGitBranch
|
||||
);
|
||||
} catch (error) {
|
||||
if (isKnownError(error) && error.serverMessage) {
|
||||
output.error(error.serverMessage);
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && isKnownError(err)) {
|
||||
output.error(err.serverMessage);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
|
||||
output.print(
|
||||
|
||||
21
packages/cli/src/commands/env/index.ts
vendored
21
packages/cli/src/commands/env/index.ts
vendored
@@ -1,7 +1,9 @@
|
||||
import chalk from 'chalk';
|
||||
import { ProjectEnvTarget } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { getEnvTargetPlaceholder } from '../../util/env/env-target';
|
||||
import {
|
||||
getEnvTargetPlaceholder,
|
||||
isValidEnvTarget,
|
||||
} from '../../util/env/env-target';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import getSubcommand from '../../util/get-subcommand';
|
||||
@@ -29,6 +31,7 @@ const help = () => {
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
--environment Set the Environment (development, preview, production) when pulling Environment Variables
|
||||
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
|
||||
'FILE'
|
||||
)} Path to the local ${'`vercel.json`'} file
|
||||
@@ -111,6 +114,7 @@ export default async function main(client: Client) {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--yes': Boolean,
|
||||
'-y': '--yes',
|
||||
'--environment': String,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
@@ -126,6 +130,17 @@ export default async function main(client: Client) {
|
||||
const subArgs = argv._.slice(1);
|
||||
const { subcommand, args } = getSubcommand(subArgs, COMMAND_CONFIG);
|
||||
const { output, config } = client;
|
||||
|
||||
const target = argv['--environment']?.toLowerCase() || 'development';
|
||||
if (!isValidEnvTarget(target)) {
|
||||
output.error(
|
||||
`Invalid environment \`${chalk.cyan(
|
||||
target
|
||||
)}\`. Valid options: ${getEnvTargetPlaceholder()}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'error') {
|
||||
return link.exitCode;
|
||||
@@ -150,7 +165,7 @@ export default async function main(client: Client) {
|
||||
return pull(
|
||||
client,
|
||||
project,
|
||||
ProjectEnvTarget.Development,
|
||||
target,
|
||||
argv,
|
||||
args,
|
||||
output,
|
||||
|
||||
35
packages/cli/src/commands/env/pull.ts
vendored
35
packages/cli/src/commands/env/pull.ts
vendored
@@ -14,6 +14,11 @@ import param from '../../util/output/param';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { EnvRecordsSource } from '../../util/env/get-env-records';
|
||||
import {
|
||||
buildDeltaString,
|
||||
createEnvObject,
|
||||
} from '../../util/env/diff-env-files';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
@@ -36,8 +41,8 @@ function readHeadSync(path: string, length: number) {
|
||||
function tryReadHeadSync(path: string, length: number) {
|
||||
try {
|
||||
return readHeadSync(path, length);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (!isErrnoException(err) || err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +74,7 @@ export default async function pull(
|
||||
const exists = typeof head !== 'undefined';
|
||||
|
||||
if (head === CONTENTS_PREFIX) {
|
||||
output.print(`Overwriting existing ${chalk.bold(filename)} file\n`);
|
||||
output.log(`Overwriting existing ${chalk.bold(filename)} file`);
|
||||
} else if (
|
||||
exists &&
|
||||
!skipConfirmation &&
|
||||
@@ -83,10 +88,10 @@ export default async function pull(
|
||||
return 0;
|
||||
}
|
||||
|
||||
output.print(
|
||||
`Downloading "${environment}" Environment Variables for Project ${chalk.bold(
|
||||
project.name
|
||||
)}\n`
|
||||
output.log(
|
||||
`Downloading \`${chalk.cyan(
|
||||
environment
|
||||
)}\` Environment Variables for Project ${chalk.bold(project.name)}`
|
||||
);
|
||||
|
||||
const pullStamp = stamp();
|
||||
@@ -107,6 +112,15 @@ export default async function pull(
|
||||
environment
|
||||
);
|
||||
|
||||
let deltaString = '';
|
||||
let oldEnv;
|
||||
if (exists) {
|
||||
oldEnv = await createEnvObject(fullPath, output);
|
||||
if (oldEnv) {
|
||||
deltaString = buildDeltaString(oldEnv, records);
|
||||
}
|
||||
}
|
||||
|
||||
const contents =
|
||||
CONTENTS_PREFIX +
|
||||
Object.entries(records)
|
||||
@@ -125,6 +139,13 @@ export default async function pull(
|
||||
)}\n`
|
||||
);
|
||||
|
||||
output.print('\n');
|
||||
if (deltaString) {
|
||||
output.print(deltaString);
|
||||
} else if (oldEnv && exists) {
|
||||
output.log('No changes found.');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
9
packages/cli/src/commands/env/rm.ts
vendored
9
packages/cli/src/commands/env/rm.ts
vendored
@@ -16,6 +16,7 @@ import param from '../../util/output/param';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isKnownError } from '../../util/env/known-error';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -120,12 +121,12 @@ export default async function rm(
|
||||
try {
|
||||
output.spinner('Removing');
|
||||
await removeEnvRecord(output, client, project.id, env);
|
||||
} catch (error) {
|
||||
if (isKnownError(error) && error.serverMessage) {
|
||||
output.error(error.serverMessage);
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && isKnownError(err)) {
|
||||
output.error(err.serverMessage);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
|
||||
output.print(
|
||||
|
||||
@@ -2,8 +2,9 @@ import chalk from 'chalk';
|
||||
import { join } from 'path';
|
||||
import { Org, Project } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { parseGitConfig, pluckRemoteUrl } from '../../util/create-git-meta';
|
||||
import { parseGitConfig, pluckRemoteUrls } from '../../util/create-git-meta';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import list, { ListChoice } from '../../util/input/list';
|
||||
import { Output } from '../../util/output';
|
||||
import link from '../../util/output/link';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
@@ -64,20 +65,37 @@ export default async function connect(
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const remoteUrl = pluckRemoteUrl(gitConfig);
|
||||
if (!remoteUrl) {
|
||||
const remoteUrls = pluckRemoteUrls(gitConfig);
|
||||
if (!remoteUrls) {
|
||||
output.error(
|
||||
`No remote origin URL found in your Git config. Make sure you've configured a remote repo in your local Git config. Run ${chalk.cyan(
|
||||
`No remote URLs found in your Git config. Make sure you've configured a remote repo in your local Git config. Run ${chalk.cyan(
|
||||
'`git remote --help`'
|
||||
)} for more details.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
output.log(`Identified Git remote "origin": ${link(remoteUrl)}`);
|
||||
|
||||
let remoteUrl: string;
|
||||
|
||||
if (Object.keys(remoteUrls).length > 1) {
|
||||
output.log(`Found multiple remote URLs.`);
|
||||
remoteUrl = await selectRemoteUrl(client, remoteUrls);
|
||||
} else {
|
||||
// If only one is found, get it — usually "origin"
|
||||
remoteUrl = Object.values(remoteUrls)[0];
|
||||
}
|
||||
|
||||
if (remoteUrl === '') {
|
||||
output.log('Aborted.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
output.log(`Connecting Git remote: ${link(remoteUrl)}`);
|
||||
|
||||
const parsedUrl = parseRepoUrl(remoteUrl);
|
||||
if (!parsedUrl) {
|
||||
output.error(
|
||||
`Failed to parse Git repo data from the following remote URL in your Git config: ${link(
|
||||
`Failed to parse Git repo data from the following remote URL: ${link(
|
||||
remoteUrl
|
||||
)}`
|
||||
);
|
||||
@@ -166,3 +184,22 @@ async function confirmRepoConnect(
|
||||
}
|
||||
return shouldReplaceProject;
|
||||
}
|
||||
|
||||
async function selectRemoteUrl(
|
||||
client: Client,
|
||||
remoteUrls: { [key: string]: string }
|
||||
): Promise<string> {
|
||||
let choices: ListChoice[] = [];
|
||||
for (const [urlKey, urlValue] of Object.entries(remoteUrls)) {
|
||||
choices.push({
|
||||
name: `${urlValue} ${chalk.gray(`(${urlKey})`)}`,
|
||||
value: urlValue,
|
||||
short: urlKey,
|
||||
});
|
||||
}
|
||||
|
||||
return await list(client, {
|
||||
message: 'Which remote do you want to connect?',
|
||||
choices,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import init from './init';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import { isError } from '../../util/is-error';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
init: ['init'],
|
||||
@@ -70,9 +71,11 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
return await init(client, argv, args);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.prettyError(err);
|
||||
if (isError(err) && typeof err.stack === 'string') {
|
||||
output.debug(err.stack);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import Client from '../util/client';
|
||||
import { getDeployment } from '../util/get-deployment';
|
||||
import { Deployment } from '@vercel/client';
|
||||
import { Build } from '../types';
|
||||
import title from 'title';
|
||||
import { isErrnoException } from '../util/is-error';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -75,8 +78,11 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
) {
|
||||
error(err.message);
|
||||
return 1;
|
||||
}
|
||||
@@ -92,7 +98,8 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
deployment = await getDeployment(client, deploymentIdOrHost);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 404) {
|
||||
error(
|
||||
`Failed to find deployment "${deploymentIdOrHost}" in ${chalk.bold(
|
||||
@@ -109,11 +116,20 @@ export default async function main(client: Client) {
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// unexpected
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { id, name, url, createdAt, routes, readyState } = deployment;
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
createdAt,
|
||||
routes,
|
||||
readyState,
|
||||
alias: aliases,
|
||||
} = deployment;
|
||||
|
||||
const { builds } =
|
||||
deployment.version === 2
|
||||
@@ -121,20 +137,20 @@ export default async function main(client: Client) {
|
||||
: { builds: [] };
|
||||
|
||||
log(
|
||||
`Fetched deployment "${url}" in ${chalk.bold(contextName)} ${elapsed(
|
||||
Date.now() - depFetchStart
|
||||
)}`
|
||||
`Fetched deployment ${chalk.bold(url)} in ${chalk.bold(
|
||||
contextName
|
||||
)} ${elapsed(Date.now() - depFetchStart)}`
|
||||
);
|
||||
|
||||
print('\n');
|
||||
print(chalk.bold(' General\n\n'));
|
||||
print(` ${chalk.cyan('id')}\t\t${id}\n`);
|
||||
print(` ${chalk.cyan('name')}\t${name}\n`);
|
||||
print(` ${chalk.cyan('readyState')}\t${stateString(readyState)}\n`);
|
||||
print(` ${chalk.cyan('url')}\t\t${url}\n`);
|
||||
print(` ${chalk.cyan('status')}\t${stateString(readyState)}\n`);
|
||||
print(` ${chalk.cyan('url')}\t\thttps://${url}\n`);
|
||||
if (createdAt) {
|
||||
print(
|
||||
` ${chalk.cyan('createdAt')}\t${new Date(createdAt)} ${elapsed(
|
||||
` ${chalk.cyan('created')}\t${new Date(createdAt)} ${elapsed(
|
||||
Date.now() - createdAt,
|
||||
true
|
||||
)}\n`
|
||||
@@ -142,6 +158,16 @@ export default async function main(client: Client) {
|
||||
}
|
||||
print('\n\n');
|
||||
|
||||
if (aliases.length > 0) {
|
||||
print(chalk.bold(' Aliases\n\n'));
|
||||
let aliasList = '';
|
||||
for (const alias of aliases) {
|
||||
aliasList += `${chalk.gray('╶')} https://${alias}\n`;
|
||||
}
|
||||
print(indent(aliasList, 4));
|
||||
print('\n\n');
|
||||
}
|
||||
|
||||
if (builds.length > 0) {
|
||||
const times: { [id: string]: string | null } = {};
|
||||
|
||||
@@ -165,19 +191,24 @@ export default async function main(client: Client) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renders the state string
|
||||
function stateString(s: Deployment['readyState']) {
|
||||
const CIRCLE = '● ';
|
||||
const sTitle = s && title(s);
|
||||
switch (s) {
|
||||
case 'INITIALIZING':
|
||||
return chalk.yellow(s);
|
||||
|
||||
case 'BUILDING':
|
||||
case 'DEPLOYING':
|
||||
case 'ANALYZING':
|
||||
return chalk.yellow(CIRCLE) + sTitle;
|
||||
case 'ERROR':
|
||||
return chalk.red(s);
|
||||
|
||||
return chalk.red(CIRCLE) + sTitle;
|
||||
case 'READY':
|
||||
return s;
|
||||
|
||||
return chalk.green(CIRCLE) + sTitle;
|
||||
case 'QUEUED':
|
||||
return chalk.gray(CIRCLE) + sTitle;
|
||||
case 'CANCELED':
|
||||
return chalk.gray(CIRCLE) + sTitle;
|
||||
default:
|
||||
return chalk.gray(s || 'UNKNOWN');
|
||||
return chalk.gray('UNKNOWN');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import validatePaths from '../util/validate-paths';
|
||||
import { getLinkedProject } from '../util/projects/link';
|
||||
import { ensureLink } from '../util/ensure-link';
|
||||
import getScope from '../util/get-scope';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -152,16 +153,7 @@ export default async function main(client: Client) {
|
||||
|
||||
const { currentTeam } = config;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const nextTimestamp = argv['--next'];
|
||||
|
||||
@@ -228,8 +220,8 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
await now.findDeployment(app);
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 404) {
|
||||
debug('Ignore findDeployment 404');
|
||||
} else {
|
||||
throw err;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { writeToConfigFile, writeToAuthConfigFile } from '../util/config/files';
|
||||
import getArgs from '../util/get-args';
|
||||
import Client from '../util/client';
|
||||
import { getCommandName, getPkgName } from '../util/pkg-name';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { errorToString } from '../util/is-error';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -63,7 +65,8 @@ export default async function main(client: Client): Promise<number> {
|
||||
method: 'DELETE',
|
||||
useCurrentTeam: false,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 403) {
|
||||
output.debug('Token is invalid so it cannot be revoked');
|
||||
} else if (err.status !== 200) {
|
||||
@@ -71,6 +74,7 @@ export default async function main(client: Client): Promise<number> {
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete config.currentTeam;
|
||||
|
||||
@@ -86,8 +90,8 @@ export default async function main(client: Client): Promise<number> {
|
||||
writeToConfigFile(config);
|
||||
writeToAuthConfigFile(authConfig);
|
||||
output.debug('Configuration has been deleted');
|
||||
} catch (err) {
|
||||
output.debug(err?.message ?? '');
|
||||
} catch (err: unknown) {
|
||||
output.debug(errorToString(err));
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getPkgName } from '../util/pkg-name';
|
||||
import getArgs from '../util/get-args';
|
||||
import Client from '../util/client';
|
||||
import { getDeployment } from '../util/get-deployment';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -125,9 +126,10 @@ export default async function main(client: Client) {
|
||||
let deployment;
|
||||
try {
|
||||
deployment = await getDeployment(client, id);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.stopSpinner();
|
||||
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 404) {
|
||||
output.error(
|
||||
`Failed to find deployment "${id}" in ${chalk.bold(contextName)}`
|
||||
@@ -142,6 +144,7 @@ export default async function main(client: Client) {
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// unexpected
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
export default async function add(
|
||||
@@ -36,12 +37,12 @@ export default async function add(
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
@@ -72,18 +72,7 @@ export default async function main(client: Client) {
|
||||
subcommand = argv._[0] || 'list';
|
||||
const args = argv._.slice(1);
|
||||
const { output } = client;
|
||||
|
||||
let contextName = '';
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (error) {
|
||||
if (error.code === 'NOT_AUTHORIZED' || error.code === 'TEAM_DELETED') {
|
||||
output.error(error.message);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
switch (subcommand) {
|
||||
case 'ls':
|
||||
|
||||
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
@@ -32,8 +33,8 @@ export default async function rm(client: Client, args: string[]) {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 404) {
|
||||
client.output.error('No such project exists');
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ export default async function main(client: Client) {
|
||||
return pullResultCode;
|
||||
}
|
||||
|
||||
client.output.print('\n');
|
||||
client.output.log('Downloading project settings');
|
||||
await writeProjectSettings(cwd, project, org);
|
||||
|
||||
const settingsStamp = stamp();
|
||||
|
||||
@@ -114,18 +114,7 @@ export default async function main(client: Client) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let contextName: string | null = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
output.spinner(
|
||||
`Fetching deployment(s) ${ids
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import Client from '../../util/client';
|
||||
import createTeam from '../../util/teams/create-team';
|
||||
import patchTeam from '../../util/teams/patch-team';
|
||||
import { errorToString, isError } from '../../util/is-error';
|
||||
|
||||
const validateSlugKeypress = (data: string, value: string) =>
|
||||
// TODO: the `value` here should contain the current value + the keypress
|
||||
@@ -56,8 +57,8 @@ export default async function add(client: Client): Promise<number> {
|
||||
valid: team,
|
||||
forceLowerCase: true,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.message === 'USER_ABORT') {
|
||||
} catch (err: unknown) {
|
||||
if (isError(err) && err.message === 'USER_ABORT') {
|
||||
output.log('Aborted');
|
||||
return 0;
|
||||
}
|
||||
@@ -71,10 +72,10 @@ export default async function add(client: Client): Promise<number> {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
team = await createTeam(client, { slug });
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.stopSpinner();
|
||||
output.print(eraseLines(2));
|
||||
output.error(err.message);
|
||||
output.error(errorToString(err));
|
||||
}
|
||||
} while (!team);
|
||||
|
||||
@@ -92,8 +93,8 @@ export default async function add(client: Client): Promise<number> {
|
||||
label: `- ${teamNamePrefix}`,
|
||||
validateKeypress: validateNameKeypress,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.message === 'USER_ABORT') {
|
||||
} catch (err: unknown) {
|
||||
if (isError(err) && err.message === 'USER_ABORT') {
|
||||
console.log(info('No name specified'));
|
||||
return gracefulExit();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { getCommandName } from '../../util/pkg-name';
|
||||
import { email as regexEmail } from '../../util/input/regexes';
|
||||
import getTeams from '../../util/teams/get-teams';
|
||||
import inviteUserToTeam from '../../util/teams/invite-user-to-team';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import { errorToString, isError } from '../../util/is-error';
|
||||
|
||||
const validateEmail = (data: string) =>
|
||||
regexEmail.test(data.trim()) || data.length === 0;
|
||||
@@ -67,17 +69,7 @@ export default async function invite(
|
||||
const currentTeam = teams.find(team => team.id === currentTeamId);
|
||||
|
||||
output.spinner('Fetching user information');
|
||||
let user;
|
||||
try {
|
||||
user = await getUser(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const user = await getUser(client);
|
||||
|
||||
domains.push(user.email.split('@')[1]);
|
||||
|
||||
@@ -107,8 +99,8 @@ export default async function invite(
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const res = await inviteUserToTeam(client, currentTeam.id, email);
|
||||
userInfo = res.username;
|
||||
} catch (err) {
|
||||
if (err.code === 'user_not_found') {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.code === 'user_not_found') {
|
||||
output.error(`No user exists with the email address "${email}".`);
|
||||
return 1;
|
||||
}
|
||||
@@ -141,8 +133,8 @@ export default async function invite(
|
||||
validateValue: validateEmail,
|
||||
autoComplete: value => emailAutoComplete(value, currentTeam.slug),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.message !== 'USER_ABORT') {
|
||||
} catch (err: unknown) {
|
||||
if (!isError(err) || err.message !== 'USER_ABORT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -174,7 +166,7 @@ export default async function invite(
|
||||
} catch (err) {
|
||||
output.stopSpinner();
|
||||
process.stderr.write(eraseLines(emails.length + 2));
|
||||
output.error(err.message);
|
||||
output.error(errorToString(err));
|
||||
hasError = true;
|
||||
for (const email of emails) {
|
||||
output.log(`${chalk.cyan(chars.tick)} ${sentEmailPrefix}${email}`);
|
||||
|
||||
@@ -43,17 +43,7 @@ export default async function list(client: Client): Promise<number> {
|
||||
const accountIsCurrent = !currentTeam;
|
||||
|
||||
output.spinner('Fetching user information');
|
||||
let user;
|
||||
try {
|
||||
user = await getUser(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const user = await getUser(client);
|
||||
|
||||
if (accountIsCurrent) {
|
||||
currentTeam = user.id;
|
||||
|
||||
@@ -41,18 +41,7 @@ export default async (client: Client): Promise<number> => {
|
||||
return 2;
|
||||
}
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client, { getTeam: false }));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client, { getTeam: false });
|
||||
|
||||
if (client.stdout.isTTY) {
|
||||
output.log(contextName);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { isErrnoException, isError, errorToString } from './util/is-error';
|
||||
|
||||
try {
|
||||
// Test to see if cwd has been deleted before
|
||||
// importing 3rd party packages that might need cwd.
|
||||
process.cwd();
|
||||
} catch (e) {
|
||||
if (e && e.message && e.message.includes('uv_cwd')) {
|
||||
} catch (err) {
|
||||
if (isError(err) && err.message.includes('uv_cwd')) {
|
||||
console.error('Error! The current working directory does not exist.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -103,7 +104,7 @@ const main = async () => {
|
||||
},
|
||||
{ permissive: true }
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
handleError(err);
|
||||
return 1;
|
||||
}
|
||||
@@ -201,14 +202,11 @@ const main = async () => {
|
||||
// Ensure that the Vercel global configuration directory exists
|
||||
try {
|
||||
await mkdirp(VERCEL_DIR);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
error(
|
||||
`${
|
||||
'An unexpected error occurred while trying to create the ' +
|
||||
`global directory "${hp(VERCEL_DIR)}" `
|
||||
}${err.message}`
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
output.error(
|
||||
`An unexpected error occurred while trying to create the global directory "${hp(
|
||||
VERCEL_DIR
|
||||
)}" ${errorToString(err)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,13 +215,13 @@ const main = async () => {
|
||||
|
||||
try {
|
||||
configExists = existsSync(VERCEL_CONFIG_PATH);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error(
|
||||
error(
|
||||
`${
|
||||
'An unexpected error occurred while trying to find the ' +
|
||||
`config file "${hp(VERCEL_CONFIG_PATH)}" `
|
||||
}${err.message}`
|
||||
}${errorToString(err)}`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -241,7 +239,7 @@ const main = async () => {
|
||||
`${
|
||||
'An unexpected error occurred while trying to read the ' +
|
||||
`config in "${hp(VERCEL_CONFIG_PATH)}" `
|
||||
}${err.message}`
|
||||
}${errorToString(err)}`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -272,13 +270,13 @@ const main = async () => {
|
||||
|
||||
try {
|
||||
configFiles.writeToConfigFile(config);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error(
|
||||
error(
|
||||
`${
|
||||
'An unexpected error occurred while trying to write the ' +
|
||||
`default config to "${hp(VERCEL_CONFIG_PATH)}" `
|
||||
}${err.message}`
|
||||
}${errorToString(err)}`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -290,13 +288,13 @@ const main = async () => {
|
||||
|
||||
try {
|
||||
authConfigExists = existsSync(VERCEL_AUTH_CONFIG_PATH);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error(
|
||||
error(
|
||||
`${
|
||||
'An unexpected error occurred while trying to find the ' +
|
||||
`auth file "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
|
||||
}${err.message}`
|
||||
}${errorToString(err)}`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -317,13 +315,13 @@ const main = async () => {
|
||||
if (authConfigExists) {
|
||||
try {
|
||||
authConfig = configFiles.readAuthConfigFile();
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error(
|
||||
error(
|
||||
`${
|
||||
'An unexpected error occurred while trying to read the ' +
|
||||
`auth config in "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
|
||||
}${err.message}`
|
||||
}${errorToString(err)}`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -345,13 +343,13 @@ const main = async () => {
|
||||
|
||||
try {
|
||||
configFiles.writeToAuthConfigFile(authConfig);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error(
|
||||
error(
|
||||
`${
|
||||
'An unexpected error occurred while trying to write the ' +
|
||||
`default config to "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
|
||||
}${err.message}`
|
||||
}${errorToString(err)}`
|
||||
)
|
||||
);
|
||||
return 1;
|
||||
@@ -543,8 +541,8 @@ const main = async () => {
|
||||
|
||||
try {
|
||||
user = await getUser(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'NOT_AUTHORIZED') {
|
||||
output.prettyError({
|
||||
message: `You do not have access to the specified account`,
|
||||
link: 'https://err.sh/vercel/scope-not-accessible',
|
||||
@@ -564,8 +562,8 @@ const main = async () => {
|
||||
|
||||
try {
|
||||
teams = await getTeams(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'not_authorized') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'not_authorized') {
|
||||
output.prettyError({
|
||||
message: `You do not have access to the specified team`,
|
||||
link: 'https://err.sh/vercel/scope-not-accessible',
|
||||
@@ -594,8 +592,8 @@ const main = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const metric = metrics();
|
||||
let exitCode;
|
||||
let metric: ReturnType<typeof metrics> | undefined;
|
||||
const eventCategory = 'Exit Code';
|
||||
|
||||
try {
|
||||
@@ -698,13 +696,14 @@ const main = async () => {
|
||||
if (shouldCollectMetrics) {
|
||||
const category = 'Command Invocation';
|
||||
|
||||
if (!metric) metric = metrics();
|
||||
metric
|
||||
.timing(category, targetCommand, end, pkg.version)
|
||||
.event(category, targetCommand, pkg.version)
|
||||
.send();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'ENOTFOUND') {
|
||||
// Error message will look like the following:
|
||||
// "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
|
||||
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
|
||||
@@ -716,11 +715,16 @@ const main = async () => {
|
||||
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
if (typeof err.stack === 'string') {
|
||||
output.debug(err.stack);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
) {
|
||||
output.prettyError(err);
|
||||
return 1;
|
||||
}
|
||||
@@ -732,9 +736,10 @@ const main = async () => {
|
||||
}
|
||||
|
||||
if (shouldCollectMetrics) {
|
||||
if (!metric) metric = metrics();
|
||||
metric
|
||||
.event(eventCategory, '1', pkg.version)
|
||||
.exception(err.message)
|
||||
.exception(errorToString(err))
|
||||
.send();
|
||||
}
|
||||
|
||||
@@ -742,23 +747,24 @@ const main = async () => {
|
||||
// but instead show the message. Any error that is handled by this should
|
||||
// actually be handled in the sub command instead. Please make sure
|
||||
// that happens for anything that lands here. It should NOT bubble up to here.
|
||||
if (err.code) {
|
||||
if (isErrnoException(err)) {
|
||||
if (typeof err.stack === 'string') {
|
||||
output.debug(err.stack);
|
||||
}
|
||||
output.prettyError(err);
|
||||
} else {
|
||||
await reportError(Sentry, client, err);
|
||||
|
||||
// Otherwise it is an unexpected error and we should show the trace
|
||||
// and an unexpected error message
|
||||
output.error(
|
||||
`An unexpected error occurred in ${subcommand}: ${err.stack}`
|
||||
);
|
||||
output.error(`An unexpected error occurred in ${subcommand}: ${err}`);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (shouldCollectMetrics) {
|
||||
if (!metric) metric = metrics();
|
||||
metric.event(eventCategory, `${exitCode}`, pkg.version).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,12 @@ export interface ProjectLinkData {
|
||||
|
||||
export interface Project extends ProjectSettings {
|
||||
id: string;
|
||||
analytics?: {
|
||||
id: string;
|
||||
enabledAt?: number;
|
||||
disabledAt?: number;
|
||||
canceledAt?: number | null;
|
||||
};
|
||||
name: string;
|
||||
accountId: string;
|
||||
updatedAt: number;
|
||||
|
||||
@@ -68,37 +68,39 @@ async function performCreateAlias(
|
||||
body: { alias },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code === 'cert_missing' || error.code === 'cert_expired') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'cert_missing' || err.code === 'cert_expired') {
|
||||
return new ERRORS.CertMissing(alias);
|
||||
}
|
||||
if (error.status === 409) {
|
||||
return { uid: error.uid, alias: error.alias } as AliasRecord;
|
||||
if (err.status === 409) {
|
||||
return { uid: err.uid, alias: err.alias } as AliasRecord;
|
||||
}
|
||||
if (error.code === 'deployment_not_found') {
|
||||
if (err.code === 'deployment_not_found') {
|
||||
return new ERRORS.DeploymentNotFound({
|
||||
context: contextName,
|
||||
id: deployment.uid,
|
||||
});
|
||||
}
|
||||
if (error.code === 'gone') {
|
||||
if (err.code === 'gone') {
|
||||
return new ERRORS.DeploymentFailedAliasImpossible();
|
||||
}
|
||||
if (error.code === 'invalid_alias') {
|
||||
if (err.code === 'invalid_alias') {
|
||||
return new ERRORS.InvalidAlias(alias);
|
||||
}
|
||||
if (error.status === 403) {
|
||||
if (error.code === 'alias_in_use') {
|
||||
if (err.status === 403) {
|
||||
if (err.code === 'alias_in_use') {
|
||||
return new ERRORS.AliasInUse(alias);
|
||||
}
|
||||
if (error.code === 'forbidden') {
|
||||
if (err.code === 'forbidden') {
|
||||
return new ERRORS.DomainPermissionDenied(alias, contextName);
|
||||
}
|
||||
}
|
||||
if (error.status === 400) {
|
||||
if (err.status === 400) {
|
||||
return new ERRORS.DeploymentNotReady({ url: deployment.url });
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,31 +23,47 @@ import {
|
||||
download,
|
||||
EdgeFunction,
|
||||
BuildResultBuildOutput,
|
||||
getLambdaOptionsFromFunction,
|
||||
normalizePath,
|
||||
} from '@vercel/build-utils';
|
||||
import pipe from 'promisepipe';
|
||||
import { unzip } from './unzip';
|
||||
import { VERCEL_DIR } from '../projects/link';
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
|
||||
const { normalize } = posix;
|
||||
export const OUTPUT_DIR = join(VERCEL_DIR, 'output');
|
||||
|
||||
/**
|
||||
* An entry in the "functions" object in `vercel.json`.
|
||||
*/
|
||||
interface FunctionConfiguration {
|
||||
memory?: number;
|
||||
maxDuration?: number;
|
||||
}
|
||||
|
||||
export async function writeBuildResult(
|
||||
outputDir: string,
|
||||
buildResult: BuildResultV2 | BuildResultV3,
|
||||
build: Builder,
|
||||
builder: BuilderV2 | BuilderV3,
|
||||
builderPkg: PackageJson,
|
||||
cleanUrls?: boolean
|
||||
vercelConfig: VercelConfig | null
|
||||
) {
|
||||
const { version } = builder;
|
||||
if (typeof version !== 'number' || version === 2) {
|
||||
return writeBuildResultV2(
|
||||
outputDir,
|
||||
buildResult as BuildResultV2,
|
||||
cleanUrls
|
||||
vercelConfig
|
||||
);
|
||||
} else if (version === 3) {
|
||||
return writeBuildResultV3(outputDir, buildResult as BuildResultV3, build);
|
||||
return writeBuildResultV3(
|
||||
outputDir,
|
||||
buildResult as BuildResultV3,
|
||||
build,
|
||||
vercelConfig
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported Builder version \`${version}\` from "${builderPkg.name}"`
|
||||
@@ -90,7 +106,7 @@ function stripDuplicateSlashes(path: string): string {
|
||||
async function writeBuildResultV2(
|
||||
outputDir: string,
|
||||
buildResult: BuildResultV2,
|
||||
cleanUrls?: boolean
|
||||
vercelConfig: VercelConfig | null
|
||||
) {
|
||||
if ('buildOutputPath' in buildResult) {
|
||||
await mergeBuilderOutput(outputDir, buildResult);
|
||||
@@ -102,9 +118,15 @@ async function writeBuildResultV2(
|
||||
for (const [path, output] of Object.entries(buildResult.output)) {
|
||||
const normalizedPath = stripDuplicateSlashes(path);
|
||||
if (isLambda(output)) {
|
||||
await writeLambda(outputDir, output, normalizedPath, lambdas);
|
||||
await writeLambda(outputDir, output, normalizedPath, undefined, lambdas);
|
||||
} else if (isPrerender(output)) {
|
||||
await writeLambda(outputDir, output.lambda, normalizedPath, lambdas);
|
||||
await writeLambda(
|
||||
outputDir,
|
||||
output.lambda,
|
||||
normalizedPath,
|
||||
undefined,
|
||||
lambdas
|
||||
);
|
||||
|
||||
// Write the fallback file alongside the Lambda directory
|
||||
let fallback = output.fallback;
|
||||
@@ -140,7 +162,7 @@ async function writeBuildResultV2(
|
||||
output,
|
||||
normalizedPath,
|
||||
overrides,
|
||||
cleanUrls
|
||||
vercelConfig?.cleanUrls
|
||||
);
|
||||
} else if (isEdgeFunction(output)) {
|
||||
await writeEdgeFunction(outputDir, output, normalizedPath);
|
||||
@@ -162,19 +184,28 @@ async function writeBuildResultV2(
|
||||
async function writeBuildResultV3(
|
||||
outputDir: string,
|
||||
buildResult: BuildResultV3,
|
||||
build: Builder
|
||||
build: Builder,
|
||||
vercelConfig: VercelConfig | null
|
||||
) {
|
||||
const { output } = buildResult;
|
||||
const src = build.src;
|
||||
if (typeof src !== 'string') {
|
||||
throw new Error(`Expected "build.src" to be a string`);
|
||||
}
|
||||
|
||||
const functionConfiguration = vercelConfig
|
||||
? await getLambdaOptionsFromFunction({
|
||||
sourceFile: src,
|
||||
config: vercelConfig,
|
||||
})
|
||||
: {};
|
||||
|
||||
const ext = extname(src);
|
||||
const path = stripDuplicateSlashes(
|
||||
build.config?.zeroConfig ? src.substring(0, src.length - ext.length) : src
|
||||
);
|
||||
if (isLambda(output)) {
|
||||
await writeLambda(outputDir, output, path);
|
||||
await writeLambda(outputDir, output, path, functionConfiguration);
|
||||
} else if (isEdgeFunction(output)) {
|
||||
await writeEdgeFunction(outputDir, output, path);
|
||||
} else {
|
||||
@@ -260,6 +291,7 @@ async function writeEdgeFunction(
|
||||
const config = {
|
||||
runtime: 'edge',
|
||||
...edgeFunction,
|
||||
entrypoint: normalizePath(edgeFunction.entrypoint),
|
||||
files: undefined,
|
||||
type: undefined,
|
||||
};
|
||||
@@ -283,6 +315,7 @@ async function writeLambda(
|
||||
outputDir: string,
|
||||
lambda: Lambda,
|
||||
path: string,
|
||||
functionConfiguration?: FunctionConfiguration,
|
||||
lambdas?: Map<Lambda, string>
|
||||
) {
|
||||
const dest = join(outputDir, 'functions', `${path}.func`);
|
||||
@@ -317,8 +350,14 @@ async function writeLambda(
|
||||
throw new Error('Malformed `Lambda` - no "files" present');
|
||||
}
|
||||
|
||||
const memory = functionConfiguration?.memory ?? lambda.memory;
|
||||
const maxDuration = functionConfiguration?.maxDuration ?? lambda.maxDuration;
|
||||
|
||||
const config = {
|
||||
...lambda,
|
||||
handler: normalizePath(lambda.handler),
|
||||
memory,
|
||||
maxDuration,
|
||||
type: undefined,
|
||||
files: undefined,
|
||||
zipBuffer: undefined,
|
||||
|
||||
@@ -15,17 +15,19 @@ export default async function createCertForCns(
|
||||
try {
|
||||
const certificate = await issueCert(client, cns);
|
||||
return certificate;
|
||||
} catch (error) {
|
||||
if (error.code === 'forbidden') {
|
||||
return new ERRORS.DomainPermissionDenied(error.domain, context);
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'forbidden') {
|
||||
return new ERRORS.DomainPermissionDenied(err.domain, context);
|
||||
}
|
||||
|
||||
const mappedError = mapCertError(error, cns);
|
||||
const mappedError = mapCertError(err, cns);
|
||||
if (mappedError) {
|
||||
return mappedError;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
} finally {
|
||||
output.stopSpinner();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import Client from '../client';
|
||||
import { Cert } from '../../types';
|
||||
import { isErrnoException } from '../is-error';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
|
||||
export default async function createCertFromFile(
|
||||
client: Client,
|
||||
@@ -25,15 +27,15 @@ export default async function createCertFromFile(
|
||||
},
|
||||
});
|
||||
return certificate;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return new Error(`The specified file "${error.path}" doesn't exist.`);
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'ENOENT') {
|
||||
return new Error(`The specified file "${err.path}" doesn't exist.`);
|
||||
}
|
||||
|
||||
if (error.status < 500) {
|
||||
return error;
|
||||
if (isAPIError(err) && err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,18 @@ export default async function startCertOrder(
|
||||
},
|
||||
});
|
||||
return cert;
|
||||
} catch (error) {
|
||||
if (error.code === 'cert_order_not_found') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'cert_order_not_found') {
|
||||
return new ERRORS.CertOrderNotFound(cns);
|
||||
}
|
||||
|
||||
const mappedError = mapCertError(error, cns);
|
||||
const mappedError = mapCertError(err, cns);
|
||||
if (mappedError) {
|
||||
return mappedError;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import * as ERRORS from '../errors-ts';
|
||||
export default async function getCertById(client: Client, id: string) {
|
||||
try {
|
||||
return await client.fetch<Cert>(`/v6/now/certs/${id}`);
|
||||
} catch (error) {
|
||||
if (error.code === 'cert_not_found') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err) && err.code === 'cert_not_found') {
|
||||
return new ERRORS.CertNotFound(id);
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ export async function getCustomCertsForDomain(
|
||||
`/v5/now/certs?${stringify({ domain, custom: true })}`
|
||||
);
|
||||
return certs;
|
||||
} catch (error) {
|
||||
if (error.code === 'forbidden') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err) && err.code === 'forbidden') {
|
||||
return new ERRORS.CertsPermissionDenied(context, domain);
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import retry from 'async-retry';
|
||||
import { Cert } from '../../types';
|
||||
import Client from '../client';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
import { isError } from '../is-error';
|
||||
|
||||
// When it's a configuration error we should retry because of the DNS propagation
|
||||
// otherwise we bail to handle the error in the upper level
|
||||
@@ -10,13 +12,15 @@ export default async function issueCert(client: Client, cns: string[]) {
|
||||
try {
|
||||
return await client.fetch<Cert>('/v3/now/certs', {
|
||||
method: 'POST',
|
||||
body: { domains: cns }
|
||||
body: { domains: cns },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'configuration_error') {
|
||||
throw error;
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.code === 'configuration_error') {
|
||||
throw err;
|
||||
} else if (isError(err)) {
|
||||
bail(err);
|
||||
} else {
|
||||
bail(error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as ERRORS from '../errors-ts';
|
||||
|
||||
export default function mapCertError(error: any, cns?: string[]) {
|
||||
export default function mapCertError(error: ERRORS.APIError, cns?: string[]) {
|
||||
const errorCode: string = error.code;
|
||||
if (errorCode === 'too_many_requests') {
|
||||
return new ERRORS.TooManyRequests('certificates', error.retryAfter);
|
||||
const retryAfter =
|
||||
typeof error.retryAfter === 'number' ? error.retryAfter : 0;
|
||||
return new ERRORS.TooManyRequests('certificates', retryAfter);
|
||||
}
|
||||
if (errorCode === 'not_found') {
|
||||
return new ERRORS.DomainNotFound(error.domain);
|
||||
|
||||
@@ -10,6 +10,7 @@ import error from '../output/error';
|
||||
import highlight from '../output/highlight';
|
||||
import { VercelConfig } from '../dev/types';
|
||||
import { AuthConfig, GlobalConfig } from '../../types';
|
||||
import { isErrnoException, isError } from '../is-error';
|
||||
|
||||
const VERCEL_DIR = getGlobalPathConfig();
|
||||
const CONFIG_FILE_PATH = join(VERCEL_DIR, 'config.json');
|
||||
@@ -25,8 +26,9 @@ export const readConfigFile = (): GlobalConfig => {
|
||||
export const writeToConfigFile = (stuff: GlobalConfig): void => {
|
||||
try {
|
||||
return writeJSON.sync(CONFIG_FILE_PATH, stuff, { indent: 2 });
|
||||
} catch (err) {
|
||||
if (err.code === 'EPERM') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err)) {
|
||||
if (isErrnoException(err) && err.code === 'EPERM') {
|
||||
console.error(
|
||||
error(
|
||||
`Not able to create ${highlight(
|
||||
@@ -45,6 +47,7 @@ export const writeToConfigFile = (stuff: GlobalConfig): void => {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
@@ -65,7 +68,8 @@ export const writeToAuthConfigFile = (authConfig: AuthConfig) => {
|
||||
indent: 2,
|
||||
mode: 0o600,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err)) {
|
||||
if (err.code === 'EPERM') {
|
||||
console.error(
|
||||
error(
|
||||
@@ -85,6 +89,7 @@ export const writeToAuthConfigFile = (authConfig: AuthConfig) => {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
@@ -123,12 +128,14 @@ export function readLocalConfig(
|
||||
if (existsSync(target)) {
|
||||
config = loadJSON.sync(target);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'JSONError') {
|
||||
} catch (err: unknown) {
|
||||
if (isError(err) && err.name === 'JSONError') {
|
||||
console.error(error(err.message));
|
||||
} else {
|
||||
} else if (isErrnoException(err)) {
|
||||
const code = err.code ? ` (${err.code})` : '';
|
||||
console.error(error(`Failed to read config file: ${target}${code}`));
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -3,76 +3,43 @@ import { join } from 'path';
|
||||
import ini from 'ini';
|
||||
import git from 'git-last-commit';
|
||||
import { exec } from 'child_process';
|
||||
import { GitMetadata } from '../types';
|
||||
import { GitMetadata, Project } from '../types';
|
||||
import { Output } from './output';
|
||||
|
||||
export function isDirty(directory: string, output: Output): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
|
||||
let debugMessage = `Failed to determine if Git repo has been modified:`;
|
||||
if (err || stderr) {
|
||||
if (err) debugMessage += `\n${err}`;
|
||||
if (stderr) debugMessage += `\n${stderr.trim()}`;
|
||||
output.debug(debugMessage);
|
||||
return resolve(false);
|
||||
}
|
||||
resolve(stdout.trim().length > 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLastCommit(directory: string): Promise<git.Commit> {
|
||||
return new Promise((resolve, reject) => {
|
||||
git.getLastCommit(
|
||||
(err, commit) => {
|
||||
if (err) return reject(err);
|
||||
resolve(commit);
|
||||
},
|
||||
{ dst: directory }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 = await parseGitConfig(configPath, output);
|
||||
if (!gitConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originUrl = pluckRemoteUrl(gitConfig);
|
||||
if (originUrl) {
|
||||
return originUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
import { errorToString } from './is-error';
|
||||
|
||||
export async function createGitMeta(
|
||||
directory: string,
|
||||
output: Output
|
||||
output: Output,
|
||||
project?: Project | null
|
||||
): Promise<GitMetadata | undefined> {
|
||||
const remoteUrl = await getRemoteUrl(join(directory, '.git/config'), output);
|
||||
// If a Git repository is already connected via `vc git`, use that remote url
|
||||
let remoteUrl;
|
||||
if (project?.link) {
|
||||
// in the form of org/repo
|
||||
const { repo } = project.link;
|
||||
|
||||
const remoteUrls = await getRemoteUrls(
|
||||
join(directory, '.git/config'),
|
||||
output
|
||||
);
|
||||
if (remoteUrls) {
|
||||
for (const urlValue of Object.values(remoteUrls)) {
|
||||
if (urlValue.includes(repo)) {
|
||||
remoteUrl = urlValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't get a remote url from the connected repo, default to the origin url
|
||||
if (!remoteUrl) {
|
||||
remoteUrl = await getOriginUrl(join(directory, '.git/config'), output);
|
||||
}
|
||||
// If we can't get the repo URL, then don't return any metadata
|
||||
if (!remoteUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [commit, dirty] = await Promise.all([
|
||||
getLastCommit(directory).catch(err => {
|
||||
output.debug(
|
||||
@@ -96,3 +63,97 @@ export async function createGitMeta(
|
||||
dirty,
|
||||
};
|
||||
}
|
||||
|
||||
function getLastCommit(directory: string): Promise<git.Commit> {
|
||||
return new Promise((resolve, reject) => {
|
||||
git.getLastCommit(
|
||||
(err, commit) => {
|
||||
if (err) return reject(err);
|
||||
resolve(commit);
|
||||
},
|
||||
{ dst: directory }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function isDirty(directory: string, output: Output): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
|
||||
let debugMessage = `Failed to determine if Git repo has been modified:`;
|
||||
if (err || stderr) {
|
||||
if (err) debugMessage += `\n${err}`;
|
||||
if (stderr) debugMessage += `\n${stderr.trim()}`;
|
||||
output.debug(debugMessage);
|
||||
return resolve(false);
|
||||
}
|
||||
resolve(stdout.trim().length > 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function parseGitConfig(configPath: string, output: Output) {
|
||||
try {
|
||||
return ini.parse(await fs.readFile(configPath, 'utf-8'));
|
||||
} catch (err: unknown) {
|
||||
output.debug(`Error while parsing repo data: ${errorToString(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function pluckRemoteUrls(gitConfig: {
|
||||
[key: string]: any;
|
||||
}): { [key: string]: string } | undefined {
|
||||
let remoteUrls: { [key: string]: string } = {};
|
||||
|
||||
for (const key of Object.keys(gitConfig)) {
|
||||
if (key.includes('remote')) {
|
||||
// ex. remote "origin" — matches origin
|
||||
const remoteName = key.match(/(?<=").*(?=")/g)?.[0];
|
||||
const remoteUrl = gitConfig[key]?.url;
|
||||
if (remoteName && remoteUrl) {
|
||||
remoteUrls[remoteName] = remoteUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(remoteUrls).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return remoteUrls;
|
||||
}
|
||||
|
||||
export async function getRemoteUrls(
|
||||
configPath: string,
|
||||
output: Output
|
||||
): Promise<{ [key: string]: string } | undefined> {
|
||||
const config = await parseGitConfig(configPath, output);
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteUrls = pluckRemoteUrls(config);
|
||||
return remoteUrls;
|
||||
}
|
||||
|
||||
export function pluckOriginUrl(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 getOriginUrl(
|
||||
configPath: string,
|
||||
output: Output
|
||||
): Promise<string | null> {
|
||||
let gitConfig = await parseGitConfig(configPath, output);
|
||||
if (!gitConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originUrl = pluckOriginUrl(gitConfig);
|
||||
if (originUrl) {
|
||||
return originUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,76 +20,77 @@ export default async function createDeploy(
|
||||
): Promise<any | DeploymentError> {
|
||||
try {
|
||||
return await now.create(paths, createArgs, org, isSettingUpProject, cwd);
|
||||
} catch (error) {
|
||||
if (error.code === 'rate_limited') {
|
||||
throw new ERRORS_TS.DeploymentsRateLimited(error.message);
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS_TS.isAPIError(err)) {
|
||||
if (err.code === 'rate_limited') {
|
||||
throw new ERRORS_TS.DeploymentsRateLimited(err.message);
|
||||
}
|
||||
|
||||
// Means that the domain used as a suffix no longer exists
|
||||
if (error.code === 'domain_missing') {
|
||||
throw new ERRORS_TS.DomainNotFound(error.value);
|
||||
if (err.code === 'domain_missing') {
|
||||
throw new ERRORS_TS.DomainNotFound(err.value);
|
||||
}
|
||||
|
||||
if (error.code === 'domain_not_found' && error.domain) {
|
||||
throw new ERRORS_TS.DomainNotFound(error.domain);
|
||||
if (err.code === 'domain_not_found' && err.domain) {
|
||||
throw new ERRORS_TS.DomainNotFound(err.domain);
|
||||
}
|
||||
|
||||
// This error occures when a domain used in the `alias`
|
||||
// is not yet verified
|
||||
if (error.code === 'domain_not_verified' && error.domain) {
|
||||
throw new ERRORS_TS.DomainNotVerified(error.domain);
|
||||
if (err.code === 'domain_not_verified' && err.domain) {
|
||||
throw new ERRORS_TS.DomainNotVerified(err.domain);
|
||||
}
|
||||
|
||||
// If the domain used as a suffix is not verified, we fail
|
||||
if (error.code === 'domain_not_verified' && error.value) {
|
||||
throw new ERRORS_TS.DomainVerificationFailed(error.value);
|
||||
if (err.code === 'domain_not_verified' && err.value) {
|
||||
throw new ERRORS_TS.DomainVerificationFailed(err.value);
|
||||
}
|
||||
|
||||
// If the domain isn't owned by the user
|
||||
if (error.code === 'not_domain_owner') {
|
||||
throw new ERRORS_TS.NotDomainOwner(error.message);
|
||||
if (err.code === 'not_domain_owner') {
|
||||
throw new ERRORS_TS.NotDomainOwner(err.message);
|
||||
}
|
||||
|
||||
if (error.code === 'builds_rate_limited') {
|
||||
throw new ERRORS_TS.BuildsRateLimited(error.message);
|
||||
if (err.code === 'builds_rate_limited') {
|
||||
throw new ERRORS_TS.BuildsRateLimited(err.message);
|
||||
}
|
||||
|
||||
// If the user doesn't have permissions over the domain used as a suffix we fail
|
||||
if (error.code === 'forbidden') {
|
||||
throw new ERRORS_TS.DomainPermissionDenied(error.value, contextName);
|
||||
if (err.code === 'forbidden') {
|
||||
throw new ERRORS_TS.DomainPermissionDenied(err.value, contextName);
|
||||
}
|
||||
|
||||
if (error.code === 'bad_request' && error.keyword) {
|
||||
if (err.code === 'bad_request' && err.keyword) {
|
||||
throw new ERRORS.SchemaValidationFailed(
|
||||
error.message,
|
||||
error.keyword,
|
||||
error.dataPath,
|
||||
error.params
|
||||
err.message,
|
||||
err.keyword,
|
||||
err.dataPath,
|
||||
err.params
|
||||
);
|
||||
}
|
||||
|
||||
if (error.code === 'domain_configured') {
|
||||
throw new ERRORS_TS.AliasDomainConfigured(error);
|
||||
if (err.code === 'domain_configured') {
|
||||
throw new ERRORS_TS.AliasDomainConfigured(err);
|
||||
}
|
||||
|
||||
if (error.code === 'missing_build_script') {
|
||||
throw new ERRORS_TS.MissingBuildScript(error);
|
||||
if (err.code === 'missing_build_script') {
|
||||
throw new ERRORS_TS.MissingBuildScript(err);
|
||||
}
|
||||
|
||||
if (error.code === 'conflicting_file_path') {
|
||||
throw new ERRORS_TS.ConflictingFilePath(error);
|
||||
if (err.code === 'conflicting_file_path') {
|
||||
throw new ERRORS_TS.ConflictingFilePath(err);
|
||||
}
|
||||
|
||||
if (error.code === 'conflicting_path_segment') {
|
||||
throw new ERRORS_TS.ConflictingPathSegment(error);
|
||||
if (err.code === 'conflicting_path_segment') {
|
||||
throw new ERRORS_TS.ConflictingPathSegment(err);
|
||||
}
|
||||
|
||||
// If the cert is missing we try to generate a new one and the retry
|
||||
if (error.code === 'cert_missing') {
|
||||
if (err.code === 'cert_missing') {
|
||||
const result = await generateCertForDeploy(
|
||||
client,
|
||||
contextName,
|
||||
error.value
|
||||
err.value
|
||||
);
|
||||
|
||||
if (result instanceof NowError) {
|
||||
@@ -107,16 +108,17 @@ export default async function createDeploy(
|
||||
);
|
||||
}
|
||||
|
||||
if (error.code === 'not_found') {
|
||||
if (err.code === 'not_found') {
|
||||
throw new ERRORS_TS.DeploymentNotFound({ context: contextName });
|
||||
}
|
||||
|
||||
const certError = mapCertError(error);
|
||||
const certError = mapCertError(err);
|
||||
if (certError) {
|
||||
return certError;
|
||||
}
|
||||
}
|
||||
|
||||
// If the error is unknown, we just throw
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DeploymentNotFound,
|
||||
DeploymentPermissionDenied,
|
||||
InvalidDeploymentId,
|
||||
isAPIError,
|
||||
} from '../errors-ts';
|
||||
import mapCertError from '../certs/map-cert-error';
|
||||
|
||||
@@ -26,23 +27,25 @@ export default async function getDeploymentByIdOrHost(
|
||||
)
|
||||
: await getDeploymentById(client, idOrHost, apiVersion);
|
||||
return deployment;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 404) {
|
||||
return new DeploymentNotFound({ id: idOrHost, context: contextName });
|
||||
}
|
||||
if (error.status === 403) {
|
||||
if (err.status === 403) {
|
||||
return new DeploymentPermissionDenied(idOrHost, contextName);
|
||||
}
|
||||
if (error.status === 400 && error.message.includes('`id`')) {
|
||||
if (err.status === 400 && err.message.includes('`id`')) {
|
||||
return new InvalidDeploymentId(idOrHost);
|
||||
}
|
||||
|
||||
const certError = mapCertError(error);
|
||||
const certError = mapCertError(err);
|
||||
if (certError) {
|
||||
return certError;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { BuildsManifest } from '../../commands/build';
|
||||
|
||||
export default async function getPrebuiltJson(directory: string) {
|
||||
export default async function getPrebuiltJson(
|
||||
directory: string
|
||||
): Promise<BuildsManifest | null> {
|
||||
try {
|
||||
return await fs.readJSON(join(directory, '.vercel/output/builds.json'));
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,13 +11,11 @@ import cliPkg from '../pkg';
|
||||
|
||||
import cmd from '../output/cmd';
|
||||
import { Output } from '../output';
|
||||
import { getDistTag } from '../get-dist-tag';
|
||||
import { NoBuilderCacheError } from '../errors-ts';
|
||||
|
||||
import * as staticBuilder from './static-builder';
|
||||
import { BuilderWithPackage } from './types';
|
||||
|
||||
type CliPackageJson = typeof cliPkg;
|
||||
import { isErrnoException } from '../is-error';
|
||||
|
||||
const require_: typeof require = eval('require');
|
||||
|
||||
@@ -37,8 +35,6 @@ const localBuilders: { [key: string]: BuilderWithPackage } = {
|
||||
'@vercel/static': createStaticBuilder('vercel'),
|
||||
};
|
||||
|
||||
const distTag = getDistTag(cliPkg.version);
|
||||
|
||||
export const cacheDirPromise = prepareCacheDir();
|
||||
export const builderDirPromise = prepareBuilderDir();
|
||||
|
||||
@@ -65,8 +61,8 @@ export async function prepareBuilderDir() {
|
||||
try {
|
||||
const buildersPkg = join(builderDir, 'package.json');
|
||||
await writeJSON(buildersPkg, { private: true }, { flag: 'wx' });
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
} catch (err: unknown) {
|
||||
if (!isErrnoException(err) || err.code !== 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -102,9 +98,8 @@ function parseVersionSafe(rawSpec: string) {
|
||||
|
||||
export function filterPackage(
|
||||
builderSpec: string,
|
||||
distTag: string,
|
||||
buildersPkg: PackageJson,
|
||||
cliPkg: Partial<CliPackageJson>
|
||||
cliPkg: Partial<PackageJson>
|
||||
) {
|
||||
if (builderSpec in localBuilders) return false;
|
||||
const parsed = npa(builderSpec);
|
||||
@@ -126,31 +121,6 @@ export function filterPackage(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip install of already installed Runtime with tag compatible match
|
||||
if (
|
||||
parsed.name &&
|
||||
parsed.type === 'tag' &&
|
||||
parsed.fetchSpec === distTag &&
|
||||
buildersPkg.dependencies
|
||||
) {
|
||||
const parsedInstalled = npa(
|
||||
`${parsed.name}@${buildersPkg.dependencies[parsed.name]}`
|
||||
);
|
||||
if (parsedInstalled.type !== 'version') {
|
||||
return true;
|
||||
}
|
||||
const semverInstalled = semver.parse(parsedInstalled.rawSpec);
|
||||
if (!semverInstalled) {
|
||||
return true;
|
||||
}
|
||||
if (semverInstalled.prerelease.length > 0) {
|
||||
return semverInstalled.prerelease[0] !== distTag;
|
||||
}
|
||||
if (distTag === 'latest') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -183,7 +153,7 @@ export async function installBuilders(
|
||||
|
||||
// Filter out any packages that come packaged with Vercel CLI
|
||||
const packagesToInstall = packages.filter(p =>
|
||||
filterPackage(p, distTag, buildersPkgBefore, cliPkg)
|
||||
filterPackage(p, buildersPkgBefore, cliPkg)
|
||||
);
|
||||
|
||||
if (packagesToInstall.length === 0) {
|
||||
@@ -362,8 +332,12 @@ export async function getBuilder(
|
||||
builder: Object.freeze(mod),
|
||||
package: Object.freeze(pkg),
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND' && !isRetry) {
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
err.code === 'MODULE_NOT_FOUND' &&
|
||||
!isRetry
|
||||
) {
|
||||
output.debug(
|
||||
`Attempted to require ${requirePath}, but it is not installed`
|
||||
);
|
||||
@@ -392,20 +366,13 @@ export function isBundledBuilder(
|
||||
return false;
|
||||
}
|
||||
|
||||
const bundledVersion = dependencies[parsed.name];
|
||||
if (bundledVersion) {
|
||||
if (parsed.type === 'tag') {
|
||||
if (parsed.fetchSpec === 'canary') {
|
||||
return bundledVersion.includes('canary');
|
||||
} else if (parsed.fetchSpec === 'latest') {
|
||||
return !bundledVersion.includes('canary');
|
||||
}
|
||||
} else if (parsed.type === 'version') {
|
||||
return parsed.fetchSpec === bundledVersion;
|
||||
}
|
||||
}
|
||||
const inCliDependencyList = !!dependencies[parsed.name];
|
||||
const inScope = parsed.scope === '@vercel';
|
||||
const isVersionedReference = ['tag', 'version', 'range'].includes(
|
||||
parsed.type
|
||||
);
|
||||
|
||||
return false;
|
||||
return inCliDependencyList && inScope && isVersionedReference;
|
||||
}
|
||||
|
||||
function getPackageName(
|
||||
|
||||
@@ -4,7 +4,7 @@ import ms from 'ms';
|
||||
import bytes from 'bytes';
|
||||
import { delimiter, dirname, join } from 'path';
|
||||
import { fork, ChildProcess } from 'child_process';
|
||||
import { createFunction } from '@zeit/fun';
|
||||
import { createFunction } from '@vercel/fun';
|
||||
import {
|
||||
Builder,
|
||||
BuildOptions,
|
||||
|
||||
@@ -94,6 +94,12 @@ import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import exposeSystemEnvs from './expose-system-envs';
|
||||
import { treeKill } from '../tree-kill';
|
||||
import { nodeHeadersToFetchHeaders } from './headers';
|
||||
import {
|
||||
errorToString,
|
||||
isErrnoException,
|
||||
isError,
|
||||
isSpawnError,
|
||||
} from '../is-error';
|
||||
|
||||
const frontendRuntimeSet = new Set(
|
||||
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
|
||||
@@ -340,8 +346,8 @@ export default class DevServer {
|
||||
}
|
||||
fileChanged(name, changed, removed);
|
||||
this.output.debug(`File created: ${name}`);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'ENOENT') {
|
||||
this.output.debug(`File created, but has since been deleted: ${name}`);
|
||||
fileRemoved(name, this.files, changed, removed);
|
||||
} else {
|
||||
@@ -375,8 +381,8 @@ export default class DevServer {
|
||||
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
|
||||
fileChanged(name, changed, removed);
|
||||
this.output.debug(`File modified: ${name}`);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'ENOENT') {
|
||||
this.output.debug(`File modified, but has since been deleted: ${name}`);
|
||||
fileRemoved(name, this.files, changed, removed);
|
||||
} else {
|
||||
@@ -507,8 +513,8 @@ export default class DevServer {
|
||||
this.output.debug(`Using local env: ${filePath}`);
|
||||
env = parseDotenv(dotenv);
|
||||
env = this.injectSystemValuesInDotenv(env);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (!isErrnoException(err) || err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -719,13 +725,15 @@ export default class DevServer {
|
||||
const parsed: WithFileNameSymbol<T> = JSON.parse(raw);
|
||||
parsed[fileNameSymbol] = rel;
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (isError(err)) {
|
||||
if (isErrnoException(err) && err.code === 'ENOENT') {
|
||||
this.output.debug(`No \`${rel}\` file present`);
|
||||
} else if (err.name === 'SyntaxError') {
|
||||
this.output.warn(
|
||||
`There is a syntax error in the \`${rel}\` file: ${err.message}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
@@ -851,7 +859,8 @@ export default class DevServer {
|
||||
while (typeof address !== 'string') {
|
||||
try {
|
||||
address = await listen(this.server, ...listenSpec);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err)) {
|
||||
this.output.debug(`Got listen error: ${err.code}`);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
if (typeof listenSpec[0] === 'number') {
|
||||
@@ -864,10 +873,13 @@ export default class DevServer {
|
||||
listenSpec[0]++;
|
||||
} else {
|
||||
this.output.error(
|
||||
`Requested socket ${chalk.cyan(listenSpec[0])} is already in use`
|
||||
`Requested socket ${chalk.cyan(
|
||||
listenSpec[0]
|
||||
)} is already in use`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
@@ -1028,12 +1040,8 @@ export default class DevServer {
|
||||
|
||||
try {
|
||||
await Promise.all(ops);
|
||||
} catch (err) {
|
||||
// Node 8 doesn't have a code for that error
|
||||
if (
|
||||
err.code === 'ERR_SERVER_NOT_RUNNING' ||
|
||||
err.message === 'Not running'
|
||||
) {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'ERR_SERVER_NOT_RUNNING') {
|
||||
process.exit(exitCode || 0);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -1303,13 +1311,16 @@ export default class DevServer {
|
||||
try {
|
||||
const vercelConfig = await this.getVercelConfig();
|
||||
await this.serveProjectAsNowV2(req, res, requestId, vercelConfig);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
|
||||
if (isError(err) && typeof err.stack === 'string') {
|
||||
this.output.debug(err.stack);
|
||||
}
|
||||
|
||||
if (!res.finished) {
|
||||
res.statusCode = 500;
|
||||
res.end(err.message);
|
||||
res.end(errorToString(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1533,16 +1544,16 @@ export default class DevServer {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
// `startDevServer()` threw an error. Most likely this means the dev
|
||||
// server process exited before sending the port information message
|
||||
// (missing dependency at runtime, for example).
|
||||
if (err.code === 'ENOENT') {
|
||||
if (isSpawnError(err) && err.code === 'ENOENT') {
|
||||
err.message = `Command not found: ${chalk.cyan(
|
||||
err.path,
|
||||
...err.spawnargs
|
||||
)}\nPlease ensure that ${cmd(err.path)} is properly installed`;
|
||||
err.link = 'https://vercel.link/command-not-found';
|
||||
)}\nPlease ensure that ${cmd(err.path!)} is properly installed`;
|
||||
(err as any).link = 'https://vercel.link/command-not-found';
|
||||
}
|
||||
|
||||
await this.sendError(
|
||||
@@ -1845,16 +1856,16 @@ export default class DevServer {
|
||||
buildEnv: { ...envConfigs.buildEnv },
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
// `startDevServer()` threw an error. Most likely this means the dev
|
||||
// server process exited before sending the port information message
|
||||
// (missing dependency at runtime, for example).
|
||||
if (err.code === 'ENOENT') {
|
||||
if (isSpawnError(err) && err.code === 'ENOENT') {
|
||||
err.message = `Command not found: ${chalk.cyan(
|
||||
err.path,
|
||||
...err.spawnargs
|
||||
)}\nPlease ensure that ${cmd(err.path)} is properly installed`;
|
||||
err.link = 'https://vercel.link/command-not-found';
|
||||
)}\nPlease ensure that ${cmd(err.path!)} is properly installed`;
|
||||
(err as any).link = 'https://vercel.link/command-not-found';
|
||||
}
|
||||
|
||||
this.output.prettyError(err);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import http from 'http';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import { Lambda as FunLambda } from '@zeit/fun';
|
||||
import { Lambda as FunLambda } from '@vercel/fun';
|
||||
import {
|
||||
Builder as BuildConfig,
|
||||
BuildOptions,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DNSInvalidPort,
|
||||
DNSInvalidType,
|
||||
DNSConflictingRecord,
|
||||
isAPIError,
|
||||
} from '../errors-ts';
|
||||
import { DNSRecordData } from '../../types';
|
||||
|
||||
@@ -26,32 +27,34 @@ export default async function addDNSRecord(
|
||||
}
|
||||
);
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (error.status === 400 && error.code === 'invalid_type') {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 400 && err.code === 'invalid_type') {
|
||||
return new DNSInvalidType(recordData.type);
|
||||
}
|
||||
|
||||
if (error.status === 400 && error.message.includes('port')) {
|
||||
if (err.status === 400 && err.message.includes('port')) {
|
||||
return new DNSInvalidPort();
|
||||
}
|
||||
|
||||
if (error.status === 400) {
|
||||
return error;
|
||||
if (err.status === 400) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (error.status === 403) {
|
||||
if (err.status === 403) {
|
||||
return new DNSPermissionDenied(domain);
|
||||
}
|
||||
|
||||
if (error.status === 404) {
|
||||
if (err.status === 404) {
|
||||
return new DomainNotFound(domain);
|
||||
}
|
||||
|
||||
if (error.status === 409) {
|
||||
const { oldId = '' } = error;
|
||||
if (err.status === 409) {
|
||||
const { oldId = '' } = err;
|
||||
return new DNSConflictingRecord(oldId);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DNSRecord, PaginationOptions } from '../../types';
|
||||
import { DomainNotFound } from '../errors-ts';
|
||||
import { DomainNotFound, isAPIError } from '../errors-ts';
|
||||
import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
|
||||
@@ -27,10 +27,10 @@ export default async function getDomainDNSRecords(
|
||||
|
||||
const data = await client.fetch<Response>(url);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error.code === 'not_found') {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.code === 'not_found') {
|
||||
return new DomainNotFound(domain);
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { Response } from 'node-fetch';
|
||||
import { DomainNotFound, InvalidDomain } from '../errors-ts';
|
||||
import { DomainNotFound, InvalidDomain, isAPIError } from '../errors-ts';
|
||||
import Client from '../client';
|
||||
|
||||
type JSONResponse = {
|
||||
@@ -33,15 +33,17 @@ export default async function importZonefile(
|
||||
|
||||
const { recordIds } = (await res.json()) as JSONResponse;
|
||||
return recordIds;
|
||||
} catch (error) {
|
||||
if (error.code === 'not_found') {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.code === 'not_found') {
|
||||
return new DomainNotFound(domain, contextName);
|
||||
}
|
||||
|
||||
if (error.code === 'invalid_domain') {
|
||||
if (err.code === 'invalid_domain') {
|
||||
return new InvalidDomain(domain);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import retry from 'async-retry';
|
||||
import { DomainAlreadyExists, InvalidDomain } from '../errors-ts';
|
||||
import { DomainAlreadyExists, InvalidDomain, isAPIError } from '../errors-ts';
|
||||
import { Domain } from '../../types';
|
||||
import Client from '../client';
|
||||
|
||||
@@ -29,16 +29,18 @@ async function performAddRequest(client: Client, domainName: string) {
|
||||
method: 'POST',
|
||||
});
|
||||
return domain;
|
||||
} catch (error) {
|
||||
if (error.code === 'invalid_name') {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.code === 'invalid_name') {
|
||||
return new InvalidDomain(domainName);
|
||||
}
|
||||
|
||||
if (error.code === 'domain_already_exists') {
|
||||
if (err.code === 'domain_already_exists') {
|
||||
return new DomainAlreadyExists(domainName);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{ retries: 5, maxTimeout: 8000 }
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import { Domain } from '../../types';
|
||||
import { DomainPermissionDenied, DomainNotFound } from '../errors-ts';
|
||||
import {
|
||||
DomainPermissionDenied,
|
||||
DomainNotFound,
|
||||
isAPIError,
|
||||
} from '../errors-ts';
|
||||
|
||||
type Response = {
|
||||
domain: Domain;
|
||||
@@ -25,15 +29,17 @@ export default async function getDomainByName(
|
||||
`/v4/domains/${encodeURIComponent(domainName)}`
|
||||
);
|
||||
return domain;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 404) {
|
||||
return new DomainNotFound(domainName, contextName);
|
||||
}
|
||||
|
||||
if (error.status === 403) {
|
||||
if (err.status === 403) {
|
||||
return new DomainPermissionDenied(domainName, contextName);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Client from '../client';
|
||||
import { DomainConfig } from '../../types';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
|
||||
export async function getDomainConfig(client: Client, domainName: string) {
|
||||
try {
|
||||
@@ -8,11 +9,11 @@ export async function getDomainConfig(client: Client, domainName: string) {
|
||||
);
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error.status < 500) {
|
||||
return error;
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { stringify } from 'querystring';
|
||||
import { UnsupportedTLD } from '../errors-ts';
|
||||
import { isAPIError, UnsupportedTLD } from '../errors-ts';
|
||||
import Client from '../client';
|
||||
|
||||
type Response = {
|
||||
@@ -15,15 +15,17 @@ export default async function getDomainPrice(
|
||||
try {
|
||||
const querystr = type ? stringify({ name, type }) : stringify({ name });
|
||||
return await client.fetch<Response>(`/v3/domains/price?${querystr}`);
|
||||
} catch (error) {
|
||||
if (error.code === 'unsupported_tld') {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.code === 'unsupported_tld') {
|
||||
return new UnsupportedTLD(name);
|
||||
}
|
||||
|
||||
if (error.status < 500) {
|
||||
return error;
|
||||
if (err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import { Domain } from '../../types';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
|
||||
type Response = {
|
||||
domain: Domain;
|
||||
@@ -20,11 +21,11 @@ export async function getDomain(
|
||||
);
|
||||
|
||||
return domain;
|
||||
} catch (error) {
|
||||
if (error.status < 500) {
|
||||
return error;
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,19 @@ export default async function moveOutDomain(
|
||||
method: 'PATCH',
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code === 'forbidden') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'forbidden') {
|
||||
return new ERRORS.DomainPermissionDenied(name, contextName);
|
||||
}
|
||||
if (error.code === 'not_found') {
|
||||
if (err.code === 'not_found') {
|
||||
return new ERRORS.DomainNotFound(name);
|
||||
}
|
||||
if (error.code === 'invalid_move_destination') {
|
||||
if (err.code === 'invalid_move_destination') {
|
||||
return new ERRORS.InvalidMoveDestination(destination);
|
||||
}
|
||||
if (error.code === 'domain_move_conflict') {
|
||||
const { pendingAsyncPurchase, resolvable, suffix, message } = error;
|
||||
if (err.code === 'domain_move_conflict') {
|
||||
const { pendingAsyncPurchase, resolvable, suffix, message } = err;
|
||||
return new ERRORS.DomainMoveConflict({
|
||||
message,
|
||||
pendingAsyncPurchase,
|
||||
@@ -39,6 +40,7 @@ export default async function moveOutDomain(
|
||||
suffix,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,28 +20,30 @@ export default async function purchaseDomain(
|
||||
body: { name, expectedPrice, renew },
|
||||
method: 'POST',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'invalid_domain') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'invalid_domain') {
|
||||
return new ERRORS.InvalidDomain(name);
|
||||
}
|
||||
if (error.code === 'not_available') {
|
||||
if (err.code === 'not_available') {
|
||||
return new ERRORS.DomainNotAvailable(name);
|
||||
}
|
||||
if (error.code === 'service_unavailabe') {
|
||||
if (err.code === 'service_unavailabe') {
|
||||
return new ERRORS.DomainServiceNotAvailable(name);
|
||||
}
|
||||
if (error.code === 'unexpected_error') {
|
||||
if (err.code === 'unexpected_error') {
|
||||
return new ERRORS.UnexpectedDomainPurchaseError(name);
|
||||
}
|
||||
if (error.code === 'source_not_found') {
|
||||
if (err.code === 'source_not_found') {
|
||||
return new ERRORS.SourceNotFound();
|
||||
}
|
||||
if (error.code === 'payment_error') {
|
||||
if (err.code === 'payment_error') {
|
||||
return new ERRORS.DomainPaymentError();
|
||||
}
|
||||
if (error.code === 'unsupported_tld') {
|
||||
if (err.code === 'unsupported_tld') {
|
||||
return new ERRORS.UnsupportedTLD(name);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,24 +10,26 @@ export default async function removeDomainByName(
|
||||
return await now.fetch(`/v3/domains/${encodeURIComponent(domain)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'not_found') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'not_found') {
|
||||
return new ERRORS.DomainNotFound(domain);
|
||||
}
|
||||
if (error.code === 'forbidden') {
|
||||
if (err.code === 'forbidden') {
|
||||
return new ERRORS.DomainPermissionDenied(domain, contextName);
|
||||
}
|
||||
if (error.code === 'domain_removal_conflict') {
|
||||
if (err.code === 'domain_removal_conflict') {
|
||||
return new ERRORS.DomainRemovalConflict({
|
||||
aliases: error.aliases,
|
||||
certs: error.certs,
|
||||
message: error.message,
|
||||
pendingAsyncPurchase: error.pendingAsyncPurchase,
|
||||
resolvable: error.resolvable,
|
||||
suffix: error.suffix,
|
||||
transferring: error.transferring,
|
||||
aliases: err.aliases,
|
||||
certs: err.certs,
|
||||
message: err.message,
|
||||
pendingAsyncPurchase: err.pendingAsyncPurchase,
|
||||
resolvable: err.resolvable,
|
||||
suffix: err.suffix,
|
||||
transferring: err.transferring,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,25 +14,27 @@ export default async function setCustomSuffix(
|
||||
suffix,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'forbidden') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'forbidden') {
|
||||
return new ERRORS.DomainPermissionDenied(domain, contextName);
|
||||
}
|
||||
if (error.code === 'domain_external') {
|
||||
if (err.code === 'domain_external') {
|
||||
return new ERRORS.DomainExternal(domain);
|
||||
}
|
||||
if (error.code === 'domain_invalid') {
|
||||
if (err.code === 'domain_invalid') {
|
||||
return new ERRORS.InvalidDomain(domain);
|
||||
}
|
||||
if (error.code === 'domain_not_found') {
|
||||
if (err.code === 'domain_not_found') {
|
||||
return new ERRORS.DomainNotFound(domain);
|
||||
}
|
||||
if (error.code === 'domain_not_verified') {
|
||||
if (err.code === 'domain_not_verified') {
|
||||
return new ERRORS.DomainNotVerified(domain);
|
||||
}
|
||||
if (error.code === 'domain_permission_denied') {
|
||||
if (err.code === 'domain_permission_denied') {
|
||||
return new ERRORS.DomainPermissionDenied(domain, contextName);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,27 @@ export default async function transferInDomain(
|
||||
body: { method: 'transfer-in', name, authCode, expectedPrice },
|
||||
method: 'POST',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'invalid_name') {
|
||||
} catch (err: unknown) {
|
||||
if (ERRORS.isAPIError(err)) {
|
||||
if (err.code === 'invalid_name') {
|
||||
return new ERRORS.InvalidDomain(name);
|
||||
}
|
||||
if (error.code === 'domain_already_exists') {
|
||||
if (err.code === 'domain_already_exists') {
|
||||
return new ERRORS.DomainNotAvailable(name);
|
||||
}
|
||||
if (error.code === 'not_transferable') {
|
||||
if (err.code === 'not_transferable') {
|
||||
return new ERRORS.DomainNotTransferable(name);
|
||||
}
|
||||
if (error.code === 'invalid_auth_code') {
|
||||
if (err.code === 'invalid_auth_code') {
|
||||
return new ERRORS.InvalidTransferAuthCode(name, authCode);
|
||||
}
|
||||
if (error.code === 'source_not_found') {
|
||||
if (err.code === 'source_not_found') {
|
||||
return new ERRORS.SourceNotFound();
|
||||
}
|
||||
if (error.code === 'registration_failed') {
|
||||
return new ERRORS.DomainRegistrationFailed(name, error.message);
|
||||
if (err.code === 'registration_failed') {
|
||||
return new ERRORS.DomainRegistrationFailed(name, err.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
2
packages/cli/src/util/env/add-env-record.ts
vendored
2
packages/cli/src/util/env/add-env-record.ts
vendored
@@ -29,6 +29,6 @@ export default async function addEnvRecord(
|
||||
const url = `/v8/projects/${projectId}/env`;
|
||||
await client.fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
80
packages/cli/src/util/env/diff-env-files.ts
vendored
Normal file
80
packages/cli/src/util/env/diff-env-files.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Output } from '../output';
|
||||
import { Dictionary } from '@vercel/client';
|
||||
import { readFile } from 'fs-extra';
|
||||
import { parseEnv } from '../parse-env';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function createEnvObject(
|
||||
envPath: string,
|
||||
output: Output
|
||||
): Promise<Dictionary<string | undefined> | undefined> {
|
||||
// Originally authored by Tyler Waters under MIT License: https://github.com/tswaters/env-file-parser/blob/f17c009b39da599380e069ee72728d1cafdb56b8/lib/parse.js
|
||||
// https://github.com/tswaters/env-file-parser/blob/f17c009b39da599380e069ee72728d1cafdb56b8/LICENSE
|
||||
const envArr = (await readFile(envPath, 'utf-8'))
|
||||
// remove double quotes
|
||||
.replace(/"/g, '')
|
||||
// split on new line
|
||||
.split(/\r?\n|\r/)
|
||||
// filter comments
|
||||
.filter(line => /^[^#]/.test(line))
|
||||
// needs equal sign
|
||||
.filter(line => /=/i.test(line));
|
||||
|
||||
const parsedEnv = parseEnv(envArr);
|
||||
if (Object.keys(parsedEnv).length === 0) {
|
||||
output.debug('Failed to parse env file.');
|
||||
return;
|
||||
}
|
||||
return parsedEnv;
|
||||
}
|
||||
|
||||
function findChanges(
|
||||
oldEnv: Dictionary<string | undefined>,
|
||||
newEnv: Dictionary<string | undefined>
|
||||
): {
|
||||
added: string[];
|
||||
changed: string[];
|
||||
removed: string[];
|
||||
} {
|
||||
const added = [];
|
||||
const changed = [];
|
||||
|
||||
for (const key of Object.keys(newEnv)) {
|
||||
if (oldEnv[key] === undefined) {
|
||||
added.push(key);
|
||||
} else if (oldEnv[key] !== newEnv[key]) {
|
||||
changed.push(key);
|
||||
}
|
||||
delete oldEnv[key];
|
||||
}
|
||||
const removed = Object.keys(oldEnv);
|
||||
|
||||
return {
|
||||
added,
|
||||
changed,
|
||||
removed,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDeltaString(
|
||||
oldEnv: Dictionary<string | undefined>,
|
||||
newEnv: Dictionary<string | undefined>
|
||||
): string {
|
||||
const { added, changed, removed } = findChanges(oldEnv, newEnv);
|
||||
|
||||
let deltaString = '';
|
||||
deltaString += chalk.green(addDeltaSection('+', added));
|
||||
deltaString += chalk.yellow(addDeltaSection('~', changed));
|
||||
deltaString += chalk.red(addDeltaSection('-', removed));
|
||||
|
||||
return deltaString ? chalk.gray('Changes:\n') + deltaString : deltaString;
|
||||
}
|
||||
|
||||
function addDeltaSection(prefix: string, arr: string[]): string {
|
||||
return (
|
||||
arr
|
||||
.sort()
|
||||
.map(item => `${prefix} ${item}`)
|
||||
.join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
7
packages/cli/src/util/env/known-error.ts
vendored
7
packages/cli/src/util/env/known-error.ts
vendored
@@ -1,3 +1,5 @@
|
||||
import { isErrnoException } from '../is-error';
|
||||
|
||||
const knownErrorsCodes = new Set([
|
||||
'PAYMENT_REQUIRED',
|
||||
'BAD_REQUEST',
|
||||
@@ -7,7 +9,8 @@ const knownErrorsCodes = new Set([
|
||||
'ENV_SHOULD_BE_A_SECRET',
|
||||
]);
|
||||
|
||||
export function isKnownError(error: { code?: string }) {
|
||||
const code = error && typeof error.code === 'string' ? error.code : '';
|
||||
export function isKnownError(error: unknown) {
|
||||
const code = isErrnoException(error) ? error.code : null;
|
||||
if (!code) return false;
|
||||
return knownErrorsCodes.has(code.toUpperCase());
|
||||
}
|
||||
|
||||
@@ -87,3 +87,18 @@ export async function responseErrorMessage(
|
||||
|
||||
return `${message} (${res.status})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Object with enumberable properties that match
|
||||
* the provided `err` instance, for use with `JSON.stringify()`.
|
||||
*/
|
||||
export function toEnumerableError<E extends Partial<Error>>(err: E) {
|
||||
const enumerable: {
|
||||
[K in keyof E]?: E[K];
|
||||
} = {};
|
||||
enumerable.name = err.name;
|
||||
for (const key of Object.getOwnPropertyNames(err) as (keyof E)[]) {
|
||||
enumerable[key] = err[key];
|
||||
}
|
||||
return enumerable;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NowError } from './now-error';
|
||||
import code from './output/code';
|
||||
import { getCommandName } from './pkg-name';
|
||||
import chalk from 'chalk';
|
||||
import { isError } from './is-error';
|
||||
|
||||
/**
|
||||
* This error is thrown when there is an API error with a payload. The error
|
||||
@@ -45,6 +46,10 @@ export class APIError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function isAPIError(v: unknown): v is APIError {
|
||||
return isError(v) && 'status' in v;
|
||||
}
|
||||
|
||||
/**
|
||||
* When you're fetching information for the current team but the client can't
|
||||
* retrieve information. This means that the team was probably deleted or the
|
||||
|
||||
@@ -81,21 +81,21 @@ async function printEvents(
|
||||
return;
|
||||
}
|
||||
poller = startPoller();
|
||||
} catch (error) {
|
||||
} catch (err: unknown) {
|
||||
stream.end();
|
||||
finish(error);
|
||||
finish(err);
|
||||
}
|
||||
}, 5000);
|
||||
})();
|
||||
}
|
||||
|
||||
let finishCalled = false;
|
||||
function finish(error?: Error) {
|
||||
function finish(err?: unknown) {
|
||||
if (finishCalled) return;
|
||||
finishCalled = true;
|
||||
clearTimeout(poller);
|
||||
if (error) {
|
||||
reject(error);
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import humanizePath from './humanize-path';
|
||||
import readJSONFile from './read-json-file';
|
||||
import { VercelConfig } from './dev/types';
|
||||
import { Output } from './output';
|
||||
import { isErrnoException } from './is-error';
|
||||
|
||||
let config: VercelConfig;
|
||||
|
||||
@@ -25,8 +26,8 @@ export default async function getConfig(
|
||||
let localPath: string;
|
||||
try {
|
||||
localPath = process.cwd();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (isErrnoException(err) && err.code === 'ENOENT') {
|
||||
return new WorkingDirectoryDoesNotExist();
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Secret,
|
||||
} from '../types';
|
||||
import getEnvRecords, { EnvRecordsSource } from './env/get-env-records';
|
||||
import { isAPIError } from './errors-ts';
|
||||
|
||||
export default async function getDecryptedEnvRecords(
|
||||
output: Output,
|
||||
@@ -39,12 +40,12 @@ export default async function getDecryptedEnvRecords(
|
||||
);
|
||||
|
||||
return { id, type, key, value: secret.value, found: true };
|
||||
} catch (error) {
|
||||
if (error && error.status === 404) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 404) {
|
||||
return { id, type, key, value: '', found: false };
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ import errorOutput from './output/error';
|
||||
import { APIError } from './errors-ts';
|
||||
import { getCommandName } from './pkg-name';
|
||||
|
||||
export default function handleError(
|
||||
error: string | Error | APIError,
|
||||
{ debug = false } = {}
|
||||
) {
|
||||
export default function handleError(error: unknown, { debug = false } = {}) {
|
||||
// Coerce Strings to Error instances
|
||||
if (typeof error === 'string') {
|
||||
error = new Error(error);
|
||||
|
||||
@@ -14,7 +14,7 @@ interface ListSeparator {
|
||||
separator: string;
|
||||
}
|
||||
|
||||
type ListChoice = ListEntry | ListSeparator | typeof inquirer.Separator;
|
||||
export type ListChoice = ListEntry | ListSeparator | typeof inquirer.Separator;
|
||||
|
||||
interface ListOptions {
|
||||
message: string;
|
||||
|
||||
82
packages/cli/src/util/is-error.ts
Normal file
82
packages/cli/src/util/is-error.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
export interface SpawnError extends NodeJS.ErrnoException {
|
||||
spawnargs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple type guard for objects.
|
||||
*
|
||||
* @param obj - A possible object
|
||||
*/
|
||||
export const isObject = (obj: unknown): obj is Record<string, unknown> =>
|
||||
typeof obj === 'object' && obj !== null;
|
||||
|
||||
/**
|
||||
* A type guard for `try...catch` errors.
|
||||
*
|
||||
* This function is based on:
|
||||
* https://github.com/stdlib-js/assert-is-error
|
||||
*/
|
||||
export const isError = (error: unknown): error is Error => {
|
||||
if (!isObject(error)) return false;
|
||||
|
||||
// Check for `Error` objects instantiated within the current global context.
|
||||
if (error instanceof Error) return true;
|
||||
|
||||
// Walk the prototype tree until we find a matching object.
|
||||
while (error) {
|
||||
if (Object.prototype.toString.call(error) === '[object Error]') return true;
|
||||
// eslint-disable-next-line no-param-reassign -- TODO: Fix eslint error following @vercel/style-guide migration
|
||||
error = Object.getPrototypeOf(error);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isErrnoException = (
|
||||
error: unknown
|
||||
): error is NodeJS.ErrnoException => {
|
||||
return isError(error) && 'code' in error;
|
||||
};
|
||||
|
||||
interface ErrorLike {
|
||||
message: string;
|
||||
name?: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type guard for error-like objects.
|
||||
*/
|
||||
export const isErrorLike = (error: unknown): error is ErrorLike =>
|
||||
isObject(error) && 'message' in error;
|
||||
|
||||
/**
|
||||
* Parses errors to string, useful for getting the error message in a
|
||||
* `try...catch` statement.
|
||||
*/
|
||||
export const errorToString = (error: unknown, fallback?: string): string => {
|
||||
if (isError(error) || isErrorLike(error)) return error.message;
|
||||
|
||||
if (typeof error === 'string') return error;
|
||||
|
||||
return fallback ?? 'An unknown error has ocurred.';
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes unknown errors to the Error type, useful for working with errors
|
||||
* in a `try...catch` statement.
|
||||
*/
|
||||
export const normalizeError = (error: unknown): Error => {
|
||||
if (isError(error)) return error;
|
||||
|
||||
const errorMessage = errorToString(error);
|
||||
|
||||
// Copy over additional properties if the object is error-like.
|
||||
return isErrorLike(error)
|
||||
? Object.assign(new Error(errorMessage), error)
|
||||
: new Error(errorMessage);
|
||||
};
|
||||
|
||||
export function isSpawnError(v: unknown): v is SpawnError {
|
||||
return isErrnoException(v) && 'spawnargs' in v;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import stamp from '../output/stamp';
|
||||
import { EmojiLabel } from '../emoji';
|
||||
import createDeploy from '../deploy/create-deploy';
|
||||
import Now, { CreateOptions } from '../index';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
|
||||
export interface SetupAndLinkOptions {
|
||||
forceDelete?: boolean;
|
||||
@@ -96,7 +97,8 @@ export default async function setupAndLink(
|
||||
'Which scope should contain your project?',
|
||||
autoConfirm
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.code === 'NOT_AUTHORIZED') {
|
||||
output.prettyError(err);
|
||||
return { status: 'error', exitCode: 1, reason: 'NOT_AUTHORIZED' };
|
||||
@@ -106,6 +108,7 @@ export default async function setupAndLink(
|
||||
output.prettyError(err);
|
||||
return { status: 'error', exitCode: 1, reason: 'TEAM_DELETED' };
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import verify from './verify';
|
||||
import executeLogin from './login';
|
||||
import Client from '../client';
|
||||
import { LoginResult } from './types';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
import { errorToString } from '../is-error';
|
||||
|
||||
export default async function doEmailLogin(
|
||||
client: Client,
|
||||
@@ -22,8 +24,8 @@ export default async function doEmailLogin(
|
||||
const data = await executeLogin(client, email);
|
||||
verificationToken = data.token;
|
||||
securityCode = data.securityCode;
|
||||
} catch (err) {
|
||||
output.error(err.message);
|
||||
} catch (err: unknown) {
|
||||
output.error(errorToString(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -51,9 +53,9 @@ export default async function doEmailLogin(
|
||||
'Email',
|
||||
ssoUserId
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.serverMessage !== 'Confirmation incomplete') {
|
||||
output.error(err.message);
|
||||
} catch (err: unknown) {
|
||||
if (!isAPIError(err) || err.serverMessage !== 'Confirmation incomplete') {
|
||||
output.error(errorToString(err));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Client from '../client';
|
||||
import { InvalidEmail, AccountNotFound } from '../errors-ts';
|
||||
import { InvalidEmail, AccountNotFound, isAPIError } from '../errors-ts';
|
||||
import { errorToString } from '../is-error';
|
||||
import { LoginData } from './types';
|
||||
|
||||
export default async function login(
|
||||
@@ -11,7 +12,8 @@ export default async function login(
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.code === 'not_exists') {
|
||||
throw new AccountNotFound(
|
||||
email,
|
||||
@@ -22,7 +24,8 @@ export default async function login(
|
||||
if (err.code === 'invalid_email') {
|
||||
throw new InvalidEmail(email, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected error: ${err.message}`);
|
||||
throw new Error(`Unexpected error: ${errorToString(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function readInput(
|
||||
message,
|
||||
});
|
||||
input = val;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.log(); // \n
|
||||
|
||||
if (err.isTtyError) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import chalk from 'chalk';
|
||||
import title from 'title';
|
||||
import bytes from 'bytes';
|
||||
import { isReady, isFailed } from '../build-state';
|
||||
import { Build, BuildOutput } from '../../types';
|
||||
@@ -8,9 +7,6 @@ export interface Times {
|
||||
[id: string]: string | null;
|
||||
}
|
||||
|
||||
// That's how long the word "Initializing" is
|
||||
const longestState = 12;
|
||||
|
||||
// That's the spacing between the source, state and time
|
||||
const padding = 8;
|
||||
|
||||
@@ -18,8 +14,6 @@ const padding = 8;
|
||||
const MAX_BUILD_GROUPS = 5;
|
||||
const MAX_OUTPUTS_PER_GROUP = 5;
|
||||
|
||||
const prepareState = (state: string) => title(state.replace('_', ' '));
|
||||
|
||||
const hasOutput = (b: Build) => Array.isArray(b.output) && b.output.length > 0;
|
||||
|
||||
// Get the common path out of multiple builds
|
||||
@@ -47,24 +41,19 @@ const getCommonPath = (buildGroup: Build[]) => {
|
||||
};
|
||||
|
||||
const styleBuild = (build: Build, times: Times, longestSource: number) => {
|
||||
const { entrypoint, readyState, id } = build;
|
||||
const state = prepareState(readyState).padEnd(longestState + padding);
|
||||
const { entrypoint, id } = build;
|
||||
const time = typeof times[id] === 'string' ? times[id] : '';
|
||||
|
||||
let stateColor = chalk.grey;
|
||||
let pathColor = chalk.cyan;
|
||||
|
||||
if (isReady(build)) {
|
||||
stateColor = chalk;
|
||||
} else if (isFailed(build)) {
|
||||
stateColor = chalk.red;
|
||||
if (isFailed(build)) {
|
||||
pathColor = chalk.red;
|
||||
}
|
||||
|
||||
const entry = entrypoint.padEnd(longestSource + padding);
|
||||
const prefix = hasOutput(build) ? '┌' : '╶';
|
||||
|
||||
return `${chalk.grey(prefix)} ${pathColor(entry)}${stateColor(state)}${time}`;
|
||||
return `${chalk.grey(prefix)} ${pathColor(entry)}${time}`;
|
||||
};
|
||||
|
||||
const styleHiddenBuilds = (
|
||||
@@ -79,43 +68,9 @@ const styleHiddenBuilds = (
|
||||
const time = typeof times[id] === 'string' ? times[id] : '';
|
||||
const prefix = isHidden === false && buildGroup.some(hasOutput) ? '┌' : '╶';
|
||||
|
||||
// Set the defaults so that they will be sorted
|
||||
const stateMap: { [readyState: string]: number } = {
|
||||
READY: 0,
|
||||
ERROR: 0,
|
||||
BUILDING: 0,
|
||||
};
|
||||
|
||||
for (const { readyState } of buildGroup) {
|
||||
stateMap[readyState]++;
|
||||
}
|
||||
|
||||
let state = Object.keys(stateMap)
|
||||
.map(readyState => {
|
||||
const counter = stateMap[readyState];
|
||||
const name = prepareState(readyState);
|
||||
|
||||
if (!counter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${counter > 9 ? '9+' : counter} ${name}`;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(', ');
|
||||
|
||||
// Since the longestState might still be shorter
|
||||
// than multiple states we still want to ensure
|
||||
// a space between the states and the time
|
||||
state = `${state} `.padEnd(longestState + padding);
|
||||
|
||||
let pathColor = chalk.cyan;
|
||||
let stateColor = chalk.grey;
|
||||
|
||||
if (buildGroup.every(isReady)) {
|
||||
stateColor = chalk;
|
||||
} else if (buildGroup.every(isFailed)) {
|
||||
stateColor = chalk.red;
|
||||
if (buildGroup.every(isFailed)) {
|
||||
pathColor = chalk.red;
|
||||
}
|
||||
|
||||
@@ -123,7 +78,7 @@ const styleHiddenBuilds = (
|
||||
pathColor = chalk.grey;
|
||||
}
|
||||
|
||||
return `${chalk.grey(prefix)} ${pathColor(entry)}${stateColor(state)}${time}`;
|
||||
return `${chalk.grey(prefix)} ${pathColor(entry)}${time}`;
|
||||
};
|
||||
|
||||
const styleOutput = (
|
||||
|
||||
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
||||
import renderLink from './link';
|
||||
import wait, { StopSpinner } from './wait';
|
||||
import type { WritableTTY } from '../../types';
|
||||
import { errorToString } from '../is-error';
|
||||
|
||||
export interface OutputOptions {
|
||||
debug?: boolean;
|
||||
@@ -79,10 +80,13 @@ export class Output {
|
||||
}
|
||||
};
|
||||
|
||||
prettyError = (
|
||||
err: Pick<Error, 'message'> & { link?: string; action?: string }
|
||||
) => {
|
||||
return this.error(err.message, undefined, err.link, err.action);
|
||||
prettyError = (err: unknown) => {
|
||||
return this.error(
|
||||
errorToString(err),
|
||||
undefined,
|
||||
(err as any).link,
|
||||
(err as any).action
|
||||
);
|
||||
};
|
||||
|
||||
ready = (str: string) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { metrics, shouldCollectMetrics } from '../metrics';
|
||||
import { APIError } from '../errors-ts';
|
||||
import renderLink from './link';
|
||||
|
||||
const metric = metrics();
|
||||
let metric: ReturnType<typeof metrics>;
|
||||
|
||||
export default function error(
|
||||
...input: string[] | [Pick<APIError, 'slug' | 'message' | 'link' | 'action'>]
|
||||
@@ -19,6 +19,7 @@ export default function error(
|
||||
}
|
||||
|
||||
if (shouldCollectMetrics) {
|
||||
if (!metric) metric = metrics();
|
||||
metric.exception(messages.join('\n')).send();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user