[cli] Refactor src/index to TypeScript (#6602)

Refactors the CLI entrypoint `src/index.js` to TypeScript.
This commit is contained in:
Nathan Rajlich
2021-08-27 12:48:31 -07:00
committed by GitHub
parent f221f041d0
commit 676a3d2568
24 changed files with 145 additions and 123 deletions

View File

@@ -1,11 +0,0 @@
declare module 'intercept-stdout' {
export default function (fn?: InterceptFn): UnhookIntercept
}
interface InterceptFn {
(text: string): string | void
}
interface UnhookIntercept {
(): void
}

View File

@@ -1,5 +0,0 @@
declare module 'promisepipe' {
export default function (
...streams: Array<NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream>
): Promise<void>
}

View File

@@ -99,6 +99,7 @@
"@types/text-table": "0.2.0",
"@types/title": "3.4.1",
"@types/universal-analytics": "0.4.2",
"@types/update-notifier": "5.1.0",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.5.1-canary.1",

View File

@@ -49,7 +49,7 @@ async function main() {
// Do the initial `ncc` build
console.log();
const src = join(dirRoot, 'src');
const src = join(dirRoot, 'src/index.ts');
const args = [
'ncc',
'build',

View File

@@ -421,7 +421,7 @@ function handleCreateAliasError<T>(
return error;
}
function getTargetsForAlias(args: string[], { alias }: VercelConfig) {
function getTargetsForAlias(args: string[], { alias }: VercelConfig = {}) {
if (args.length) {
return [args[args.length - 1]]
.map(target => (target.indexOf('.') !== -1 ? toHost(target) : target))

View File

@@ -120,10 +120,7 @@ export default async (client: Client) => {
paths = [process.cwd()];
}
let localConfig: VercelConfig | null = client.localConfig;
if (!localConfig || localConfig instanceof Error) {
localConfig = readLocalConfig(paths[0]);
}
let localConfig = client.localConfig || readLocalConfig(paths[0]);
for (const path of paths) {
try {

View File

@@ -20,10 +20,9 @@ import epipebomb from 'epipebomb';
import updateNotifier from 'update-notifier';
import { URL } from 'url';
import * as Sentry from '@sentry/node';
import { NowBuildError } from '@vercel/build-utils';
import hp from './util/humanize-path';
import commands from './commands/index.ts';
import pkg from './util/pkg.ts';
import commands from './commands';
import pkg from './util/pkg';
import createOutput from './util/output';
import cmd from './util/output/cmd';
import info from './util/output/info';
@@ -31,9 +30,9 @@ import error from './util/output/error';
import param from './util/output/param';
import highlight from './util/output/highlight';
import getArgs from './util/get-args';
import getUser from './util/get-user.ts';
import getTeams from './util/teams/get-teams.ts';
import Client from './util/client.ts';
import getUser from './util/get-user';
import getTeams from './util/teams/get-teams';
import Client from './util/client';
import { handleError } from './util/error';
import reportError from './util/report-error';
import getConfig from './util/get-config';
@@ -44,13 +43,14 @@ import {
getDefaultAuthConfig,
} from './util/config/get-default';
import * as ERRORS from './util/errors-ts';
import { NowError } from './util/now-error';
import { APIError } from './util/errors-ts.ts';
import { SENTRY_DSN } from './util/constants.ts';
import { APIError } from './util/errors-ts';
import { SENTRY_DSN } from './util/constants';
import getUpdateCommand from './util/get-update-command';
import { metrics, shouldCollectMetrics } from './util/metrics.ts';
import { getCommandName, getTitleName } from './util/pkg-name.ts';
import doLoginPrompt from './util/login/prompt.ts';
import { metrics, shouldCollectMetrics } from './util/metrics';
import { getCommandName, getTitleName } from './util/pkg-name';
import doLoginPrompt from './util/login/prompt';
import { GlobalConfig } from './types';
import { VercelConfig } from '@vercel/client';
const isCanary = pkg.version.includes('canary');
@@ -77,8 +77,8 @@ Sentry.init({
environment: isCanary ? 'canary' : 'stable',
});
let client;
let debug = () => {};
let client: Client;
let debug: (s: string) => void = () => {};
let apiUrl = 'https://api.vercel.com';
const main = async () => {
@@ -108,26 +108,30 @@ const main = async () => {
debug = output.debug;
const localConfigPath = argv['--local-config'];
const localConfig = await getConfig(output, localConfigPath);
if (localConfigPath && localConfig instanceof ERRORS.CantFindConfig) {
output.error(
`Couldn't find a project configuration file at \n ${localConfig.meta.paths.join(
' or\n '
)}`
);
return 1;
}
let localConfig: VercelConfig | Error | undefined = await getConfig(
output,
localConfigPath
);
if (localConfig instanceof ERRORS.CantParseJSONFile) {
output.error(`Couldn't parse JSON file ${localConfig.meta.file}.`);
return 1;
}
if (
(localConfig instanceof NowError || localConfig instanceof NowBuildError) &&
!(localConfig instanceof ERRORS.CantFindConfig)
) {
if (localConfig instanceof ERRORS.CantFindConfig) {
if (localConfigPath) {
output.error(
`Couldn't find a project configuration file at \n ${localConfig.meta.paths.join(
' or\n '
)}`
);
return 1;
} else {
localConfig = undefined;
}
}
if (localConfig instanceof Error) {
output.prettyError(localConfig);
return 1;
}
@@ -207,7 +211,7 @@ const main = async () => {
return 0;
}
let config;
let config: GlobalConfig | null = null;
if (configExists) {
try {
@@ -229,8 +233,11 @@ const main = async () => {
// multiple providers. In that case, we really
// need to migrate.
if (
// @ts-ignore
config.sh ||
// @ts-ignore
config.user ||
// @ts-ignore
typeof config.user === 'object' ||
typeof config.currentTeam === 'object'
) {
@@ -300,6 +307,7 @@ const main = async () => {
// This is from when Vercel CLI supported
// multiple providers. In that case, we really
// need to migrate.
// @ts-ignore
if (authConfig.credentials) {
authConfigExists = false;
}
@@ -346,6 +354,11 @@ const main = async () => {
return 1;
}
if (!config) {
output.error(`Vercel global config was not loaded.`);
return 1;
}
// Shared API `Client` instance for all sub-commands to utilize
client = new Client({
apiUrl,
@@ -397,7 +410,7 @@ const main = async () => {
}
if (subcommandExists) {
debug('user supplied known subcommand', targetOrSubcommand);
debug(`user supplied known subcommand: "${targetOrSubcommand}"`);
subcommand = targetOrSubcommand;
} else {
debug('user supplied a possible target for deployment');
@@ -457,14 +470,12 @@ const main = async () => {
}
if (typeof argv['--token'] === 'string' && subcommand === 'switch') {
console.error(
error({
message: `This command doesn't work with ${param(
'--token'
)}. Please use ${param('--scope')}.`,
slug: 'no-token-allowed',
})
);
output.prettyError({
message: `This command doesn't work with ${param(
'--token'
)}. Please use ${param('--scope')}.`,
link: 'https://err.sh/vercel/no-token-allowed',
});
return 1;
}
@@ -473,12 +484,10 @@ const main = async () => {
const token = argv['--token'];
if (token.length === 0) {
console.error(
error({
message: `You defined ${param('--token')}, but it's missing a value`,
slug: 'missing-token-value',
})
);
output.prettyError({
message: `You defined ${param('--token')}, but it's missing a value`,
link: 'https://err.sh/vercel/missing-token-value',
});
return 1;
}
@@ -486,16 +495,14 @@ const main = async () => {
const invalid = token.match(/(\W)/g);
if (invalid) {
const notContain = Array.from(new Set(invalid)).sort();
console.error(
error({
message: `You defined ${param(
'--token'
)}, but its contents are invalid. Must not contain: ${notContain
.map(c => JSON.stringify(c))
.join(', ')}`,
slug: 'invalid-token-value',
})
);
output.prettyError({
message: `You defined ${param(
'--token'
)}, but its contents are invalid. Must not contain: ${notContain
.map(c => JSON.stringify(c))
.join(', ')}`,
link: 'https://err.sh/vercel/invalid-token-value',
});
return 1;
}
@@ -517,7 +524,7 @@ const main = async () => {
}
const targetCommand = commands.get(subcommand);
const scope = argv['--scope'] || argv['--team'] || localConfig.scope;
const scope = argv['--scope'] || argv['--team'] || localConfig?.scope;
if (
typeof scope === 'string' &&
@@ -531,12 +538,10 @@ const main = async () => {
user = await getUser(client);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED') {
console.error(
error({
message: `You do not have access to the specified account`,
slug: 'scope-not-accessible',
})
);
output.prettyError({
message: `You do not have access to the specified account`,
link: 'https://err.sh/vercel/scope-not-accessible',
});
return 1;
}
@@ -554,12 +559,10 @@ const main = async () => {
teams = await getTeams(client);
} catch (err) {
if (err.code === 'not_authorized') {
console.error(
error({
message: `You do not have access to the specified team`,
slug: 'scope-not-accessible',
})
);
output.prettyError({
message: `You do not have access to the specified team`,
link: 'https://err.sh/vercel/scope-not-accessible',
});
return 1;
}
@@ -572,12 +575,10 @@ const main = async () => {
teams && teams.find(team => team.id === scope || team.slug === scope);
if (!related) {
console.error(
error({
message: 'The specified scope does not exist',
slug: 'scope-not-existent',
})
);
output.prettyError({
message: 'The specified scope does not exist',
link: 'https://err.sh/vercel/scope-not-existent',
});
return 1;
}
@@ -672,7 +673,7 @@ const main = async () => {
return exitCode;
};
const handleRejection = async err => {
const handleRejection = async (err: any) => {
debug('handling rejection');
if (err) {
@@ -689,7 +690,7 @@ const handleRejection = async err => {
process.exit(1);
};
const handleUnexpected = async err => {
const handleUnexpected = async (err: Error) => {
const { message } = err;
// We do not want to render errors about Sentry not being reachable
@@ -698,9 +699,8 @@ const handleUnexpected = async err => {
return;
}
await reportError(Sentry, client, err);
console.error(error(`An unexpected error occurred!\n${err.stack}`));
await reportError(Sentry, client, err);
process.exit(1);
};
@@ -711,6 +711,7 @@ process.on('uncaughtException', handleUnexpected);
main()
.then(exitCode => {
process.exitCode = exitCode;
// @ts-ignore - "nowExit" is a non-standard event name
process.emit('nowExit');
})
.catch(handleUnexpected);

View File

@@ -16,13 +16,13 @@ export interface JSONObject {
}
export interface AuthConfig {
_: string;
_?: string;
token?: string;
skipWrite?: boolean;
}
export interface GlobalConfig {
_: string;
_?: string;
currentTeam?: string;
includeScheme?: string;
collectMetrics?: boolean;

View File

@@ -35,7 +35,7 @@ export async function getDeploymentForAlias(
localConfigPath: string | undefined,
user: User,
contextName: string,
localConfig: VercelConfig
localConfig?: VercelConfig
) {
output.spinner(`Fetching deployment to alias in ${chalk.bold(contextName)}`);
@@ -52,7 +52,7 @@ export async function getDeploymentForAlias(
}
const appName =
(localConfig && localConfig.name) ||
localConfig?.name ||
path.basename(path.resolve(process.cwd(), localConfigPath || ''));
if (!appName) {

View File

@@ -34,7 +34,7 @@ export interface ClientOptions {
authConfig: AuthConfig;
output: Output;
config: GlobalConfig;
localConfig: VercelConfig;
localConfig?: VercelConfig;
}
const isJSONObject = (v: any): v is JSONObject => {
@@ -47,7 +47,7 @@ export default class Client extends EventEmitter {
authConfig: AuthConfig;
output: Output;
config: GlobalConfig;
localConfig: VercelConfig;
localConfig?: VercelConfig;
private requestIdCounter: number;
constructor(opts: ClientOptions) {

View File

@@ -1,6 +1,6 @@
import { AuthConfig, GlobalConfig } from '../../types';
export const getDefaultConfig = async (existingCopy: GlobalConfig) => {
export const getDefaultConfig = async (existingCopy?: GlobalConfig | null) => {
let migrated = false;
const config: GlobalConfig = {
@@ -51,7 +51,7 @@ export const getDefaultConfig = async (existingCopy: GlobalConfig) => {
return { config, migrated };
};
export const getDefaultAuthConfig = async (existing?: AuthConfig) => {
export const getDefaultAuthConfig = async (existing?: AuthConfig | null) => {
let migrated = false;
const config: AuthConfig = {

View File

@@ -15,6 +15,7 @@ export class APIError extends Error {
status: number;
serverMessage: string;
link?: string;
slug?: string;
action?: string;
retryAfter: number | null | 'never';
[key: string]: any;

View File

@@ -2,7 +2,6 @@ import { join, basename } from 'path';
import chalk from 'chalk';
import { remove } from 'fs-extra';
import { ProjectLinkResult, ProjectSettings } from '../../types';
import { VercelConfig } from '../dev/types';
import {
getLinkedProject,
linkFolderToProject,
@@ -46,6 +45,7 @@ export default async function setupAndLink(
): Promise<ProjectLinkResult> {
const {
authConfig: { token },
localConfig,
apiUrl,
output,
config,
@@ -144,13 +144,9 @@ export default async function setupAndLink(
return { status: 'error', exitCode: 1 };
}
let localConfig: VercelConfig = {};
if (client.localConfig && !(client.localConfig instanceof Error)) {
localConfig = client.localConfig;
}
config.currentTeam = org.type === 'team' ? org.id : undefined;
const isZeroConfig = !localConfig.builds || localConfig.builds.length === 0;
const isZeroConfig =
!localConfig || !localConfig.builds || localConfig.builds.length === 0;
try {
let settings: ProjectSettings = {};
@@ -170,7 +166,7 @@ export default async function setupAndLink(
forceNew: undefined,
withCache: undefined,
quiet,
wantsPublic: localConfig.public,
wantsPublic: localConfig?.public || false,
isFile,
type: null,
nowConfig: localConfig,
@@ -181,7 +177,7 @@ export default async function setupAndLink(
skipAutoDetectionConfirmation: false,
};
if (!localConfig.builds || localConfig.builds.length === 0) {
if (isZeroConfig) {
// Only add projectSettings for zero config deployments
createArgs.projectSettings = { sourceFilesOutsideRootDirectory };
}

View File

@@ -88,7 +88,9 @@ function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
}
}
function prettyError(err: Error & { link?: string; action?: string }) {
function prettyError(
err: Pick<Error, 'message'> & { link?: string; action?: string }
) {
return error(err.message, undefined, err.link, err.action);
}

View File

@@ -5,7 +5,9 @@ import renderLink from './link';
const metric = metrics();
export default function error(...input: string[] | [APIError]) {
export default function error(
...input: string[] | [Pick<APIError, 'slug' | 'message' | 'link' | 'action'>]
) {
let messages = input;
if (typeof input[0] === 'object') {
const { slug, message, link, action = 'Learn More' } = input[0];

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"baseUrl": ".",
"strict": true,
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
@@ -12,7 +13,7 @@
"resolveJsonModule": true,
"sourceMap": true,
"outDir": "./dist",
"typeRoots": ["./@types", "./node_modules/@types"]
"typeRoots": ["./types", "./node_modules/@types"]
},
"include": ["src/**/*"]
"include": ["./types", "src/**/*"]
}

View File

@@ -0,0 +1,3 @@
declare module 'epipebomb' {
export default function (): void;
}

View File

@@ -0,0 +1,11 @@
declare module 'intercept-stdout' {
export default function (fn?: InterceptFn): UnhookIntercept;
}
interface InterceptFn {
(text: string): string | void;
}
interface UnhookIntercept {
(): void;
}

View File

@@ -3,7 +3,7 @@ declare module 'is-port-reachable' {
timeout?: number | undefined;
host?: string;
}
export default function(
export default function (
port: number | undefined,
options?: IsPortReachableOptions
): Promise<boolean>;

View File

@@ -0,0 +1,7 @@
declare module 'promisepipe' {
export default function (
...streams: Array<
NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream
>
): Promise<void>;
}

View File

@@ -1,7 +1,7 @@
declare module 'serve-handler' {
import http from 'http';
export default function(
export default function (
request: http.IncomingMessage,
response: http.ServerResponse,
options: serveHandler.Options
@@ -59,5 +59,5 @@ declare module 'serve-handler/src/directory' {
directory: string;
}
export default function(spec: Spec): string;
export default function (spec: Spec): string;
}

View File

@@ -0,0 +1,3 @@
declare module '@zeit/source-map-support' {
function install(): void;
}

View File

@@ -1987,6 +1987,11 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/configstore@*":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/configstore/-/configstore-5.0.1.tgz#7be34d28ce29a408c98e717ada0488664eaf6173"
integrity sha512-c/QCznvk7bLKGhHETj29rqKufui3jaAxjBhK4R2zUrMG5UG0qTwfWYxBoUbH8JCyDjdCWMIxPJ7/Fdz1UcAnWg==
"@types/content-type@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07"
@@ -2399,6 +2404,14 @@
resolved "https://registry.yarnpkg.com/@types/universal-analytics/-/universal-analytics-0.4.2.tgz#d21a122a984bf8261eb206bcb7ccb1c0e8353d04"
integrity sha512-ndv0aYA5tNPdl4KYUperlswuiSj49+o7Pwx8hrCiE9x2oeiMrn7A2jXFDdTLFIymiYZImDX02ycq0i6uQ3TL0A==
"@types/update-notifier@5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-5.1.0.tgz#52ed6a2e9851fd6f1c88e93c85e8a0e1d5500fda"
integrity sha512-aGY5pH1Q/DcToKXl4MCj1c0uDUB+zSVFDRCI7Q7js5sguzBTqJV/5kJA2awofbtWYF3xnon1TYdZYnFditRPtQ==
dependencies:
"@types/configstore" "*"
boxen "^4.2.0"
"@types/which@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"