[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/text-table": "0.2.0",
"@types/title": "3.4.1", "@types/title": "3.4.1",
"@types/universal-analytics": "0.4.2", "@types/universal-analytics": "0.4.2",
"@types/update-notifier": "5.1.0",
"@types/which": "1.3.2", "@types/which": "1.3.2",
"@types/write-json-file": "2.2.1", "@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.5.1-canary.1", "@vercel/frameworks": "0.5.1-canary.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,9 @@ import renderLink from './link';
const metric = metrics(); 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; let messages = input;
if (typeof input[0] === 'object') { if (typeof input[0] === 'object') {
const { slug, message, link, action = 'Learn More' } = input[0]; const { slug, message, link, action = 'Learn More' } = input[0];

View File

@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"strict": true, "strict": true,
"noEmitOnError": true, "noEmitOnError": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
@@ -12,7 +13,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "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; timeout?: number | undefined;
host?: string; host?: string;
} }
export default function( export default function (
port: number | undefined, port: number | undefined,
options?: IsPortReachableOptions options?: IsPortReachableOptions
): Promise<boolean>; ): 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' { declare module 'serve-handler' {
import http from 'http'; import http from 'http';
export default function( export default function (
request: http.IncomingMessage, request: http.IncomingMessage,
response: http.ServerResponse, response: http.ServerResponse,
options: serveHandler.Options options: serveHandler.Options
@@ -59,5 +59,5 @@ declare module 'serve-handler/src/directory' {
directory: string; 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" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== 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": "@types/content-type@1.1.3":
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07" 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" resolved "https://registry.yarnpkg.com/@types/universal-analytics/-/universal-analytics-0.4.2.tgz#d21a122a984bf8261eb206bcb7ccb1c0e8353d04"
integrity sha512-ndv0aYA5tNPdl4KYUperlswuiSj49+o7Pwx8hrCiE9x2oeiMrn7A2jXFDdTLFIymiYZImDX02ycq0i6uQ3TL0A== 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": "@types/which@1.3.2":
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"