diff --git a/.eslintrc.js b/.eslintrc.js index e131f7cb8f2f..7d9386a11ea0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -301,6 +301,7 @@ module.exports = { { files: ['rollup.config.ts'], rules: { + // rollup config must be default exported 'import/no-default-export': 'off', }, }, @@ -311,5 +312,42 @@ module.exports = { 'no-console': 'off', }, }, + { + files: ['*.{js,jsx}'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + }, + }, + // CLI lint configs + { + files: [ + 'packages/cli/bin/**/*.{ts,js}', + 'packages/cli/src/reporters/Reporter.ts', + ], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['packages/cli/bin/**/*.js'], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + }, + }, + { + files: ['packages/cli/**/*.{ts,tsx,js}'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', disallowTypeAnnotations: true }, + ], + '@typescript-eslint/consistent-type-exports': 'error', + 'import/first': 'error', + 'import/newline-after-import': 'error', + 'import/no-duplicates': 'error', + 'simple-import-sort/imports': 'error', + }, + }, ], }; diff --git a/package.json b/package.json index d2df363bc502..083629d0f555 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint-markdown-fix": "yarn lint-markdown --fix", "lint-markdown": "markdownlint \"**/*.md\" --config=.markdownlint.json --ignore-path=.markdownlintignore", - "lint": "cross-env NODE_OPTIONS=\"--max-old-space-size=16384\" eslint . --ext .js,.jsx,.ts,.tsx", - "postinstall": "yarn husky install && yarn build", + "lint": "ts-eslint -p ./tsconfig.eslint.json -p \"packages/*/tsconfig.json\"", + "postinstall": "yarn patch-package && yarn husky install && yarn build", "pre-commit": "yarn lint-staged", "pre-push": "yarn check-format", "start": "nx run website:start", @@ -75,10 +75,12 @@ "@types/marked": "^3.0.2", "@types/ncp": "^2.0.5", "@types/node": "^16.11.4", + "@types/node-fetch": "^3.0.3", "@types/prettier": "^2.4.2", "@types/rimraf": "^3.0.2", "@types/semver": "^7.3.9", "@types/tmp": "^0.2.2", + "@types/yargs": "^17.0.8", "all-contributors-cli": "^6.20.0", "cross-env": "^7.0.3", "cspell": "^5.12.3", @@ -101,6 +103,7 @@ "markdownlint-cli": "^0.29.0", "ncp": "^2.0.0", "node-fetch": "^3.0.0", + "patch-package": "^6.4.7", "prettier": "^2.5.0", "pretty-format": "^27.3.1", "rimraf": "^3.0.2", diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 000000000000..7e7370143b26 --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 TypeScript ESLint and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000000..505fa3225b3b --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,15 @@ +

typescript-eslint CLI

+ +

+ CI + NPM Version + NPM Downloads +

+ +CLI for coordinating lint runs that use typescript-eslint. + +TODO: docs on CLI args + +## Contributing + +[See the contributing guide here](../../CONTRIBUTING.md) diff --git a/packages/cli/bin/ts-eslint.js b/packages/cli/bin/ts-eslint.js new file mode 100755 index 000000000000..9b643b460a1c --- /dev/null +++ b/packages/cli/bin/ts-eslint.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +// @ts-check + +if (process.env.USE_TS_ESLINT_SRC == null) { + // to use V8's code cache to speed up instantiation time + require('v8-compile-cache'); +} + +/** + * @param {unknown} thing + * @returns {thing is Record} + */ +function isObject(thing) { + return typeof thing === 'object' && thing != null; +} + +/** + * Get the error message of a given value. + * @param {unknown} error The value to get. + * @returns {string} The error message. + */ +function getErrorMessage(error) { + // Lazy loading because this is used only if an error happened. + const util = require('util'); + + if (!isObject(error)) { + return String(error); + } + + // Use the stacktrace if it's an error object. + if (typeof error.stack === 'string') { + return error.stack; + } + + // Otherwise, dump the object. + return util.format('%o', error); +} + +/** + * Catch and report unexpected error. + * @param {unknown} error The thrown error object. + * @returns {void} + */ +function onFatalError(error) { + process.exitCode = 2; + const message = getErrorMessage(error); + console.error(` +An unhandled exception occurred! +${message}`); +} + +(async function main() { + process.on('uncaughtException', onFatalError); + process.on('unhandledRejection', onFatalError); + + /** @type {import('../src/index')} */ + const cli = (() => { + if (process.env.USE_TS_ESLINT_SRC == null) { + // using an ignore because after a build a ts-expect-error will no longer error because TS will follow the + // build maps to the source files... + // @ts-ignore - have to reference the built file, not the src file + return require('../dist/index'); + } + + // ensure ts-node is registered correctly + // eslint-disable-next-line import/no-extraneous-dependencies + require('ts-node').register({ + transpileOnly: true, + project: require('path').resolve(__dirname, '..', 'tsconfig.json'), + }); + return require('../src/index'); + })(); + + await cli.execute(); +})().catch(onFatalError); diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js new file mode 100644 index 000000000000..c23ca67fbc68 --- /dev/null +++ b/packages/cli/jest.config.js @@ -0,0 +1,20 @@ +'use strict'; + +// @ts-check +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + globals: { + 'ts-jest': { + isolatedModules: true, + }, + }, + testEnvironment: 'node', + transform: { + ['^.+\\.tsx?$']: 'ts-jest', + }, + testRegex: ['./tests/.+\\.test\\.ts$'], + collectCoverage: false, + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + coverageReporters: ['text-summary', 'lcov'], +}; diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000000..2dfb77e67250 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,77 @@ +{ + "name": "@typescript-eslint/cli", + "version": "5.9.0", + "description": "TypeScript-ESLint CLI", + "keywords": [ + "eslint", + "typescript", + "estree", + "cli" + ], + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "files": [ + "dist", + "package.json", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "https://github.com/typescript-eslint/typescript-eslint.git", + "directory": "packages/cli" + }, + "bugs": { + "url": "https://github.com/typescript-eslint/typescript-eslint/issues" + }, + "license": "MIT", + "main": "dist/index.js", + "bin": { + "ts-eslint": "./bin/ts-eslint.js" + }, + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -b tsconfig.build.json", + "postbuild": "downlevel-dts dist _ts3.4/dist", + "clean": "tsc -b tsconfig.build.json --clean", + "postclean": "rimraf dist && rimraf _ts3.4 && rimraf coverage", + "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", + "typecheck": "tsc -p tsconfig.json --noEmit", + "ts-eslint": "USE_TS_ESLINT_SRC=1 ts-node --transpile-only ./bin/ts-eslint.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "typesVersions": { + "<3.8": { + "*": [ + "_ts3.4/*" + ] + } + }, + "dependencies": { + "@typescript-eslint/experimental-utils": "5.9.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "ink": "^3.2.0", + "ink-use-stdout-dimensions": "^1.0.5", + "is-glob": "^4.0.3", + "jest-worker": "^27.4.5", + "react": "^17.0.2", + "semver": "^7.3.5", + "v8-compile-cache": "^2.3.0", + "yargs": "^17.3.1" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", + "typescript": "*" + }, + "devDependencies": { + "@types/is-glob": "*", + "@types/semver": "*", + "@types/yargs": "*" + } +} diff --git a/packages/cli/project.json b/packages/cli/project.json new file mode 100644 index 000000000000..4b2535e26f5b --- /dev/null +++ b/packages/cli/project.json @@ -0,0 +1,5 @@ +{ + "root": "packages/cli", + "type": "library", + "implicitDependencies": [] +} diff --git a/packages/cli/src/FileEnumerator.ts b/packages/cli/src/FileEnumerator.ts new file mode 100644 index 000000000000..46be3f7e62ba --- /dev/null +++ b/packages/cli/src/FileEnumerator.ts @@ -0,0 +1,31 @@ +import { version } from 'eslint/package.json'; +import * as semver from 'semver'; + +const isESLintV8 = semver.major(version) >= 8; + +declare class _FileEnumerator { + constructor(options?: { + readonly cwd?: string; + readonly extensions?: string | null; + readonly globInputPaths?: boolean; + readonly errorOnUnmatchedPattern?: boolean; + readonly ignore?: boolean; + }); + + iterateFiles( + patternOrPatterns: string | readonly string[], + ): IterableIterator<{ + readonly filePath: string; + readonly config: unknown; + readonly ignored: boolean; + }>; +} + +const FileEnumerator = ( + isESLintV8 + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + require('eslint/use-at-your-own-risk').FileEnumerator + : require('eslint/lib/cli-engine/file-enumerator') +) as typeof _FileEnumerator; + +export { FileEnumerator }; diff --git a/packages/cli/src/commands/Command.ts b/packages/cli/src/commands/Command.ts new file mode 100644 index 000000000000..a05b466031a9 --- /dev/null +++ b/packages/cli/src/commands/Command.ts @@ -0,0 +1,54 @@ +import type { ArgumentsCamelCase, CommandBuilder, CommandModule } from 'yargs'; + +import type { Reporter } from '../reporters/Reporter'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export interface Command + extends CommandModule< + TRawOpts & GlobalOptions, + TProcessedOpts & GlobalOptions + > { + /** object declaring the options the command accepts, or a function accepting and returning a yargs instance */ + builder: CommandBuilder< + TRawOpts & GlobalOptions, + TProcessedOpts & GlobalOptions + >; + /** string used as the description for the command in help text, use `false` for a hidden command */ + describe: string; + /** a function which will be passed the parsed argv. */ + handler: ( + args: ArgumentsCamelCase, + ) => Promise; +} + +export interface CommandNoOpts extends CommandModule { + /** string (or array of strings) that executes this command when given on the command line, first string may contain positional args */ + command: ReadonlyArray | string; + /** string used as the description for the command in help text, use `false` for a hidden command */ + describe: string; + /** a function which will be passed the parsed argv. */ + handler: () => void | Promise; +} + +export enum ReporterConfig { + ink = 'ink', + plain = 'plain', +} + +// numbering is important as each level includes the prior levels +// eg level >= LogLevel.error should include both info and debug +export enum LogLevel { + error = 0, + info = 1, + debug = 2, +} + +export interface GlobalOptionsRaw { + logLevel: keyof typeof LogLevel; + reporter: ReporterConfig; +} + +export interface GlobalOptions { + logLevel: LogLevel; + reporter: Reporter; +} diff --git a/packages/cli/src/commands/env.ts b/packages/cli/src/commands/env.ts new file mode 100644 index 000000000000..8c4ae4655aae --- /dev/null +++ b/packages/cli/src/commands/env.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-console -- no need to use a reporter for this command as it's intended to dump straight to console */ + +import Module from 'module'; + +import type { CommandNoOpts } from './Command'; + +const packagesToResolve = [ + '@typescript-eslint/cli', + '@typescript-eslint/eslint-plugin', + '@typescript-eslint/experimental-utils', + '@typescript-eslint/parser', + '@typescript-eslint/scope-manager', + '@typescript-eslint/typescript-estree', + 'typescript', +]; + +const command: CommandNoOpts = { + aliases: ['environment', 'env-info', 'support-info'], + command: 'env', + describe: + 'Prints information about your environment to provide when reporting an issue.', + handler: () => { + const nodeVersion = process.version.replace(/^v/, ''); + const versions: Record = { + node: nodeVersion, + }; + + const require = Module.createRequire(process.cwd()); + + let maxPackageLength = 0; + let maxVersionLength = Math.max(nodeVersion.length, 'version'.length); + for (const pkg of packagesToResolve) { + const packageJson = require(`${pkg}/package.json`) as { version: string }; + versions[pkg] = packageJson.version; + + maxPackageLength = Math.max(maxPackageLength, pkg.length); + maxVersionLength = Math.max(maxVersionLength, packageJson.version.length); + } + + console.log( + '|', + 'package'.padEnd(maxPackageLength, ' '), + '|', + 'version'.padEnd(maxVersionLength, ' '), + '|', + ); + console.log( + '|', + '-'.padEnd(maxPackageLength, '-'), + '|', + '-'.padEnd(maxVersionLength, '-'), + '|', + ); + for (const [pkg, version] of Object.entries(versions)) { + console.log( + '|', + pkg.padEnd(maxPackageLength, ' '), + '|', + version.padEnd(maxVersionLength, ' '), + '|', + ); + } + }, +}; + +export { command }; diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts new file mode 100644 index 000000000000..4aec7a66da90 --- /dev/null +++ b/packages/cli/src/commands/lint.ts @@ -0,0 +1,77 @@ +import { sync as globSync } from 'globby'; +import isGlob from 'is-glob'; + +import type { AbsolutePath } from '../path'; +import { getAbsolutePath } from '../path'; +import { lintProjects } from '../workers/lintProjects'; +import type { Command } from './Command'; + +export interface OptionsRaw { + cwd: string; + project: readonly string[]; +} +export interface Options { + cwd: AbsolutePath; + project: readonly string[]; +} + +const command: Command = { + builder: yargs => { + return yargs.options({ + project: { + alias: ['p', 'projects'], + array: true, + demandOption: 'Must pass at least one project.', + describe: + 'Path to a tsconfig, relative to the CWD. Can also specify a glob pattern - ensure you wrap in quotes to prevent CLI expansion of the glob.', + normalize: true, + requiresArg: true, + type: 'string', + }, + cwd: { + coerce: (cwd: OptionsRaw['cwd']) => getAbsolutePath(cwd), + default: process.cwd(), + describe: + 'The path to the current working directory to use for the run.', + type: 'string', + }, + }); + }, + command: 'lint', + describe: 'Lint your project.', + handler: async args => { + const projects = new Set(); + const globProjects: string[] = []; + for (const project of args.project) { + if (isGlob(project)) { + globProjects.push(project); + } else { + projects.add(getAbsolutePath(project, args.cwd)); + } + } + for (const globProject of globSync( + ['!**/node_modules/**', ...globProjects], + { cwd: args.cwd }, + )) { + projects.add(getAbsolutePath(globProject, args.cwd)); + } + + args.reporter.debug('found the following projects', Array.from(projects)); + args.reporter.log('Linting', projects.size, 'projects'); + + const lintReturnState = await lintProjects( + { + cwd: args.cwd, + logLevel: args.logLevel, + projects: Array.from(projects), + }, + args.reporter, + ); + + args.reporter.debug('linting finished'); + + process.exitCode = lintReturnState; + }, +}; + +export { command }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 000000000000..862b4d02faff --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,75 @@ +import path from 'path'; +import type { CommandModule } from 'yargs'; +import * as yargs from 'yargs'; + +import type { GlobalOptions, GlobalOptionsRaw } from './commands/Command'; +import { LogLevel, ReporterConfig } from './commands/Command'; +import * as lintCommand from './commands/lint'; +import { InkReporter } from './reporters/InkReporter'; +import { PlainReporter } from './reporters/PlainReporter'; + +function prepareReporter(argvRaw: GlobalOptionsRaw): void { + /* yargs and dynamic js - the middleware overwrites the options object + @ts-expect-error */ + const argvResult = argvRaw as GlobalOptions; + + const logLevel = LogLevel[argvRaw.logLevel]; + argvResult.logLevel = logLevel; + + switch (argvRaw.reporter) { + default: + case ReporterConfig.ink: + argvResult.reporter = new InkReporter(logLevel); + return; + + case ReporterConfig.plain: + argvResult.reporter = new PlainReporter(logLevel); + return; + } +} + +async function execute(): Promise { + // @ts-expect-error - yargs and dynamic js + const argv: GlobalOptions = await yargs + .usage('Usage: $0 -p ./tsconfig.json') + .scriptName('ts-eslint') + .strict(true) + .commandDir(path.resolve(__dirname, 'commands'), { + extensions: ['js', 'ts'], + exclude: /\.d\.ts$/, + visit: (mod: { readonly command: CommandModule }) => { + return mod.command; + }, + }) + // specify the `lint` command as the default command + .command('$0', false, lintCommand.command as CommandModule) + // global options + .options({ + logLevel: { + describe: 'Control the log level', + default: 'error' as const, + enum: Object.keys(LogLevel) as (keyof typeof LogLevel)[], + global: true, + type: 'string', + }, + reporter: { + describe: 'Control how the console output is rendered', + default: process.env.CI ? ReporterConfig.plain : ReporterConfig.ink, + enum: Object.values(ReporterConfig) as ReporterConfig[], + global: true, + type: 'string', + }, + }) + .middleware(prepareReporter) + .help() + .wrap(yargs.terminalWidth()).argv; + + argv.reporter.cleanup(); +} + +if (require.main === module) { + // for easy testing - execute directly if we are the main node script + void execute(); +} + +export { execute }; diff --git a/packages/cli/src/path.ts b/packages/cli/src/path.ts new file mode 100644 index 000000000000..6fbe44ad3cd3 --- /dev/null +++ b/packages/cli/src/path.ts @@ -0,0 +1,15 @@ +import path from 'path'; + +type AbsolutePath = string & { __AbsolutePathBrand: unknown }; +function getAbsolutePath( + p: string, + base: string = process.cwd(), +): AbsolutePath { + if (path.isAbsolute(p)) { + return p as AbsolutePath; + } + return path.resolve(base, p) as AbsolutePath; +} + +export type { AbsolutePath }; +export { getAbsolutePath }; diff --git a/packages/cli/src/reporters/InkReporter/Divider.tsx b/packages/cli/src/reporters/InkReporter/Divider.tsx new file mode 100644 index 000000000000..25787633673e --- /dev/null +++ b/packages/cli/src/reporters/InkReporter/Divider.tsx @@ -0,0 +1,18 @@ +import * as ink from 'ink'; +import React from 'react'; + +interface Props { + readonly stdoutWidth: number; +} +const Divider = React.memo(({ stdoutWidth }: Props): JSX.Element => { + const bar = ''.padEnd(stdoutWidth, '-'); + + return ( + + {bar} + + ); +}); + +export type { Props as DividerProps }; +export { Divider }; diff --git a/packages/cli/src/reporters/InkReporter/InkCLUI.tsx b/packages/cli/src/reporters/InkReporter/InkCLUI.tsx new file mode 100644 index 000000000000..1db17b8779f3 --- /dev/null +++ b/packages/cli/src/reporters/InkReporter/InkCLUI.tsx @@ -0,0 +1,95 @@ +import * as ink from 'ink'; +import useStdoutDimensions from 'ink-use-stdout-dimensions'; +import React, { useMemo } from 'react'; + +import type { LogLevel } from '../../commands/Command'; +import { Divider } from './Divider'; +import type { Progress } from './ProgressBar'; +import { ProgressBar } from './ProgressBar'; + +function sortEntriesByKey(obj: Record): [string, T][] { + return Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : 1)); +} + +interface Props { + hideProgress?: boolean; + logLevel: LogLevel; + lintResult: { [id: string]: string }; + logLines: { type: 'log' | 'debug' | 'error'; line: string }[]; + progress: { + [id: string]: Progress; + }; +} + +function InkCLUI(props: Readonly): JSX.Element { + const [stdoutWidth] = useStdoutDimensions(); + const lintResult = useMemo( + () => sortEntriesByKey(props.lintResult), + [props.lintResult], + ); + const progress = useMemo( + () => sortEntriesByKey(props.progress), + [props.progress], + ); + return ( + + + {(line, idx): JSX.Element => ( + + { + switch (line.type) { + case 'debug': + return { + color: 'gray', + }; + + case 'error': + return { + color: 'red', + }; + + case 'log': + return { + color: 'white', + }; + } + })()} + > + {line.line} + + + )} + + + {lintResult.length > 0 && ( + + {lintResult.map(([id, result]) => ( + + {id} + {result} + + ))} + + )} + + {props.hideProgress !== true && ( + <> + + + + {progress.map(([id, progress]) => ( + + {id} + + + ))} + + + )} + + ); +} + +export type { Props as InkCLUIProps }; +export { InkCLUI }; diff --git a/packages/cli/src/reporters/InkReporter/ProgressBar.tsx b/packages/cli/src/reporters/InkReporter/ProgressBar.tsx new file mode 100644 index 000000000000..6b85b899a396 --- /dev/null +++ b/packages/cli/src/reporters/InkReporter/ProgressBar.tsx @@ -0,0 +1,36 @@ +import * as ink from 'ink'; +import React, { useMemo } from 'react'; + +interface Progress { + readonly current: number; + readonly max: number; +} +interface Props extends Progress { + readonly stdoutWidth: number; +} +const ProgressBar = React.memo( + ({ current, max, stdoutWidth }: Props): JSX.Element => { + const progress = current / max; + const text = `[${current}/${max} (${(progress * 100).toFixed(1)}%)]`; + // calculate the "max length" of the text so the progress bar remains a constant width + const textMaxLength = useMemo( + () => `[${max}/${max} (100.0%)]`.length, + [max], + ); + const bar = ''.padEnd( + Math.round(progress * stdoutWidth) - textMaxLength, + '█', + ); + + return ( + + {bar} + + {text} + + ); + }, +); + +export type { Progress, Props as ProgressBarProps }; +export { ProgressBar }; diff --git a/packages/cli/src/reporters/InkReporter/index.tsx b/packages/cli/src/reporters/InkReporter/index.tsx new file mode 100644 index 000000000000..6bf4c3b1caeb --- /dev/null +++ b/packages/cli/src/reporters/InkReporter/index.tsx @@ -0,0 +1,120 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import * as ink from 'ink'; +import React from 'react'; + +import { LogLevel } from '../../commands/Command'; +import { throttle } from '../../throttle'; +import type { Reporter } from '../Reporter'; +import { ReporterBase } from '../Reporter'; +import type { InkCLUIProps } from './InkCLUI'; +import { InkCLUI } from './InkCLUI'; + +class InkReporter extends ReporterBase implements Reporter { + #state: InkCLUIProps = { + logLevel: LogLevel.error, + logLines: [], + progress: {}, + lintResult: {}, + }; + + readonly #inkInstance: ink.Instance; + + constructor(logLevel: LogLevel) { + super(logLevel); + this.#state.logLevel = logLevel; + + this.#inkInstance = ink.render(Initializing...); + } + + #prepareLog( + type: InkCLUIProps['logLines'][number]['type'], + args: readonly unknown[], + ): InkCLUIProps['logLines'][number][] { + return args + .map(arg => { + if (typeof arg === 'object') { + return JSON.stringify(arg, null, 2); + } + return String(arg); + }) + .join(' ') + .split('\n') + .map(line => ({ + type, + line, + })); + } + + log(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.info)) { + return; + } + this.#state.logLines = this.#state.logLines.concat( + this.#prepareLog('log', args), + ); + this.#render(); + } + + error(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.error)) { + return; + } + this.#state.logLines = this.#state.logLines.concat( + this.#prepareLog('error', args), + ); + this.#render(); + } + + debug(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.debug)) { + return; + } + this.#state.logLines = this.#state.logLines.concat( + this.#prepareLog('debug', args), + ); + this.#render(); + } + + updateProgress(id: string, current: number, max: number): void { + this.#state.progress = { + ...this.#state.progress, + [id]: { current, max }, + }; + this.#render(); + } + + async logLintResult( + id: string, + results: TSESLint.ESLint.LintResult[], + ): Promise { + const eslint = new TSESLint.ESLint(); + const formatter = await eslint.loadFormatter(); + const result = formatter.format(results); + this.#state.lintResult = { + ...this.#state.lintResult, + [id]: result === '' ? '\n\u2714 No Lint Reports\n' : result, + }; + this.#render(); + } + + #render = throttle( + (): void => { + this.#inkInstance.rerender(); + }, + // at most 30 times a second + 1000 / 30, + ); + + cleanup(): void { + // ensure no more throttled renders occur + this.#render.cleanup(); + + // do one final render without the progress bar before cleaning up ink + this.#inkInstance.rerender( + , + ); + this.#inkInstance.cleanup(); + } +} + +export { InkReporter }; diff --git a/packages/cli/src/reporters/PlainReporter.ts b/packages/cli/src/reporters/PlainReporter.ts new file mode 100644 index 000000000000..b6231b5a7949 --- /dev/null +++ b/packages/cli/src/reporters/PlainReporter.ts @@ -0,0 +1,50 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; + +import { LogLevel } from '../commands/Command'; +import type { Reporter } from './Reporter'; +import { ReporterBase } from './Reporter'; + +class PlainReporter extends ReporterBase implements Reporter { + log(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.info)) { + return; + } + this.console.log(...args); + } + + error(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.error)) { + return; + } + this.console.error(...args); + } + + debug(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.debug)) { + return; + } + this.console.log(...args); + } + + updateProgress(): void { + // plain reporter intentionally hides progress reports + } + + async logLintResult( + _id: string, + results: TSESLint.ESLint.LintResult[], + ): Promise { + const eslint = new TSESLint.ESLint(); + const formatter = await eslint.loadFormatter(); + const result = formatter.format(results); + if (result !== '') { + this.log(); + } + } + + cleanup(): void { + // nothing to cleanup + } +} + +export { PlainReporter }; diff --git a/packages/cli/src/reporters/Reporter.ts b/packages/cli/src/reporters/Reporter.ts new file mode 100644 index 000000000000..73992ff64919 --- /dev/null +++ b/packages/cli/src/reporters/Reporter.ts @@ -0,0 +1,59 @@ +import type { TSESLint } from '@typescript-eslint/experimental-utils'; + +import type { LogLevel } from '../commands/Command'; + +interface ReporterLogs { + log(...args: readonly unknown[]): void; + error(...args: readonly unknown[]): void; + debug(...args: readonly unknown[]): void; +} + +interface Reporter extends ReporterLogs { + // called when a worker updates its status + updateProgress(id: string, current: number, max: number): void; + // called when lint results are ready + logLintResult( + id: string, + results: TSESLint.ESLint.LintResult[], + ): Promise; + // called at the end of the program + cleanup(): void; +} + +type OldConsole = Readonly; +const oldConsole: OldConsole = { + ...console, +}; + +abstract class ReporterBase implements ReporterLogs { + protected readonly console: OldConsole = oldConsole; + protected readonly logLevel: LogLevel; + + constructor(logLevel: LogLevel) { + this.logLevel = logLevel; + + console.log = (...args): void => { + this.log(...args); + }; + console.info = (...args): void => { + this.log(...args); + }; + console.warn = (...args): void => { + this.log('[WARN]', ...args); + }; + console.error = (...args): void => { + this.error(...args); + }; + } + + abstract log(...args: readonly unknown[]): void; + abstract error(...args: readonly unknown[]): void; + abstract debug(...args: readonly unknown[]): void; + + protected isLogLevel(level: LogLevel): boolean { + return this.logLevel >= level; + } +} + +export type { Reporter, ReporterLogs }; +export { ReporterBase }; diff --git a/packages/cli/src/reporters/WorkerReporter.ts b/packages/cli/src/reporters/WorkerReporter.ts new file mode 100644 index 000000000000..c9680b09b904 --- /dev/null +++ b/packages/cli/src/reporters/WorkerReporter.ts @@ -0,0 +1,135 @@ +/** + * This reporter is used by workers to ensure they log in a consistent, parsable format that can be sent + * across the thread boundary. + */ + +import { LogLevel } from '../commands/Command'; +import type { ThrottledFunction } from '../throttle'; +import { throttle } from '../throttle'; +import type { Reporter } from './Reporter'; +import { ReporterBase } from './Reporter'; + +interface WorkerMessageBase { + readonly id: string; +} + +interface WorkerMessageLog extends WorkerMessageBase { + readonly type: 'log'; + readonly messages: readonly unknown[]; +} +interface WorkerMessageDebug extends WorkerMessageBase { + readonly type: 'debug'; + readonly messages: readonly unknown[]; +} +interface WorkerMessageUpdateProgress extends WorkerMessageBase { + readonly type: 'update-progress'; + readonly current: number; + readonly max: number; +} +interface WorkerMessageError extends WorkerMessageBase { + readonly type: 'error'; + readonly error: { + readonly message: string; + readonly stack: string; + }; +} +type WorkerMessage = + | WorkerMessageLog + | WorkerMessageDebug + | WorkerMessageUpdateProgress + | WorkerMessageError; + +class WorkerReporter extends ReporterBase { + readonly #id: string; + + constructor(id: string, logLevel: LogLevel) { + super(logLevel); + this.#id = id; + } + + static handleMessage(raw: Buffer, reporter: Reporter): void { + const messages = raw.toString('utf8').trim(); + for (const rawMessage of messages.split('\n')) { + try { + const message = JSON.parse(rawMessage) as WorkerMessage; + switch (message.type) { + case 'log': + reporter.log(`[${message.id}]`, ...message.messages); + break; + + case 'debug': + reporter.debug(`[${message.id}]`, ...message.messages); + break; + + case 'error': + reporter.error(`[${message.id}]`, message.error); + break; + + case 'update-progress': + reporter.updateProgress(message.id, message.current, message.max); + break; + } + } catch (ex) { + reporter.error(rawMessage, '\n', ex); + } + } + } + + log(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.info)) { + return; + } + + const message: WorkerMessageLog = { + id: this.#id, + type: 'log', + messages: args, + }; + this.console.log(JSON.stringify(message)); + } + + error(error: Error): void { + if (!this.isLogLevel(LogLevel.error)) { + return; + } + + const message: WorkerMessageError = { + id: this.#id, + type: 'error', + error: { + message: error.message, + stack: error.stack!, + }, + }; + this.console.log(JSON.stringify(message)); + } + + debug(...args: readonly unknown[]): void { + if (!this.isLogLevel(LogLevel.debug)) { + return; + } + + const message: WorkerMessageDebug = { + id: this.#id, + type: 'debug', + messages: args, + }; + this.console.log(JSON.stringify(message)); + } + + // throttle so that we don't overflow the log buffer if the main thread can't keep up + updateProgress: ThrottledFunction<[number, number]> = throttle( + (current: number, max: number): void => { + const message: WorkerMessageUpdateProgress = { + id: this.#id, + type: 'update-progress', + current, + max, + }; + this.console.log(JSON.stringify(message)); + }, + 0, + ); +} + +export { WorkerReporter }; diff --git a/packages/cli/src/throttle.ts b/packages/cli/src/throttle.ts new file mode 100644 index 000000000000..5f7c50086d4b --- /dev/null +++ b/packages/cli/src/throttle.ts @@ -0,0 +1,49 @@ +interface ThrottledFunction { + (...args: T): void; + cleanup: () => void; + flush: () => void; + immediate: (...args: T) => void; +} + +function throttle( + fn: (...args: T) => void, + waitMs: number, +): ThrottledFunction { + let timeout: NodeJS.Timeout | null = null; + let lastArgs: T; + const retVal = ((...args) => { + lastArgs = args; + + if (timeout) { + return; + } + + timeout = setTimeout(() => { + timeout = null; + + fn(...lastArgs); + }, waitMs); + }) as ThrottledFunction; + + retVal.cleanup = (): void => { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + }; + retVal.immediate = (...args): void => { + retVal.cleanup(); + fn(...args); + }; + retVal.flush = (): void => { + if (timeout) { + fn(...lastArgs); + } + retVal.cleanup; + }; + + return retVal; +} + +export type { ThrottledFunction }; +export { throttle }; diff --git a/packages/cli/src/workers/lintProjects/index.ts b/packages/cli/src/workers/lintProjects/index.ts new file mode 100644 index 000000000000..ec6bcff14e40 --- /dev/null +++ b/packages/cli/src/workers/lintProjects/index.ts @@ -0,0 +1,96 @@ +import { Worker as JestWorker } from 'jest-worker'; +import path from 'path'; + +import type { LogLevel } from '../../commands/Command'; +import type { AbsolutePath } from '../../path'; +import type { Reporter } from '../../reporters/Reporter'; +import { WorkerReporter } from '../../reporters/WorkerReporter'; +import type * as lintProjectWorker from './lintProjectWorker'; + +// the value is also reported as the exit code +const enum LintReturnState { + Success = 0, + Warning = 1, + Error = 2, + Fatal = 3, +} + +async function lintProjects( + { + cwd, + logLevel, + projects, + }: { + readonly cwd: string; + readonly logLevel: LogLevel; + readonly projects: readonly AbsolutePath[]; + }, + reporter: Reporter, +): Promise { + const worker = new JestWorker(require.resolve('./lintProjectWorker'), { + enableWorkerThreads: false, + forkOptions: + process.env.USE_TS_ESLINT_SRC == null + ? {} + : { execArgv: ['-r', 'ts-node/register/transpile-only'] }, + exposedMethods: ['processProject'], + maxRetries: 0, + numWorkers: projects.length, + }) as JestWorker & { + processProject: typeof lintProjectWorker.processProject; + }; + + worker.getStdout().on('data', (data: Buffer) => { + WorkerReporter.handleMessage(data, reporter); + }); + worker.getStderr().on('data', (data: Buffer) => { + reporter.error(data.toString('utf8')); + }); + + const promises: Promise[] = []; + for (const project of projects) { + promises.push( + (async (): Promise => { + try { + const id = path.relative(cwd, project); + const results = await worker.processProject({ + cwd, + id, + logLevel, + project, + }); + await reporter.logLintResult(id, results); + + if (results.find(r => r.messages.find(m => m.fatal === true))) { + return LintReturnState.Fatal; + } + if (results.find(r => r.errorCount > 0)) { + return LintReturnState.Error; + } + // TODO - implement "max-warnings" option from ESLint + if (results.find(r => r.warningCount > 0)) { + return LintReturnState.Warning; + } + return LintReturnState.Success; + } catch (ex: unknown) { + reporter.error( + `An error occurred when linting project ${project}:`, + (ex as object).toString(), + ); + return LintReturnState.Fatal; + } + })(), + ); + } + + let maxReturnState = LintReturnState.Success; + for (const report of await Promise.all(promises)) { + maxReturnState = Math.max(report, maxReturnState); + } + // as per jest-worker docs - intentionally not awaiting the end + void worker.end(); + + return maxReturnState; +} + +export { lintProjects }; diff --git a/packages/cli/src/workers/lintProjects/lintProjectWorker.ts b/packages/cli/src/workers/lintProjects/lintProjectWorker.ts new file mode 100644 index 000000000000..fc73e3be86cc --- /dev/null +++ b/packages/cli/src/workers/lintProjects/lintProjectWorker.ts @@ -0,0 +1,113 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import debugLogger from 'debug'; +import * as ts from 'typescript'; + +import type { LogLevel } from '../../commands/Command'; +import { FileEnumerator } from '../../FileEnumerator'; +import type { AbsolutePath } from '../../path'; +import { getAbsolutePath } from '../../path'; +import { WorkerReporter } from '../../reporters/WorkerReporter'; + +async function processProject({ + cwd, + id, + logLevel, + project, +}: { + cwd: string; + id: string; + logLevel: LogLevel; + project: AbsolutePath; +}): Promise { + const reporter = new WorkerReporter(id, logLevel); + // ensure the debug package logs to our debug channel + debugLogger.log = reporter.debug.bind(reporter); + + const tsconfig = ts.getParsedCommandLineOfConfigFile( + project, + {}, + { + fileExists: ts.sys.fileExists, + getCurrentDirectory: ts.sys.getCurrentDirectory, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile, + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + onUnRecoverableConfigFileDiagnostic: diagnostic => { + throw new Error( + ts.flattenDiagnosticMessageText( + diagnostic.messageText, + ts.sys.newLine, + ), + ); + }, + }, + ); + + if (tsconfig == null) { + throw new Error(`Unable to parse the project "${project}"`); + } + + reporter.debug(tsconfig.fileNames.length, 'files included in the tsconfig'); + + // TODO - handling for --fix + + // force single-run inference + process.env.TSESTREE_SINGLE_RUN = 'true'; + const eslint = new TSESLint.ESLint({ + cwd, + overrideConfig: { + parserOptions: { + allowAutomaticSingleRunInference: true, + project: [project], + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, + }, + }, + }); + + let count = 0; + reporter.updateProgress.immediate(0, tsconfig.fileNames.length); + + const absFilenames = tsconfig.fileNames.map(f => getAbsolutePath(f)); + const enumerator = new FileEnumerator({ + cwd, + }); + + const results: Promise[] = []; + for (const { filePath, ignored } of enumerator.iterateFiles(absFilenames)) { + if (ignored) { + reporter.updateProgress(++count, tsconfig.fileNames.length); + continue; + } + results.push( + (async (): Promise => { + try { + const result = await eslint.lintFiles(filePath); + reporter.updateProgress(++count, tsconfig.fileNames.length); + return result; + } catch (ex: unknown) { + // eslint will sometimes report exceptions with the current AST node + // if that happens it'll crash the IPC because the AST contains circular references + if (isObject(ex) && isObject(ex.currentNode)) { + const { parent: _, ...currentNode } = ex.currentNode; + ex.currentNode = currentNode; + throw ex; + } + + throw ex; + } + })(), + ); + } + const flattenedResults = (await Promise.all(results)).flat(); + + // ensure the updates are fully flushed + reporter.updateProgress.flush(); + + return flattenedResults; +} + +function isObject(thing: unknown): thing is Record { + return typeof thing === 'object' && thing != null; +} + +export { processProject }; diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json new file mode 100644 index 000000000000..cce9185120a9 --- /dev/null +++ b/packages/cli/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "jsx": "react", + "rootDir": "./src", + "resolveJsonModule": true + }, + "include": ["src", "typings"], + "references": [{ "path": "../experimental-utils/tsconfig.build.json" }] +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000000..f564a295a18d --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "composite": false, + "checkJs": true, + "jsx": "react", + "rootDir": "." + }, + "include": ["src", "bin", "typings", "tests", "tools"], + "references": [{ "path": "../experimental-utils/tsconfig.build.json" }] +} diff --git a/packages/cli/typings/v8-compile-cache.d.ts b/packages/cli/typings/v8-compile-cache.d.ts new file mode 100644 index 000000000000..ae4f4ea1b3dc --- /dev/null +++ b/packages/cli/typings/v8-compile-cache.d.ts @@ -0,0 +1,3 @@ +declare module 'v8-compile-cache' { + export {}; +} diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 874aae4be62e..d0cc2a07627b 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -58,6 +58,7 @@ "@types/debug": "*", "@types/marked": "*", "@types/prettier": "*", + "@types/semver": "*", "chalk": "^4.1.2", "marked": "^3.0.7", "prettier": "*", diff --git a/packages/experimental-utils/src/ts-eslint/Linter.ts b/packages/experimental-utils/src/ts-eslint/Linter.ts index 77c338f5a753..8ac49e466448 100644 --- a/packages/experimental-utils/src/ts-eslint/Linter.ts +++ b/packages/experimental-utils/src/ts-eslint/Linter.ts @@ -176,7 +176,7 @@ namespace Linter { export interface ConfigOverride extends BaseConfig { excludedFiles?: string | string[]; - files: string | string[]; + files?: string | string[]; } export interface Config extends BaseConfig { diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index 18ecbee7943f..72c132dbbf2e 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -38,6 +38,7 @@ interface ParserOptions { lib?: Lib[]; // typescript-estree specific + allowAutomaticSingleRunInference?: boolean; comment?: boolean; debugLevel?: DebugLevel; errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; diff --git a/packages/typescript-estree/tsconfig.json b/packages/typescript-estree/tsconfig.json index bc1141dd051a..e2e1b75ba330 100644 --- a/packages/typescript-estree/tsconfig.json +++ b/packages/typescript-estree/tsconfig.json @@ -4,7 +4,7 @@ "composite": false, "rootDir": "." }, - "include": ["src", "typings", "tests", "tools"], + "include": ["bin", "src", "typings", "tests", "tools"], "exclude": ["tests/fixtures/**/*"], "references": [ { "path": "../types/tsconfig.build.json" }, diff --git a/patches/@types+yargs+17.0.8.patch b/patches/@types+yargs+17.0.8.patch new file mode 100644 index 000000000000..4f52788ff63a --- /dev/null +++ b/patches/@types+yargs+17.0.8.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@types/yargs/index.d.ts b/node_modules/@types/yargs/index.d.ts +index 842b4a4..9c9eef4 100755 +--- a/node_modules/@types/yargs/index.d.ts ++++ b/node_modules/@types/yargs/index.d.ts +@@ -846,7 +846,7 @@ declare namespace yargs { + + // prettier-ignore + type InferredOptionTypeInner = +- O extends { default: any, coerce: (arg: any) => infer T } ? T : ++ O extends { coerce: (arg: any) => infer T } ? T : + O extends { default: infer D } ? D : + O extends { type: "count" } ? number : + O extends { count: true } ? number : diff --git a/workspace.json b/workspace.json index 6fbf76fb0786..e88c9d227970 100644 --- a/workspace.json +++ b/workspace.json @@ -2,6 +2,7 @@ "version": 2, "projects": { "@typescript-eslint/ast-spec": "packages/ast-spec", + "@typescript-eslint/cli": "packages/cli", "@typescript-eslint/eslint-plugin": "packages/eslint-plugin", "@typescript-eslint/eslint-plugin-internal": "packages/eslint-plugin-internal", "@typescript-eslint/eslint-plugin-tslint": "packages/eslint-plugin-tslint", diff --git a/yarn.lock b/yarn.lock index 3c305e231a21..b54344d0487b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3751,6 +3751,13 @@ dependencies: "@types/node" "*" +"@types/node-fetch@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-3.0.3.tgz#9d969c9a748e841554a40ee435d26e53fa3ee899" + integrity sha512-HhggYPH5N+AQe/OmN6fmhKmRRt2XuNJow+R3pQwJxOOF9GuwM7O2mheyGeIrs5MOIeNjDEdgdoyHBOrFeJBR3g== + dependencies: + node-fetch "*" + "@types/node@*", "@types/node@12.20.24", "@types/node@^16.11.4": version "16.11.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.4.tgz#90771124822d6663814f7c1c9b45a6654d8fd964" @@ -3873,6 +3880,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== +"@types/yargs@*", "@types/yargs@^17.0.8": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.8.tgz#d23a3476fd3da8a0ea44b5494ca7fa677b9dad4c" + integrity sha512-wDeUwiUmem9FzsyysEwRukaEdDNcwbROvQ9QGRKaLI6t+IltNzbn4/i4asmB10auvZGQCzSQ6t0GSczEThlUXw== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^16.0.0": version "16.0.4" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" @@ -3880,6 +3894,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/yoga-layout@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@types/yoga-layout/-/yoga-layout-1.9.2.tgz#efaf9e991a7390dc081a0b679185979a83a9639a" + integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -4011,6 +4030,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -4458,6 +4482,11 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +auto-bind@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" + integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== + autoprefixer@^10.3.5, autoprefixer@^10.3.7: version "10.4.0" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8" @@ -5067,7 +5096,7 @@ clear-module@^4.1.1: parent-module "^2.0.0" resolve-from "^5.0.0" -cli-boxes@^2.2.1: +cli-boxes@^2.2.0, cli-boxes@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== @@ -5168,6 +5197,13 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-excerpt@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-3.0.0.tgz#fcfb6748c03dba8431c19f5474747fad3f250f10" + integrity sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw== + dependencies: + convert-to-spaces "^1.0.1" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -5506,6 +5542,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +convert-to-spaces@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz#7e3e48bbe6d997b1417ddca2868204b4d3d85715" + integrity sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU= + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -7222,6 +7263,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + findup-sync@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0" @@ -7345,6 +7393,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^7.0.1, fs-extra@~7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.0, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -7355,15 +7412,6 @@ fs-extra@^9.0.0, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@~7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -8277,6 +8325,40 @@ init-package-json@^2.0.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" +ink-use-stdout-dimensions@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ink-use-stdout-dimensions/-/ink-use-stdout-dimensions-1.0.5.tgz#7739876c00284840601c4150aa84eb7adc143de2" + integrity sha512-rVsqnw4tQEAJUoknU09+zHdDf30GJdkumkHr0iz/TOYMYEZJkYqziQSGJAM+Z+M603EDfO89+Nxyn/Ko2Zknfw== + +ink@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ink/-/ink-3.2.0.tgz#434793630dc57d611c8fe8fffa1db6b56f1a16bb" + integrity sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg== + dependencies: + ansi-escapes "^4.2.1" + auto-bind "4.0.0" + chalk "^4.1.0" + cli-boxes "^2.2.0" + cli-cursor "^3.1.0" + cli-truncate "^2.1.0" + code-excerpt "^3.0.0" + indent-string "^4.0.0" + is-ci "^2.0.0" + lodash "^4.17.20" + patch-console "^1.0.0" + react-devtools-core "^4.19.1" + react-reconciler "^0.26.2" + scheduler "^0.20.2" + signal-exit "^3.0.2" + slice-ansi "^3.0.0" + stack-utils "^2.0.2" + string-width "^4.2.2" + type-fest "^0.12.0" + widest-line "^3.1.0" + wrap-ansi "^6.2.0" + ws "^7.5.5" + yoga-layout-prebuilt "^1.9.6" + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" @@ -9471,6 +9553,13 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -10454,6 +10543,15 @@ node-emoji@^1.10.0: dependencies: lodash "^4.17.21" +node-fetch@*, node-fetch@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.1.0.tgz#714f4922dc270239487654eaeeab86b8206cb52e" + integrity sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.2" + formdata-polyfill "^4.0.10" + node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -10466,15 +10564,6 @@ node-fetch@^2.6.0, node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" -node-fetch@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.1.0.tgz#714f4922dc270239487654eaeeab86b8206cb52e" - integrity sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.2" - formdata-polyfill "^4.0.10" - node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -11199,6 +11288,30 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-console@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-1.0.0.tgz#19b9f028713feb8a3c023702a8cc8cb9f7466f9d" + integrity sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA== + +patch-package@^6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148" + integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^2.4.2" + cross-spawn "^6.0.5" + find-yarn-workspace-root "^2.0.0" + fs-extra "^7.0.1" + is-ci "^2.0.0" + klaw-sync "^6.0.0" + minimist "^1.2.0" + open "^7.4.2" + rimraf "^2.6.3" + semver "^5.6.0" + slash "^2.0.0" + tmp "^0.0.33" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -11942,6 +12055,14 @@ react-dev-utils@12.0.0-next.47: strip-ansi "^6.0.0" text-table "^0.2.0" +react-devtools-core@^4.19.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.22.1.tgz#b276d42f860bedc373c9b3c0f5f96734318dd453" + integrity sha512-pvpNDHE7p0FtcCmIWGazoY8LLVfBI9sw0Kf10kdHhPI9Tzt3OG/qEt16GrAbE0keuna5WzX3r1qPKVjqOqsuUg== + dependencies: + shell-quote "^1.6.1" + ws "^7" + react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -12008,6 +12129,15 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" +react-reconciler@^0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91" + integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -12955,6 +13085,11 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -13193,7 +13328,7 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stack-utils@^2.0.3: +stack-utils@^2.0.2, stack-utils@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== @@ -13893,6 +14028,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.12.0.tgz#f57a27ab81c68d136a51fd71467eff94157fa1ee" + integrity sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -14255,7 +14395,7 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3: +v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -14719,6 +14859,11 @@ write-pkg@^4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" +ws@^7, ws@^7.5.5: + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + ws@^7.3.1, ws@^7.4.6: version "7.5.5" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" @@ -14804,6 +14949,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^21.0.0: + version "21.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" + integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== + yargs@15.4.1, yargs@^15.0.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -14847,6 +14997,19 @@ yargs@^17.0.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" @@ -14857,6 +15020,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yoga-layout-prebuilt@^1.9.6: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz#2936fbaf4b3628ee0b3e3b1df44936d6c146faa6" + integrity sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g== + dependencies: + "@types/yoga-layout" "1.9.2" + z-schema@~5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.2.tgz#f410394b2c9fcb9edaf6a7511491c0bb4e89a504"