From 3d674bbeb937cc6fd0080d86557f81bc03fe95cc Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 11 Apr 2024 13:40:18 +0100 Subject: [PATCH 01/10] Add CLI utilities --- packages/cli-utils/src/commands/doctor.ts | 2 + packages/cli-utils/src/term/csi.ts | 311 ++++++++++++++++++++++ packages/cli-utils/src/term/index.ts | 5 + packages/cli-utils/src/term/mode.ts | 37 +++ packages/cli-utils/src/term/symbols.ts | 155 +++++++++++ packages/cli-utils/src/term/tty.ts | 42 +++ packages/cli-utils/src/term/write.ts | 65 +++++ 7 files changed, 617 insertions(+) create mode 100644 packages/cli-utils/src/term/csi.ts create mode 100644 packages/cli-utils/src/term/index.ts create mode 100644 packages/cli-utils/src/term/mode.ts create mode 100644 packages/cli-utils/src/term/symbols.ts create mode 100644 packages/cli-utils/src/term/tty.ts create mode 100644 packages/cli-utils/src/term/write.ts diff --git a/packages/cli-utils/src/commands/doctor.ts b/packages/cli-utils/src/commands/doctor.ts index a6979975..a23b9eda 100644 --- a/packages/cli-utils/src/commands/doctor.ts +++ b/packages/cli-utils/src/commands/doctor.ts @@ -6,6 +6,7 @@ import type { TsConfigJson } from 'type-fest'; import { resolveTypeScriptRootDir } from '@gql.tada/internal'; import { existsSync } from 'node:fs'; +import { cmd, write } from '../term'; import { getGraphQLSPConfig } from '../lsp'; const MINIMUM_VERSIONS = { @@ -26,6 +27,7 @@ export async function executeTadaDoctor() { const file = path.resolve(packageJsonPath); packageJsonContents = JSON.parse(await fs.readFile(file, 'utf-8')); } catch (error) { + write([cmd(cmd.Style, cmd.style.BrightRed), '']); console.error( 'Failed to read package.json in current working directory, try running the doctor command in your workspace folder.' ); diff --git a/packages/cli-utils/src/term/csi.ts b/packages/cli-utils/src/term/csi.ts new file mode 100644 index 00000000..84484eeb --- /dev/null +++ b/packages/cli-utils/src/term/csi.ts @@ -0,0 +1,311 @@ +import { hasColor } from './tty'; + +// See: http://xtermjs.org/docs/api/vtfeatures/#csi +enum Command { + InsertChars = '@', + ScrollLeft = 'SP@', + Up = 'A', + ScrollRight = 'SPA', + Down = 'B', + Forward = 'C', + Backward = 'D', + PrevLine = 'E', + NextLine = 'F', + ToColumn = 'G', + ToPosition = 'H', + Tab = 'I', + Erase = 'J', + EraseLine = 'K', + InsertLines = 'L', + DeleteLines = 'M', + DeleteChars = 'P', + ScrollUp = 'S', + ScrollDown = 'T', + EraseChars = 'X', + TabBackwards = 'Z', + RepeatChar = 'b', + ToRow = 'd', + DownLine = 'e', + TabClear = 'g', + SetMode = 'h', + UnsetMode = 'l', + SetPrivateMode = '?h', + UnsetPrivateMode = '?l', + ResetPrivateMode = '?r', + Style = 'm', + Reset = '!p', + Protect = '"q', + Cursor = 'SPq', + SetMargin = 'r', + SaveCursor = 's', + RestoreCursor = 'u', + InsertColumns = "'}", + DeleteColumns = "'~", +} + +enum TabClear { + Current = 0, + All = 3, +} + +enum Erase { + Forward = 0, + Backward = 1, + All = 2, + Scrollback = 3, +} + +enum EraseLine { + Forward = 0, + Backward = 1, + All = 2, +} + +enum Protect { + Insert = 4, + AutomaticNewline = 20, +} + +enum Mode { + Insert = 4, + AutomaticNewline = 20, +} + +enum PrivateMode { + AppCursorKeys = 1, + USASCII = 2, + Column132 = 3, + OriginMode = 6, + AutoWrap = 7, + AutoRepeat = 8, + X10Mouse = 9, + BlinkCursor = 12, + ShowCursor = 25, + ReverseWrapAround = 45, + AlternativeScreenBuffer = 47, + AppKeypad = 66, + X11Mouse = 1000, + CellMotionMouseTracking = 1002, + AllMotionMouseTracking = 1003, + FocusEvents = 1004, + Utf8Mouse = 1005, + SGRMouse = 1006, + UrxvtMouse = 1015, + SGRPixelsMouse = 1016, + SaveCursor = 1048, + BracketedPaste = 2004, +} + +enum Cursor { + Empty = 0, + Block = 1, + BlinkBlock = 2, + Underline = 3, + BlinkUnderline = 4, + Bar = 5, + BlinkBar = 6, +} + +enum Style { + Reset = 0, + Bold = 1, + Faint = 2, + Italic = 3, + Underline = 4, + Blink = 5, + RapidBlink = 6, + Invert = 7, + Invisible = 8, + Strikethrough = 9, + DoubleUnderlined = 21, + Normal = 22, // No Bold, No Faint + NoItalic = 23, + NoUnderline = 24, + NoBlink = 25, + NoInvert = 27, + Visible = 28, + NoStrikethrough = 29, + + Black = 30, + Red = 31, + Green = 32, + Yellow = 33, + Blue = 34, + Magenta = 35, + Cyan = 36, + White = 37, + Foreground = 39, + + OnBlack = 40, + OnRed = 41, + OnGreen = 42, + OnYellow = 43, + OnBlue = 44, + OnMagenta = 45, + OnCyan = 46, + OnWhite = 47, + OnBackground = 49, + + BrightBlack = 90, + BrightRed = 91, + BrightGreen = 92, + BrightYellow = 93, + BrightBlue = 94, + BrightMagenta = 95, + BrightCyan = 96, + BrightWhite = 97, + + OnBrightBlack = 100, + OnBrightRed = 101, + OnBrightGreen = 102, + OnBrightYellow = 103, + OnBrightBlue = 104, + OnBrightMagenta = 105, + OnBrightCyan = 106, + OnBrightWhite = 107, + + DoubleUnderline = '4:2', + CurlyUnderline = '4:3', + DottedUnderline = '4:4', + DashedUnderline = '4:5', +} + +type CSI = '\x1B['; +const CSI = '\x1B['; + +type CommandNoParam = + | Command.Reset + | Command.SaveCursor + | Command.RestoreCursor + | Command.ResetPrivateMode; + +type CommandSingleParam = + | Command.ScrollLeft + | Command.ScrollRight + | Command.ScrollUp + | Command.ScrollDown + | Command.Up + | Command.Down + | Command.Backward + | Command.Forward + | Command.PrevLine + | Command.NextLine + | Command.DownLine + | Command.ToColumn + | Command.ToRow + | Command.Tab + | Command.TabBackwards + | Command.InsertChars + | Command.InsertLines + | Command.InsertColumns + | Command.DeleteChars + | Command.DeleteLines + | Command.DeleteColumns + | Command.RepeatChar; + +type CommandParam = + | number + | TabClear + | Erase + | EraseLine + | Protect + | Cursor + | Mode + | PrivateMode + | Style + | readonly Mode[] + | readonly PrivateMode[] + | readonly Style[]; + +function _cmd(code: CommandNoParam): `${CSI}${CommandNoParam}`; +function _cmd(code: CommandSingleParam, count?: number): `${CSI}${number}${CommandSingleParam}`; +function _cmd(code: Command.TabClear, mode?: TabClear): `${CSI}${TabClear}${Command.TabClear}`; +function _cmd(code: Command.Erase, mode?: Erase): `${CSI}${Erase}${Command.Erase}`; +function _cmd(code: Command.EraseLine, mode?: EraseLine): `${CSI}${EraseLine}${Command.EraseLine}`; +function _cmd(code: Command.Protect, mode?: Protect): `${CSI}${Protect}${Command.Protect}`; +function _cmd(code: Command.Cursor, style: Cursor): `${CSI}${Cursor}${Command.Cursor}`; + +function _cmd( + code: Command.ToPosition, + row: number, + column: number +): `${CSI}${number};${number}${Command.ToPosition}`; + +function _cmd( + code: Command.SetMargin, + top: number, + bottom: number +): `${CSI}${number};${number}${Command.SetMargin}`; + +function _cmd( + code: Command.SetMode, + modes: Mode | readonly Mode[] +): `${CSI}${Mode}${Command.SetMode}`; + +function _cmd( + code: Command.SetPrivateMode, + modes: PrivateMode | readonly PrivateMode[] +): `${CSI}?${PrivateMode}${Command.SetMode}`; + +function _cmd( + code: Command.UnsetMode, + modes: Mode | readonly Mode[] +): `${CSI}${Mode}${Command.UnsetMode}`; + +function _cmd( + code: Command.UnsetPrivateMode, + modes: PrivateMode | readonly PrivateMode[] +): `${CSI}?${PrivateMode}${Command.UnsetMode}`; + +function _cmd( + code: Command.Style, + styles: Style | readonly Style[] +): `${CSI}${Style}${Command.Style}`; + +function _cmd(code: Command, a?: CommandParam, b?: number): cmdCode { + if (!hasColor && code === Command.Style) return ''; + let out = CSI; + if (code === Command.SetPrivateMode) { + out += '?'; + code = Command.SetMode; + } else if (code === Command.UnsetPrivateMode) { + out += '?'; + code = Command.UnsetMode; + } + if (Array.isArray(a)) { + out += a.join(';'); + } else if (a != null) { + out += `${a}`; + if (b != null) out += `;${b}`; + } + out += code; + return out as cmdCode; +} + +interface cmdCodes { + tabClear: typeof TabClear; + erase: typeof Erase; + eraseLine: typeof EraseLine; + protect: typeof Protect; + cursor: typeof Cursor; + mode: typeof Mode; + privateMode: typeof PrivateMode; + style: typeof Style; +} + +type cmd = typeof _cmd & cmdCodes & typeof Command; + +export const cmd: cmd = Object.assign(_cmd as cmd, Command, { + tabClear: TabClear, + erase: Erase, + eraseLine: EraseLine, + protect: Protect, + cursor: Cursor, + mode: Mode, + privateMode: PrivateMode, + style: Style, +}); + +export type cmdCode = `${CSI}${string}${Command}` | ''; + +export { Command, TabClear, Erase, EraseLine, Protect, Mode, PrivateMode, Cursor, Style }; diff --git a/packages/cli-utils/src/term/index.ts b/packages/cli-utils/src/term/index.ts new file mode 100644 index 00000000..acecbd38 --- /dev/null +++ b/packages/cli-utils/src/term/index.ts @@ -0,0 +1,5 @@ +export { mode, modeOff } from './mode'; +export { CLIError, log, error } from './write'; +export * from './csi'; +export * from './symbols'; +export * from './tty'; diff --git a/packages/cli-utils/src/term/mode.ts b/packages/cli-utils/src/term/mode.ts new file mode 100644 index 00000000..44c40c6e --- /dev/null +++ b/packages/cli-utils/src/term/mode.ts @@ -0,0 +1,37 @@ +import type { PrivateMode } from './csi'; +import { cmd, Mode } from './csi'; +import { isTTY, output } from './tty'; + +const mode = (...modes: readonly (Mode | PrivateMode)[]): void => { + if (isTTY) { + const normalModes: Mode[] = []; + const privateModes: PrivateMode[] = []; + for (const mode of modes) { + if (mode === Mode.Insert || mode === Mode.AutomaticNewline) { + normalModes.push(mode); + } else { + privateModes.push(mode); + } + } + if (normalModes.length) output.write(cmd(cmd.SetMode, normalModes)); + if (privateModes.length) output.write(cmd(cmd.SetPrivateMode, privateModes)); + } +}; + +const modeOff = (...modes: readonly (Mode | PrivateMode)[]): void => { + if (isTTY) { + const normalModes: Mode[] = []; + const privateModes: PrivateMode[] = []; + for (const mode of modes) { + if (mode === Mode.Insert || mode === Mode.AutomaticNewline) { + normalModes.push(mode); + } else { + privateModes.push(mode); + } + } + if (normalModes.length) output.write(cmd(cmd.UnsetMode, normalModes)); + if (privateModes.length) output.write(cmd(cmd.UnsetPrivateMode, privateModes)); + } +}; + +export { mode, modeOff }; diff --git a/packages/cli-utils/src/term/symbols.ts b/packages/cli-utils/src/term/symbols.ts new file mode 100644 index 00000000..e0915732 --- /dev/null +++ b/packages/cli-utils/src/term/symbols.ts @@ -0,0 +1,155 @@ +export const enum Chars { + Bell = '\x07', + Newline = '\x0a', + Tab = '\x09', + Space = ' ', +} + +export const enum Box { + TopLeft = '┌', + TopRight = '┐', + BottomLeft = '└', + BottomRight = '┘', + Vertical = '│', + VerticalRight = '├', + VerticalLeft = '┤', + Horizontal = '─', + HorizontalDown = '┬', + HorizontalUp = '┴', + Cross = '┼', +} + +export const enum HeavyBox { + TopLeft = '┏', + TopRight = '┓', + BottomLeft = '┗', + BottomRight = '┛', + Vertical = '┃', + VerticalRight = '┣', + VerticalLeft = '┫', + Horizontal = '━', + HorizontalDown = '┳', + HorizontalUp = '┻', + Cross = '╋', +} + +export const enum DoubleBox { + TopLeft = '╔', + TopRight = '╗', + BottomLeft = '╚', + BottomRight = '╝', + Vertical = '║', + VerticalRight = '╠', + VerticalLeft = '╣', + Horizontal = '═', + HorizontalDown = '╦', + HorizontalUp = '╩', + Cross = '╬', +} + +export enum RoundedBox { + TopLeft = '╭', + TopRight = '╮', + BottomLeft = '╰', + BottomRight = '╯', + Vertical = '│', + VerticalRight = '├', + VerticalLeft = '┤', + Horizontal = '─', + HorizontalDown = '┬', + HorizontalUp = '┴', + Cross = '┼', +} + +export const enum Arrow { + Right = '→', + Left = '←', + Up = '↑', + Down = '↓', + LeftRight = '↔', + UpDown = '↕', +} + +export const enum Triangle { + Right = '▶', + Left = '◀', + Up = '▲', + Down = '▼', +} + +export const enum SmallTriangle { + Right = '▸', + Left = '◂', + Up = '▴', + Down = '▾', +} + +export const enum Line { + DiagonalCross = '╳', + Diagonalbackwards = '╲', + Diagonal = '╱', + VerticalDashed = '┆', + VerticalDashedHeavy = '┇', + Vertical = '│', + VerticalHeavy = '┃', + HorizontalDashed = '┄', + HorizontalDashedHeavy = '┅', + Horizontal = '─', + HorizontalHeavy = '━', +} + +export const enum Circle { + Filled = '●', + Outline = '◯', + OutlineFilled = '◉', + Dotted = '◌', + Doubled = '◎', + Small = '•', + HalfLeft = '◐', + HalfTop = '◓', + HalfRight = '◑', + HalfBottom = '◒', +} + +export const enum Chevron { + Default = '', + Small = '›', + Heavy = '❯', +} + +export const enum Diamond { + Default = '◆', + Outline = '◇', +} + +export const enum Square { + Default = '■', + Outline = '☐', + Crossed = '☒', +} + +export const enum Heart { + Default = '❤︎', + Outline = '♥', +} + +export const enum Icons { + Tick = '✓', + TickSwoosh = '✔', + Cross = '×', + CrossSwoosh = '✘', + Home = '⌂', + Note = '♪', + Warning = '⚠', + Info = 'ℹ', + Star = '★', +} + +export const enum Shade { + Light = '░', + Medium = '▒', + Heavy = '▓', +} + +export const dotSpinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; +export const circleSpinner = ['◐', '◓', '◑', '◒']; diff --git a/packages/cli-utils/src/term/tty.ts b/packages/cli-utils/src/term/tty.ts new file mode 100644 index 00000000..cb8521ce --- /dev/null +++ b/packages/cli-utils/src/term/tty.ts @@ -0,0 +1,42 @@ +import type { WriteStream } from 'node:tty'; +import { cmd, PrivateMode } from './csi'; + +export let isTTY = process.env.TERM !== 'dumb' && !process.env.CI; +export let pipe: WriteStream | null = null; +export let output: WriteStream = process.stdout; +if (!output.isTTY && process.stderr.isTTY) { + output = process.stderr; + pipe = process.stdout; +} else { + isTTY = output.isTTY; +} + +const hasColorArg = process.argv.includes('--color'); +const hasColorEnv = process.env.FORCE_COLOR || (!process.env.NO_COLOR && !process.env.CI); +export const hasColor = (isTTY && hasColorEnv) || hasColorArg; + +export let columns = 0; +export let rows = 0; + +export function init() { + if (isTTY) { + columns = output.columns; + rows = output.rows; + output.on('resize', () => { + columns = output.columns; + rows = output.rows; + }); + + process.stdin.setRawMode(true); + output.write(cmd(cmd.UnsetPrivateMode, PrivateMode.ShowCursor)); + + process.on('exit', () => { + output.write( + cmd(cmd.Reset) + + cmd(cmd.ResetPrivateMode) + + cmd(cmd.SetPrivateMode, PrivateMode.ShowCursor) + + '\n' + ); + }); + } +} diff --git a/packages/cli-utils/src/term/write.ts b/packages/cli-utils/src/term/write.ts new file mode 100644 index 00000000..09e574ab --- /dev/null +++ b/packages/cli-utils/src/term/write.ts @@ -0,0 +1,65 @@ +import { isTTY, output } from './tty'; + +export class CLIError extends Error { + output: string; + constructor(message: string) { + super(stripAnsi(message)); + this.output = message; + } +} + +const ansiRegex = /([\x1B\x9B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])/g; +const stripAnsi = (input: string) => input.replace(ansiRegex, ''); + +let buffer = ''; +let frame: any; + +function flush() { + if (frame != null) clearImmediate(frame); + frame = null; + output.write(buffer); + buffer = ''; +} + +function writeRaw(input: string) { + buffer += isTTY ? input : stripAnsi(input); + if (frame == null) frame = setImmediate(flush); +} + +function log(input: readonly string[], ...args: readonly string[]): void; +function log(input: string, ...args: readonly string[]): void; +function log(): void; + +function log(input?: string | readonly string[], ...args: readonly string[]): void { + if (!input) { + flush(); + } else if (Array.isArray(input)) { + let argIndex = 0; + for (let index = 0; index < input.length; index++) { + writeRaw(input[index]); + if (argIndex < args.length) writeRaw(args[argIndex++]); + } + } else { + writeRaw(input as string); + for (const arg of args) writeRaw(arg); + } +} + +function error(input: readonly string[], ...args: readonly string[]): CLIError; +function error(input: string, ...args: readonly string[]): CLIError; + +function error(input: string | readonly string[], ...args: readonly string[]): CLIError { + let message = ''; + if (Array.isArray(input)) { + let argIndex = 0; + for (let index = 0; index < input.length; index++) { + message += input[index]; + if (argIndex < args.length) message += args[argIndex++]; + } + } else { + message = input as string; + } + return new CLIError(message); +} + +export { log, error }; From f97d102c806f260ad806a96d5aac483d123fa963 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 11 Apr 2024 14:59:22 +0100 Subject: [PATCH 02/10] Add custom logger for `doctor` command --- packages/cli-utils/src/commands/doctor.ts | 75 +++++----- packages/cli-utils/src/loggers/check.ts | 161 ++++++++++++++++++++++ 2 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 packages/cli-utils/src/loggers/check.ts diff --git a/packages/cli-utils/src/commands/doctor.ts b/packages/cli-utils/src/commands/doctor.ts index a23b9eda..13bd62ff 100644 --- a/packages/cli-utils/src/commands/doctor.ts +++ b/packages/cli-utils/src/commands/doctor.ts @@ -6,8 +6,8 @@ import type { TsConfigJson } from 'type-fest'; import { resolveTypeScriptRootDir } from '@gql.tada/internal'; import { existsSync } from 'node:fs'; -import { cmd, write } from '../term'; import { getGraphQLSPConfig } from '../lsp'; +import { print, error } from '../loggers/check'; const MINIMUM_VERSIONS = { typescript: '4.1.0', @@ -15,7 +15,9 @@ const MINIMUM_VERSIONS = { lsp: '1.0.0', }; -export async function executeTadaDoctor() { +async function* task() { + yield { text: 'Checking TypeScript version' }; + // Check TypeScript version const cwd = process.cwd(); const packageJsonPath = path.resolve(cwd, 'package.json'); @@ -23,15 +25,15 @@ export async function executeTadaDoctor() { dependencies: Record; devDependencies: Record; }; + try { const file = path.resolve(packageJsonPath); packageJsonContents = JSON.parse(await fs.readFile(file, 'utf-8')); - } catch (error) { - write([cmd(cmd.Style, cmd.style.BrightRed), '']); - console.error( - 'Failed to read package.json in current working directory, try running the doctor command in your workspace folder.' + } catch (_error) { + throw error( + 'Failed to read package.json in current working directory\n' + + 'Try running the doctor command in your workspace folder.' ); - return; } const deps = Object.entries({ @@ -41,38 +43,36 @@ export async function executeTadaDoctor() { const typeScriptVersion = deps.find((x) => x[0] === 'typescript'); if (!typeScriptVersion) { - console.error('Failed to find a "typescript" installation, try installing one.'); - return; + throw error('Failed to find a "typescript" installation, try installing one.'); } else if (semiver(typeScriptVersion[1], MINIMUM_VERSIONS.typescript) === -1) { // TypeScript version lower than v4.1 which is when they introduced template lits - console.error( - `Found an outdated "TypeScript" version, gql.tada requires at least ${MINIMUM_VERSIONS.typescript}.` + throw error( + `Found an outdated "TypeScript" version.\ngql.tada requires at least ${MINIMUM_VERSIONS.typescript}.` ); - return; } + yield { text: 'Checking installed dependencies' }; + const gqlspVersion = deps.find((x) => x[0] === "'@0no-co/graphqlsp'"); if (!gqlspVersion) { - console.error('Failed to find a "@0no-co/graphqlsp" installation, try installing one.'); - return; + throw error('Failed to find a "@0no-co/graphqlsp" installation, try installing one.'); } else if (semiver(gqlspVersion[1], MINIMUM_VERSIONS.lsp) === -1) { - console.error( + throw error( `Found an outdated "@0no-co/graphqlsp" version, gql.tada requires at least ${MINIMUM_VERSIONS.lsp}.` ); - return; } const gqlTadaVersion = deps.find((x) => x[0] === "'gql.tada'"); if (!gqlTadaVersion) { - console.error('Failed to find a "gql.tada" installation, try installing one.'); - return; + throw error('Failed to find a "gql.tada" installation, try installing one.'); } else if (semiver(gqlTadaVersion[1], '1.0.0') === -1) { - console.error( + throw error( `Found an outdated "gql.tada" version, gql.tada requires at least ${MINIMUM_VERSIONS.tada}.` ); - return; } + yield { text: 'Checking tsconfig.json' }; + const tsconfigpath = path.resolve(cwd, 'tsconfig.json'); const root = (await resolveTypeScriptRootDir(tsconfigpath)) || cwd; @@ -80,38 +80,33 @@ export async function executeTadaDoctor() { try { const file = path.resolve(root, 'tsconfig.json'); tsconfigContents = await fs.readFile(file, 'utf-8'); - } catch (error) { - console.error( + } catch (_error) { + throw error( 'Failed to read tsconfig.json in current working directory, try adding a "tsconfig.json".' ); - return; } let tsConfig: TsConfigJson; try { tsConfig = parse(tsconfigContents) as TsConfigJson; - } catch (err) { - console.error('Unable to parse tsconfig.json in current working directory.', err); - return; + } catch (_error: any) { + throw error(['Unable to parse tsconfig.json in current working directory.\n', `${_error}`]); } // Check GraphQLSP version, later on we can check if a ts version is > 5.5.0 to use gql.tada/lsp instead of // the LSP package. const config = getGraphQLSPConfig(tsConfig); if (!config) { - console.error(`Missing a "@0no-co/graphqlsp" plugin in your tsconfig.`); - return; + throw error(`Missing a "@0no-co/graphqlsp" plugin in your tsconfig.`); } // TODO: this is optional I guess with the CLI being there and all if (!config.tadaOutputLocation) { - console.error(`Missing a "tadaOutputLocation" setting in your GraphQLSP configuration.`); - return; + throw error(`Missing a "tadaOutputLocation" setting in your GraphQLSP configuration.`); } if (!config.schema) { - console.error(`Missing a "schema" setting in your GraphQLSP configuration.`); - return; + throw error(`Missing a "schema" setting in your GraphQLSP configuration.`); } else { const isFile = typeof config.schema === 'string' && @@ -119,18 +114,24 @@ export async function executeTadaDoctor() { if (isFile) { const resolvedFile = path.resolve(root, config.schema as string); if (!existsSync(resolvedFile)) { - console.error(`The schema setting does not point at an existing file "${resolvedFile}"`); - return; + throw error(`The schema setting does not point at an existing file "${resolvedFile}"`); } } else { try { typeof config.schema === 'string' ? new URL(config.schema) : new URL(config.schema.url); } catch (e) { - console.error( - `The schema setting does not point at a valid URL "${JSON.stringify(config.schema)}"` + throw error( + `The schema setting does not point at a valid URL: "${JSON.stringify(config.schema)}"` ); - return; } } } } + +export async function executeTadaDoctor() { + await print({ + title: 'doctor', + description: 'Detecting problems in your setup', + task, + }); +} diff --git a/packages/cli-utils/src/loggers/check.ts b/packages/cli-utils/src/loggers/check.ts new file mode 100644 index 00000000..2ad54a6a --- /dev/null +++ b/packages/cli-utils/src/loggers/check.ts @@ -0,0 +1,161 @@ +import { CLIError, Chars, Icons, HeavyBox, Style, circleSpinner, init, cmd, log } from '../term'; +export { error } from '../term'; + +function delay(ms = 750) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function printTitle(title: string) { + log([ + cmd(cmd.Style, Style.BrightBlack), + HeavyBox.TopLeft, + Chars.Space, + cmd(cmd.Style, [Style.Magenta, Style.Invert]), + Chars.Space, + title.trim(), + Chars.Space, + cmd(cmd.Style, Style.Reset), + Chars.Newline, + ]); +} + +function printLine(text?: string) { + log([cmd(cmd.Style, Style.BrightBlack), HeavyBox.Vertical, Chars.Space]); + if (text) log(text); + log([cmd(cmd.Style, Style.Reset), Chars.Newline]); +} + +function printCompletedTask(description: string, isLast = false) { + log([ + cmd(cmd.Style, Style.BrightBlack), + isLast ? HeavyBox.BottomLeft : HeavyBox.VerticalRight, + Chars.Space, + cmd(cmd.Style, Style.Green), + Icons.TickSwoosh, + Chars.Space, + cmd(cmd.Style, Style.Foreground), + description, + cmd(cmd.Style, Style.Reset), + Chars.Newline, + ]); +} + +function printFailedTask(description: string) { + log([ + cmd(cmd.Style, Style.BrightBlack), + HeavyBox.BottomLeft, + Chars.Space, + cmd(cmd.Style, Style.BrightRed), + Icons.CrossSwoosh, + Chars.Space, + cmd(cmd.Style, Style.Foreground), + description, + cmd(cmd.Style, Style.Reset), + Chars.Newline, + ]); +} + +function printRunningTask(description: string) { + let state = 0; + function print() { + if (state) { + log([cmd(cmd.DeleteLines, 1), cmd(cmd.ToColumn, 1)]); + } + log([ + cmd(cmd.Style, Style.Magenta), + circleSpinner[state++ % circleSpinner.length], + Chars.Space, + cmd(cmd.Style, Style.Foreground), + description, + cmd(cmd.Style, Style.Reset), + ]); + } + + const intervalId = setInterval(print, 150); + return () => { + clearInterval(intervalId); + log([cmd(cmd.DeleteLines, 1), cmd(cmd.ToColumn, 1)]); + }; +} + +interface PrintSignal { + text: string; +} + +interface PrintConfig { + title: string; + description?: string; + task(): AsyncIterable; +} + +export async function print(config: PrintConfig) { + init(); + + printTitle(config.title); + if (config.description) printLine(config.description); + + printLine(); + + let current: PrintSignal | undefined; + let cancel: (() => void) | undefined; + try { + for await (const signal of config.task()) { + if (cancel) { + cancel(); + } + if (current) { + printCompletedTask(current.text); + printLine(); + } + cancel = printRunningTask((current = signal).text); + await delay(); + } + + if (cancel) { + cancel(); + } + if (current) { + printCompletedTask(current.text, true); + printLine(); + } + + log([ + Chars.Newline, + cmd(cmd.Style, [cmd.style.Green, cmd.style.Invert]), + ' Done ', + cmd(cmd.Style, cmd.style.NoInvert), + Chars.Space, + 'You are all set and ready to go.', + ]); + } catch (error: any) { + if (cancel) { + cancel(); + } + + if (current) { + printFailedTask(current.text); + } + + if (error instanceof CLIError) { + log([ + Chars.Newline, + cmd(cmd.Style, [cmd.style.BrightRed, cmd.style.Invert]), + ' Error ', + cmd(cmd.Style, cmd.style.NoInvert), + Chars.Newline, + `${error.output}`, + ]); + } else { + log([ + Chars.Newline, + cmd(cmd.Style, [cmd.style.BrightRed, cmd.style.Invert]), + ' Unexpected Error ', + cmd(cmd.Style, cmd.style.NoInvert), + Chars.Newline, + `${error}`, + ]); + } + } +} From fb47e475f8f931723fa4213b891f122965720ff0 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 11 Apr 2024 17:29:56 +0100 Subject: [PATCH 03/10] Update to create initTTY interface --- packages/cli-utils/src/term/csi.ts | 6 +- packages/cli-utils/src/term/index.ts | 3 +- packages/cli-utils/src/term/mode.ts | 37 ------ packages/cli-utils/src/term/tty.ts | 190 +++++++++++++++++++++------ packages/cli-utils/src/term/write.ts | 63 +++------ 5 files changed, 177 insertions(+), 122 deletions(-) delete mode 100644 packages/cli-utils/src/term/mode.ts diff --git a/packages/cli-utils/src/term/csi.ts b/packages/cli-utils/src/term/csi.ts index 84484eeb..49ebde6f 100644 --- a/packages/cli-utils/src/term/csi.ts +++ b/packages/cli-utils/src/term/csi.ts @@ -1,4 +1,8 @@ -import { hasColor } from './tty'; +let hasColor = false; + +export function _setColor(color: boolean) { + hasColor = color; +} // See: http://xtermjs.org/docs/api/vtfeatures/#csi enum Command { diff --git a/packages/cli-utils/src/term/index.ts b/packages/cli-utils/src/term/index.ts index acecbd38..84583337 100644 --- a/packages/cli-utils/src/term/index.ts +++ b/packages/cli-utils/src/term/index.ts @@ -1,5 +1,4 @@ -export { mode, modeOff } from './mode'; -export { CLIError, log, error } from './write'; +export { CLIError, text, error } from './write'; export * from './csi'; export * from './symbols'; export * from './tty'; diff --git a/packages/cli-utils/src/term/mode.ts b/packages/cli-utils/src/term/mode.ts deleted file mode 100644 index 44c40c6e..00000000 --- a/packages/cli-utils/src/term/mode.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PrivateMode } from './csi'; -import { cmd, Mode } from './csi'; -import { isTTY, output } from './tty'; - -const mode = (...modes: readonly (Mode | PrivateMode)[]): void => { - if (isTTY) { - const normalModes: Mode[] = []; - const privateModes: PrivateMode[] = []; - for (const mode of modes) { - if (mode === Mode.Insert || mode === Mode.AutomaticNewline) { - normalModes.push(mode); - } else { - privateModes.push(mode); - } - } - if (normalModes.length) output.write(cmd(cmd.SetMode, normalModes)); - if (privateModes.length) output.write(cmd(cmd.SetPrivateMode, privateModes)); - } -}; - -const modeOff = (...modes: readonly (Mode | PrivateMode)[]): void => { - if (isTTY) { - const normalModes: Mode[] = []; - const privateModes: PrivateMode[] = []; - for (const mode of modes) { - if (mode === Mode.Insert || mode === Mode.AutomaticNewline) { - normalModes.push(mode); - } else { - privateModes.push(mode); - } - } - if (normalModes.length) output.write(cmd(cmd.UnsetMode, normalModes)); - if (privateModes.length) output.write(cmd(cmd.UnsetPrivateMode, privateModes)); - } -}; - -export { mode, modeOff }; diff --git a/packages/cli-utils/src/term/tty.ts b/packages/cli-utils/src/term/tty.ts index cb8521ce..b0f1fa20 100644 --- a/packages/cli-utils/src/term/tty.ts +++ b/packages/cli-utils/src/term/tty.ts @@ -1,42 +1,158 @@ import type { WriteStream } from 'node:tty'; -import { cmd, PrivateMode } from './csi'; - -export let isTTY = process.env.TERM !== 'dumb' && !process.env.CI; -export let pipe: WriteStream | null = null; -export let output: WriteStream = process.stdout; -if (!output.isTTY && process.stderr.isTTY) { - output = process.stderr; - pipe = process.stdout; -} else { - isTTY = output.isTTY; +import { emitKeypressEvents } from 'node:readline'; + +import type { Source } from 'wonka'; +import { make, pipe, share, takeLast, takeUntil, onPush, toPromise, map } from 'wonka'; + +import { cmd, _setColor, Mode, PrivateMode } from './csi'; +import { text } from './write'; + +export interface KeypressEvent { + text?: string; + sequence: string; + name: string; + ctrl: boolean; + meta: boolean; + shift: boolean; } -const hasColorArg = process.argv.includes('--color'); -const hasColorEnv = process.env.FORCE_COLOR || (!process.env.NO_COLOR && !process.env.CI); -export const hasColor = (isTTY && hasColorEnv) || hasColorArg; - -export let columns = 0; -export let rows = 0; - -export function init() { - if (isTTY) { - columns = output.columns; - rows = output.rows; - output.on('resize', () => { - columns = output.columns; - rows = output.rows; - }); - - process.stdin.setRawMode(true); - output.write(cmd(cmd.UnsetPrivateMode, PrivateMode.ShowCursor)); - - process.on('exit', () => { - output.write( - cmd(cmd.Reset) + - cmd(cmd.ResetPrivateMode) + - cmd(cmd.SetPrivateMode, PrivateMode.ShowCursor) + - '\n' - ); - }); +export interface TTY { + output: WriteStream; + pipeTo: WriteStream | null; + inputSource: Source; + cancelSource: Source; + + write(input: readonly string[], ...args: readonly string[]): void; + write(...input: readonly string[]): void; + + pipeOutput(input: Source): Promise; + + mode(...modes: readonly (Mode | PrivateMode)[]): void; + modeOff(...modes: readonly (Mode | PrivateMode)[]): void; +} + +export function initTTY(): TTY { + let isTTY = process.env.TERM !== 'dumb' && !process.env.CI; + let pipeTo: WriteStream | null = null; + let output: WriteStream = process.stdout; + if (!output.isTTY && process.stderr.isTTY) { + output = process.stderr; + pipeTo = process.stdout; + } else { + isTTY = output.isTTY; + } + + const hasColorArg = process.argv.includes('--color'); + const hasColorEnv = 'FORCE_COLOR' in process.env || (!process.env.NO_COLOR && !process.env.CI); + + _setColor((isTTY && hasColorEnv) || hasColorArg); + emitKeypressEvents(process.stdin); + + const inputSource = pipe( + make((observer) => { + if (isTTY) output.write(cmd(cmd.UnsetPrivateMode, PrivateMode.ShowCursor)); + if (process.stdin.isTTY) process.stdin.setRawMode(true); + + let isEnding = false; + const onEnd = () => { + if (!isEnding) { + isEnding = true; + if (isTTY) + output.write( + cmd(cmd.Reset) + + cmd(cmd.ResetPrivateMode) + + cmd(cmd.SetPrivateMode, PrivateMode.ShowCursor) + ); + if (process.stdin.isTTY) process.stdin.setRawMode(false); + observer.complete(); + } + }; + + const onKeypress = (text: string | undefined, event: KeypressEvent) => { + if ( + (event.ctrl && (event.name === 'x' || event.name === 'c' || event.name === 'd')) || + event.name === 'escape' + ) { + observer.complete(); + } else if (!isEnding) { + observer.next({ ...event, text }); + } + }; + + process.stdin.on('keypress', onKeypress); + process.stdin.on('end', onEnd); + process.on('SIGKILL', onEnd); + process.on('SIGINT', onEnd); + process.on('SIGTERM', onEnd); + process.on('exit', onEnd); + + return () => { + process.stdin.off('keypress', onKeypress); + process.stdin.off('end', onEnd); + process.off('SIGKILL', onEnd); + process.off('SIGINT', onEnd); + process.off('SIGTERM', onEnd); + process.off('exit', onEnd); + onEnd(); + }; + }), + share + ); + + const cancelSource = pipe( + inputSource, + takeLast(1), + map(() => undefined) + ); + + function write() { + // eslint-disable-next-line prefer-rest-params + output.write(text.apply(arguments)); } + + const mode = (...modes: readonly (Mode | PrivateMode)[]): void => { + if (isTTY) { + const normalModes: Mode[] = []; + const privateModes: PrivateMode[] = []; + for (const mode of modes) { + if (mode === Mode.Insert || mode === Mode.AutomaticNewline) { + normalModes.push(mode); + } else { + privateModes.push(mode); + } + } + if (normalModes.length) output.write(cmd(cmd.SetMode, normalModes)); + if (privateModes.length) output.write(cmd(cmd.SetPrivateMode, privateModes)); + } + }; + + const pipeOutput = (input: Source) => + pipe(input, onPush(write), takeUntil(cancelSource), toPromise); + + const modeOff = (...modes: readonly (Mode | PrivateMode)[]): void => { + if (isTTY) { + const normalModes: Mode[] = []; + const privateModes: PrivateMode[] = []; + for (const mode of modes) { + if (mode === Mode.Insert || mode === Mode.AutomaticNewline) { + normalModes.push(mode); + } else { + privateModes.push(mode); + } + } + if (normalModes.length) output.write(cmd(cmd.UnsetMode, normalModes)); + if (privateModes.length) output.write(cmd(cmd.UnsetPrivateMode, privateModes)); + } + }; + + return { + output, + pipeTo, + inputSource, + cancelSource, + write, + pipeOutput, + mode, + modeOff, + }; } diff --git a/packages/cli-utils/src/term/write.ts b/packages/cli-utils/src/term/write.ts index 09e574ab..93b55c40 100644 --- a/packages/cli-utils/src/term/write.ts +++ b/packages/cli-utils/src/term/write.ts @@ -1,4 +1,5 @@ -import { isTTY, output } from './tty'; +const ansiRegex = /([\x1B\x9B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])/g; +export const stripAnsi = (input: string) => input.replace(ansiRegex, ''); export class CLIError extends Error { output: string; @@ -8,58 +9,30 @@ export class CLIError extends Error { } } -const ansiRegex = /([\x1B\x9B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])/g; -const stripAnsi = (input: string) => input.replace(ansiRegex, ''); - -let buffer = ''; -let frame: any; - -function flush() { - if (frame != null) clearImmediate(frame); - frame = null; - output.write(buffer); - buffer = ''; -} - -function writeRaw(input: string) { - buffer += isTTY ? input : stripAnsi(input); - if (frame == null) frame = setImmediate(flush); -} - -function log(input: readonly string[], ...args: readonly string[]): void; -function log(input: string, ...args: readonly string[]): void; -function log(): void; +function text(input: readonly string[], ...args: readonly string[]): string; +function text(...input: readonly string[]): string; -function log(input?: string | readonly string[], ...args: readonly string[]): void { - if (!input) { - flush(); - } else if (Array.isArray(input)) { +function text(input: string | readonly string[], ...args: readonly string[]): string { + let out = ''; + if (Array.isArray(input)) { let argIndex = 0; for (let index = 0; index < input.length; index++) { - writeRaw(input[index]); - if (argIndex < args.length) writeRaw(args[argIndex++]); + out += input[index]; + if (argIndex < args.length) out += args[argIndex++]; } - } else { - writeRaw(input as string); - for (const arg of args) writeRaw(arg); + } else if (typeof input === 'string') { + out += input; + for (const arg of args) out += arg; } + return out; } function error(input: readonly string[], ...args: readonly string[]): CLIError; -function error(input: string, ...args: readonly string[]): CLIError; +function error(...input: readonly string[]): CLIError; -function error(input: string | readonly string[], ...args: readonly string[]): CLIError { - let message = ''; - if (Array.isArray(input)) { - let argIndex = 0; - for (let index = 0; index < input.length; index++) { - message += input[index]; - if (argIndex < args.length) message += args[argIndex++]; - } - } else { - message = input as string; - } - return new CLIError(message); +function error(): CLIError { + // eslint-disable-next-line prefer-rest-params + return new CLIError(text.apply(arguments)); } -export { log, error }; +export { text, error }; From 40ec4858a72bee4d98ec23340ca11937be79395e Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 11 Apr 2024 17:45:56 +0100 Subject: [PATCH 04/10] Convert to const enum and remove const alias --- packages/cli-utils/LICENSE.md | 94 ++++++++++++---- packages/cli-utils/package.json | 9 +- packages/cli-utils/src/term/csi.ts | 173 ++++++++++++----------------- packages/cli-utils/src/term/tty.ts | 18 +-- pnpm-lock.yaml | 3 + 5 files changed, 159 insertions(+), 138 deletions(-) diff --git a/packages/cli-utils/LICENSE.md b/packages/cli-utils/LICENSE.md index 7dff7938..fe4e2de2 100644 --- a/packages/cli-utils/LICENSE.md +++ b/packages/cli-utils/LICENSE.md @@ -44,6 +44,30 @@ 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. +## @0no-co/graphql.web + +MIT License + +Copyright (c) 0no.co + +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. + ## json5 MIT License @@ -70,11 +94,11 @@ SOFTWARE. [others]: https://github.com/json5/json5/contributors -## semiver +## @clack/prompts MIT License -Copyright (c) Luke Edwards (lukeed.com) +Copyright (c) Nate Moore 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: @@ -82,11 +106,13 @@ The above copyright notice and this permission notice shall be included in all c 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. -## @clack/prompts +--- + +`ansi-regex` is adapted from https://github.com/chalk/ansi-regex MIT License -Copyright (c) Nate Moore +Copyright (c) Sindre Sorhus (https://sindresorhus.com) 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: @@ -94,13 +120,11 @@ The above copyright notice and this permission notice shall be included in all c 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. ---- - -`ansi-regex` is adapted from https://github.com/chalk/ansi-regex +## semiver MIT License -Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Luke Edwards (lukeed.com) 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: @@ -250,23 +274,29 @@ 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. -## which +## wonka -The ISC License +MIT License -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 0no.co -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +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 SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +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. ## shebang-command @@ -292,7 +322,7 @@ 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. -## isexe +## which The ISC License @@ -320,4 +350,22 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of 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. \ No newline at end of file +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. + +## isexe + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index 29dce3bb..e07cf572 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -48,13 +48,14 @@ "sade": "^1.8.1", "semiver": "^1.1.0", "type-fest": "^4.10.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "wonka": "^6.3.4" }, "dependencies": { - "@gql.tada/internal": "workspace:*", "@0no-co/graphqlsp": "^1.9.1", - "ts-morph": "~22.0.0", - "graphql": "^16.8.1" + "@gql.tada/internal": "workspace:*", + "graphql": "^16.8.1", + "ts-morph": "~22.0.0" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/packages/cli-utils/src/term/csi.ts b/packages/cli-utils/src/term/csi.ts index 49ebde6f..e31a8422 100644 --- a/packages/cli-utils/src/term/csi.ts +++ b/packages/cli-utils/src/term/csi.ts @@ -5,7 +5,7 @@ export function _setColor(color: boolean) { } // See: http://xtermjs.org/docs/api/vtfeatures/#csi -enum Command { +const enum CSI { InsertChars = '@', ScrollLeft = 'SP@', Up = 'A', @@ -47,35 +47,35 @@ enum Command { DeleteColumns = "'~", } -enum TabClear { +const enum TabClear { Current = 0, All = 3, } -enum Erase { +const enum Erase { Forward = 0, Backward = 1, All = 2, Scrollback = 3, } -enum EraseLine { +const enum EraseLine { Forward = 0, Backward = 1, All = 2, } -enum Protect { +const enum Protect { Insert = 4, AutomaticNewline = 20, } -enum Mode { +const enum Mode { Insert = 4, AutomaticNewline = 20, } -enum PrivateMode { +const enum PrivateMode { AppCursorKeys = 1, USASCII = 2, Column132 = 3, @@ -100,7 +100,7 @@ enum PrivateMode { BracketedPaste = 2004, } -enum Cursor { +const enum Cursor { Empty = 0, Block = 1, BlinkBlock = 2, @@ -110,7 +110,7 @@ enum Cursor { BlinkBar = 6, } -enum Style { +const enum Style { Reset = 0, Bold = 1, Faint = 2, @@ -174,38 +174,34 @@ enum Style { DashedUnderline = '4:5', } -type CSI = '\x1B['; -const CSI = '\x1B['; +type escapeCSI = '\x1B['; +const escapeCSI = '\x1B['; -type CommandNoParam = - | Command.Reset - | Command.SaveCursor - | Command.RestoreCursor - | Command.ResetPrivateMode; +type CommandNoParam = CSI.Reset | CSI.SaveCursor | CSI.RestoreCursor | CSI.ResetPrivateMode; type CommandSingleParam = - | Command.ScrollLeft - | Command.ScrollRight - | Command.ScrollUp - | Command.ScrollDown - | Command.Up - | Command.Down - | Command.Backward - | Command.Forward - | Command.PrevLine - | Command.NextLine - | Command.DownLine - | Command.ToColumn - | Command.ToRow - | Command.Tab - | Command.TabBackwards - | Command.InsertChars - | Command.InsertLines - | Command.InsertColumns - | Command.DeleteChars - | Command.DeleteLines - | Command.DeleteColumns - | Command.RepeatChar; + | CSI.ScrollLeft + | CSI.ScrollRight + | CSI.ScrollUp + | CSI.ScrollDown + | CSI.Up + | CSI.Down + | CSI.Backward + | CSI.Forward + | CSI.PrevLine + | CSI.NextLine + | CSI.DownLine + | CSI.ToColumn + | CSI.ToRow + | CSI.Tab + | CSI.TabBackwards + | CSI.InsertChars + | CSI.InsertLines + | CSI.InsertColumns + | CSI.DeleteChars + | CSI.DeleteLines + | CSI.DeleteColumns + | CSI.RepeatChar; type CommandParam = | number @@ -221,60 +217,57 @@ type CommandParam = | readonly PrivateMode[] | readonly Style[]; -function _cmd(code: CommandNoParam): `${CSI}${CommandNoParam}`; -function _cmd(code: CommandSingleParam, count?: number): `${CSI}${number}${CommandSingleParam}`; -function _cmd(code: Command.TabClear, mode?: TabClear): `${CSI}${TabClear}${Command.TabClear}`; -function _cmd(code: Command.Erase, mode?: Erase): `${CSI}${Erase}${Command.Erase}`; -function _cmd(code: Command.EraseLine, mode?: EraseLine): `${CSI}${EraseLine}${Command.EraseLine}`; -function _cmd(code: Command.Protect, mode?: Protect): `${CSI}${Protect}${Command.Protect}`; -function _cmd(code: Command.Cursor, style: Cursor): `${CSI}${Cursor}${Command.Cursor}`; - -function _cmd( - code: Command.ToPosition, +function cmd(code: CommandNoParam): `${escapeCSI}${CommandNoParam}`; +function cmd( + code: CommandSingleParam, + count?: number +): `${escapeCSI}${number}${CommandSingleParam}`; +function cmd(code: CSI.TabClear, mode?: TabClear): `${escapeCSI}${TabClear}${CSI.TabClear}`; +function cmd(code: CSI.Erase, mode?: Erase): `${escapeCSI}${Erase}${CSI.Erase}`; +function cmd(code: CSI.EraseLine, mode?: EraseLine): `${escapeCSI}${EraseLine}${CSI.EraseLine}`; +function cmd(code: CSI.Protect, mode?: Protect): `${escapeCSI}${Protect}${CSI.Protect}`; +function cmd(code: CSI.Cursor, style: Cursor): `${escapeCSI}${Cursor}${CSI.Cursor}`; + +function cmd( + code: CSI.ToPosition, row: number, column: number -): `${CSI}${number};${number}${Command.ToPosition}`; +): `${escapeCSI}${number};${number}${CSI.ToPosition}`; -function _cmd( - code: Command.SetMargin, +function cmd( + code: CSI.SetMargin, top: number, bottom: number -): `${CSI}${number};${number}${Command.SetMargin}`; +): `${escapeCSI}${number};${number}${CSI.SetMargin}`; -function _cmd( - code: Command.SetMode, - modes: Mode | readonly Mode[] -): `${CSI}${Mode}${Command.SetMode}`; +function cmd(code: CSI.SetMode, modes: Mode | readonly Mode[]): `${escapeCSI}${Mode}${CSI.SetMode}`; -function _cmd( - code: Command.SetPrivateMode, +function cmd( + code: CSI.SetPrivateMode, modes: PrivateMode | readonly PrivateMode[] -): `${CSI}?${PrivateMode}${Command.SetMode}`; +): `${escapeCSI}?${PrivateMode}${CSI.SetMode}`; -function _cmd( - code: Command.UnsetMode, +function cmd( + code: CSI.UnsetMode, modes: Mode | readonly Mode[] -): `${CSI}${Mode}${Command.UnsetMode}`; +): `${escapeCSI}${Mode}${CSI.UnsetMode}`; -function _cmd( - code: Command.UnsetPrivateMode, +function cmd( + code: CSI.UnsetPrivateMode, modes: PrivateMode | readonly PrivateMode[] -): `${CSI}?${PrivateMode}${Command.UnsetMode}`; +): `${escapeCSI}?${PrivateMode}${CSI.UnsetMode}`; -function _cmd( - code: Command.Style, - styles: Style | readonly Style[] -): `${CSI}${Style}${Command.Style}`; +function cmd(code: CSI.Style, styles: Style | readonly Style[]): `${escapeCSI}${Style}${CSI.Style}`; -function _cmd(code: Command, a?: CommandParam, b?: number): cmdCode { - if (!hasColor && code === Command.Style) return ''; - let out = CSI; - if (code === Command.SetPrivateMode) { +function cmd(code: CSI, a?: CommandParam, b?: number): cmdCode { + if (!hasColor && code === CSI.Style) return ''; + let out = escapeCSI; + if (code === CSI.SetPrivateMode) { out += '?'; - code = Command.SetMode; - } else if (code === Command.UnsetPrivateMode) { + code = CSI.SetMode; + } else if (code === CSI.UnsetPrivateMode) { out += '?'; - code = Command.UnsetMode; + code = CSI.UnsetMode; } if (Array.isArray(a)) { out += a.join(';'); @@ -286,30 +279,6 @@ function _cmd(code: Command, a?: CommandParam, b?: number): cmdCode { return out as cmdCode; } -interface cmdCodes { - tabClear: typeof TabClear; - erase: typeof Erase; - eraseLine: typeof EraseLine; - protect: typeof Protect; - cursor: typeof Cursor; - mode: typeof Mode; - privateMode: typeof PrivateMode; - style: typeof Style; -} - -type cmd = typeof _cmd & cmdCodes & typeof Command; - -export const cmd: cmd = Object.assign(_cmd as cmd, Command, { - tabClear: TabClear, - erase: Erase, - eraseLine: EraseLine, - protect: Protect, - cursor: Cursor, - mode: Mode, - privateMode: PrivateMode, - style: Style, -}); - -export type cmdCode = `${CSI}${string}${Command}` | ''; +export type cmdCode = `${escapeCSI}${string}${CSI}` | ''; -export { Command, TabClear, Erase, EraseLine, Protect, Mode, PrivateMode, Cursor, Style }; +export { cmd, CSI, TabClear, Erase, EraseLine, Protect, Mode, PrivateMode, Cursor, Style }; diff --git a/packages/cli-utils/src/term/tty.ts b/packages/cli-utils/src/term/tty.ts index b0f1fa20..5b52e0b5 100644 --- a/packages/cli-utils/src/term/tty.ts +++ b/packages/cli-utils/src/term/tty.ts @@ -4,7 +4,7 @@ import { emitKeypressEvents } from 'node:readline'; import type { Source } from 'wonka'; import { make, pipe, share, takeLast, takeUntil, onPush, toPromise, map } from 'wonka'; -import { cmd, _setColor, Mode, PrivateMode } from './csi'; +import { cmd, _setColor, CSI, Mode, PrivateMode } from './csi'; import { text } from './write'; export interface KeypressEvent { @@ -50,7 +50,7 @@ export function initTTY(): TTY { const inputSource = pipe( make((observer) => { - if (isTTY) output.write(cmd(cmd.UnsetPrivateMode, PrivateMode.ShowCursor)); + if (isTTY) output.write(cmd(CSI.UnsetPrivateMode, PrivateMode.ShowCursor)); if (process.stdin.isTTY) process.stdin.setRawMode(true); let isEnding = false; @@ -59,9 +59,9 @@ export function initTTY(): TTY { isEnding = true; if (isTTY) output.write( - cmd(cmd.Reset) + - cmd(cmd.ResetPrivateMode) + - cmd(cmd.SetPrivateMode, PrivateMode.ShowCursor) + cmd(CSI.Reset) + + cmd(CSI.ResetPrivateMode) + + cmd(CSI.SetPrivateMode, PrivateMode.ShowCursor) ); if (process.stdin.isTTY) process.stdin.setRawMode(false); observer.complete(); @@ -121,8 +121,8 @@ export function initTTY(): TTY { privateModes.push(mode); } } - if (normalModes.length) output.write(cmd(cmd.SetMode, normalModes)); - if (privateModes.length) output.write(cmd(cmd.SetPrivateMode, privateModes)); + if (normalModes.length) output.write(cmd(CSI.SetMode, normalModes)); + if (privateModes.length) output.write(cmd(CSI.SetPrivateMode, privateModes)); } }; @@ -140,8 +140,8 @@ export function initTTY(): TTY { privateModes.push(mode); } } - if (normalModes.length) output.write(cmd(cmd.UnsetMode, normalModes)); - if (privateModes.length) output.write(cmd(cmd.UnsetPrivateMode, privateModes)); + if (normalModes.length) output.write(cmd(CSI.UnsetMode, normalModes)); + if (privateModes.length) output.write(cmd(CSI.UnsetPrivateMode, privateModes)); } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c322184b..85e0516b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.4.2 + wonka: + specifier: ^6.3.4 + version: 6.3.4 packages/internal: dependencies: From 8f2aeef35cc95f25626923a028ddce66f44a6b89 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 11 Apr 2024 22:12:55 +0100 Subject: [PATCH 05/10] Add stdin handling --- packages/cli-utils/src/term/tty.ts | 149 +++++++++++++++------------ packages/cli-utils/src/term/write.ts | 5 +- 2 files changed, 84 insertions(+), 70 deletions(-) diff --git a/packages/cli-utils/src/term/tty.ts b/packages/cli-utils/src/term/tty.ts index 5b52e0b5..fba012eb 100644 --- a/packages/cli-utils/src/term/tty.ts +++ b/packages/cli-utils/src/term/tty.ts @@ -1,14 +1,26 @@ -import type { WriteStream } from 'node:tty'; -import { emitKeypressEvents } from 'node:readline'; +import { + fromValue, + make, + concat, + pipe, + filter, + share, + takeUntil, + toPromise, + onPush, + onStart, + onEnd, +} from 'wonka'; import type { Source } from 'wonka'; -import { make, pipe, share, takeLast, takeUntil, onPush, toPromise, map } from 'wonka'; +import type { WriteStream, ReadStream } from 'node:tty'; +import { emitKeypressEvents } from 'node:readline'; import { cmd, _setColor, CSI, Mode, PrivateMode } from './csi'; import { text } from './write'; export interface KeypressEvent { - text?: string; + data?: string; sequence: string; name: string; ctrl: boolean; @@ -20,7 +32,7 @@ export interface TTY { output: WriteStream; pipeTo: WriteStream | null; inputSource: Source; - cancelSource: Source; + cancelSource: Source; write(input: readonly string[], ...args: readonly string[]): void; write(...input: readonly string[]): void; @@ -31,6 +43,37 @@ export interface TTY { modeOff(...modes: readonly (Mode | PrivateMode)[]): void; } +function fromReadStream(stream: ReadStream): Source { + return make((observer) => { + function onKeypress(data: string | undefined, event: KeypressEvent) { + switch (event.name) { + case 'c': + case 'd': + case 'x': + if (event.ctrl) cleanup(); + case 'escape': + cleanup(); + default: + observer.next({ ...event, data }); + } + } + + function cleanup() { + observer.complete(); + stream.removeListener('keypress', onKeypress); + stream.setRawMode(false); + stream.unref(); + } + + emitKeypressEvents(stream); + stream.setRawMode(true); + stream.setEncoding('utf8'); + stream.resume(); + stream.addListener('keypress', onKeypress); + return cleanup; + }); +} + export function initTTY(): TTY { let isTTY = process.env.TERM !== 'dumb' && !process.env.CI; let pipeTo: WriteStream | null = null; @@ -44,72 +87,47 @@ export function initTTY(): TTY { const hasColorArg = process.argv.includes('--color'); const hasColorEnv = 'FORCE_COLOR' in process.env || (!process.env.NO_COLOR && !process.env.CI); - _setColor((isTTY && hasColorEnv) || hasColorArg); - emitKeypressEvents(process.stdin); - - const inputSource = pipe( - make((observer) => { - if (isTTY) output.write(cmd(CSI.UnsetPrivateMode, PrivateMode.ShowCursor)); - if (process.stdin.isTTY) process.stdin.setRawMode(true); - - let isEnding = false; - const onEnd = () => { - if (!isEnding) { - isEnding = true; - if (isTTY) - output.write( - cmd(CSI.Reset) + - cmd(CSI.ResetPrivateMode) + - cmd(CSI.SetPrivateMode, PrivateMode.ShowCursor) - ); - if (process.stdin.isTTY) process.stdin.setRawMode(false); - observer.complete(); - } - }; - - const onKeypress = (text: string | undefined, event: KeypressEvent) => { - if ( - (event.ctrl && (event.name === 'x' || event.name === 'c' || event.name === 'd')) || - event.name === 'escape' - ) { - observer.complete(); - } else if (!isEnding) { - observer.next({ ...event, text }); - } - }; - - process.stdin.on('keypress', onKeypress); - process.stdin.on('end', onEnd); - process.on('SIGKILL', onEnd); - process.on('SIGINT', onEnd); - process.on('SIGTERM', onEnd); - process.on('exit', onEnd); - - return () => { - process.stdin.off('keypress', onKeypress); - process.stdin.off('end', onEnd); - process.off('SIGKILL', onEnd); - process.off('SIGINT', onEnd); - process.off('SIGTERM', onEnd); - process.off('exit', onEnd); - onEnd(); - }; - }), - share - ); + + function _start() { + _setColor((isTTY && hasColorEnv) || hasColorArg); + if (isTTY) { + output.write(cmd(CSI.UnsetPrivateMode, PrivateMode.ShowCursor)); + } + } + + function _end() { + if (isTTY) { + output.write( + cmd(CSI.Reset) + + cmd(CSI.ResetPrivateMode) + + cmd(CSI.SetPrivateMode, PrivateMode.ShowCursor) + + '\n' + ); + } + } + + const inputSource = pipe(fromReadStream(process.stdin), onStart(_start), onEnd(_end), share); const cancelSource = pipe( - inputSource, - takeLast(1), - map(() => undefined) + concat([ + pipe( + inputSource, + filter(() => false) + ), + fromValue(null), + ]), + share ); - function write() { - // eslint-disable-next-line prefer-rest-params - output.write(text.apply(arguments)); + function write(...input: any[]) { + output.write(text(...input)); } + const pipeOutput = (input: Source) => { + return pipe(input, onPush(write), takeUntil(cancelSource), toPromise); + }; + const mode = (...modes: readonly (Mode | PrivateMode)[]): void => { if (isTTY) { const normalModes: Mode[] = []; @@ -126,9 +144,6 @@ export function initTTY(): TTY { } }; - const pipeOutput = (input: Source) => - pipe(input, onPush(write), takeUntil(cancelSource), toPromise); - const modeOff = (...modes: readonly (Mode | PrivateMode)[]): void => { if (isTTY) { const normalModes: Mode[] = []; diff --git a/packages/cli-utils/src/term/write.ts b/packages/cli-utils/src/term/write.ts index 93b55c40..e3df362c 100644 --- a/packages/cli-utils/src/term/write.ts +++ b/packages/cli-utils/src/term/write.ts @@ -30,9 +30,8 @@ function text(input: string | readonly string[], ...args: readonly string[]): st function error(input: readonly string[], ...args: readonly string[]): CLIError; function error(...input: readonly string[]): CLIError; -function error(): CLIError { - // eslint-disable-next-line prefer-rest-params - return new CLIError(text.apply(arguments)); +function error(...input: any[]): CLIError { + return new CLIError(text(...input)); } export { text, error }; From 946e7f6c1625135b2e22dc4ce3f9df10dd307353 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 11 Apr 2024 22:13:16 +0100 Subject: [PATCH 06/10] Update check logger for new interface --- packages/cli-utils/src/loggers/check.ts | 287 +++++++++++++----------- 1 file changed, 157 insertions(+), 130 deletions(-) diff --git a/packages/cli-utils/src/loggers/check.ts b/packages/cli-utils/src/loggers/check.ts index 2ad54a6a..e1ab752c 100644 --- a/packages/cli-utils/src/loggers/check.ts +++ b/packages/cli-utils/src/loggers/check.ts @@ -1,87 +1,151 @@ -import { CLIError, Chars, Icons, HeavyBox, Style, circleSpinner, init, cmd, log } from '../term'; +import { + pipe, + interval, + fromValue, + fromAsyncIterable, + switchMap, + concat, + delay, + map, + scan, +} from 'wonka'; + +import * as t from '../term'; export { error } from '../term'; -function delay(ms = 750) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} +const clearLine = () => t.cmd(t.CSI.DeleteLines, 1) + t.cmd(t.CSI.ToColumn, 1); function printTitle(title: string) { - log([ - cmd(cmd.Style, Style.BrightBlack), - HeavyBox.TopLeft, - Chars.Space, - cmd(cmd.Style, [Style.Magenta, Style.Invert]), - Chars.Space, - title.trim(), - Chars.Space, - cmd(cmd.Style, Style.Reset), - Chars.Newline, - ]); + return fromValue( + t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + t.HeavyBox.TopLeft, + ' ', + t.cmd(t.CSI.Style, [t.Style.Magenta, t.Style.Invert]), + ` ${title.trim()} `, + t.cmd(t.CSI.Style, t.Style.Reset), + '\n', + ]) + ); } function printLine(text?: string) { - log([cmd(cmd.Style, Style.BrightBlack), HeavyBox.Vertical, Chars.Space]); - if (text) log(text); - log([cmd(cmd.Style, Style.Reset), Chars.Newline]); + let out = t.text([t.cmd(t.CSI.Style, t.Style.BrightBlack), t.HeavyBox.Vertical]); + if (text) out += t.text` ${text}\n${t.HeavyBox.Vertical}`; + out += t.text([t.cmd(t.CSI.Style, t.Style.Reset), '\n']); + return fromValue(out); } function printCompletedTask(description: string, isLast = false) { - log([ - cmd(cmd.Style, Style.BrightBlack), - isLast ? HeavyBox.BottomLeft : HeavyBox.VerticalRight, - Chars.Space, - cmd(cmd.Style, Style.Green), - Icons.TickSwoosh, - Chars.Space, - cmd(cmd.Style, Style.Foreground), - description, - cmd(cmd.Style, Style.Reset), - Chars.Newline, - ]); + return fromValue( + t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + isLast ? t.HeavyBox.BottomLeft : t.HeavyBox.VerticalRight, + ' ', + t.cmd(t.CSI.Style, t.Style.Green), + t.Icons.TickSwoosh, + ' ', + t.cmd(t.CSI.Style, t.Style.Foreground), + description, + t.cmd(t.CSI.Style, t.Style.Reset), + '\n', + ]) + ); } function printFailedTask(description: string) { - log([ - cmd(cmd.Style, Style.BrightBlack), - HeavyBox.BottomLeft, - Chars.Space, - cmd(cmd.Style, Style.BrightRed), - Icons.CrossSwoosh, - Chars.Space, - cmd(cmd.Style, Style.Foreground), - description, - cmd(cmd.Style, Style.Reset), - Chars.Newline, - ]); + return fromValue( + t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + t.HeavyBox.BottomLeft, + ' ', + t.cmd(t.CSI.Style, t.Style.BrightRed), + t.Icons.CrossSwoosh, + ' ', + t.cmd(t.CSI.Style, t.Style.Foreground), + description, + t.cmd(t.CSI.Style, t.Style.Reset), + '\n', + ]) + ); } function printRunningTask(description: string) { - let state = 0; - function print() { - if (state) { - log([cmd(cmd.DeleteLines, 1), cmd(cmd.ToColumn, 1)]); - } - log([ - cmd(cmd.Style, Style.Magenta), - circleSpinner[state++ % circleSpinner.length], - Chars.Space, - cmd(cmd.Style, Style.Foreground), - description, - cmd(cmd.Style, Style.Reset), - ]); + return pipe( + interval(150), + map((state) => + t.text([ + clearLine(), + t.cmd(t.CSI.Style, t.Style.Magenta), + t.circleSpinner[state % t.circleSpinner.length], + ' ', + t.cmd(t.CSI.Style, t.Style.Foreground), + description, + t.cmd(t.CSI.Style, t.Style.Reset), + ]) + ) + ); +} + +function printSuccess() { + return fromValue( + t.text([ + t.Chars.Newline, + t.cmd(t.CSI.Style, [t.Style.Green, t.Style.Invert]), + ' Done ', + t.cmd(t.CSI.Style, t.Style.NoInvert), + t.Chars.Space, + 'You are all set and ready to go.', + ]) + ); +} + +function printError(error: Error) { + if (error instanceof t.CLIError) { + return fromValue( + t.text([ + t.Chars.Newline, + t.cmd(t.CSI.Style, [t.Style.BrightRed, t.Style.Invert]), + ' Error ', + t.cmd(t.CSI.Style, t.Style.NoInvert), + t.Chars.Newline, + `${error.output}`, + ]) + ); + } else { + return fromValue( + t.text([ + t.Chars.Newline, + t.cmd(t.CSI.Style, [t.Style.BrightRed, t.Style.Invert]), + ' Unexpected Error ', + t.cmd(t.CSI.Style, t.Style.NoInvert), + t.Chars.Newline, + `${error}`, + ]) + ); } +} - const intervalId = setInterval(print, 150); - return () => { - clearInterval(intervalId); - log([cmd(cmd.DeleteLines, 1), cmd(cmd.ToColumn, 1)]); - }; +async function* printTask(task: () => AsyncIterable): AsyncIterable { + try { + for await (const signal of task()) { + yield signal; + if (signal.kind && signal.kind !== 'update') return; + } + yield { kind: 'complete' }; + } catch (error: any) { + yield { kind: 'error', error }; + } } -interface PrintSignal { - text: string; +type PrintSignal = + | { kind: 'complete' } + | { kind: 'error'; error: Error } + | { kind?: 'update'; text: string }; + +interface PrintState { + prev: { kind?: 'update'; text: string } | null; + task: PrintSignal; } interface PrintConfig { @@ -91,71 +155,34 @@ interface PrintConfig { } export async function print(config: PrintConfig) { - init(); - - printTitle(config.title); - if (config.description) printLine(config.description); - - printLine(); - - let current: PrintSignal | undefined; - let cancel: (() => void) | undefined; - try { - for await (const signal of config.task()) { - if (cancel) { - cancel(); - } - if (current) { - printCompletedTask(current.text); - printLine(); - } - cancel = printRunningTask((current = signal).text); - await delay(); - } - - if (cancel) { - cancel(); - } - if (current) { - printCompletedTask(current.text, true); - printLine(); - } - - log([ - Chars.Newline, - cmd(cmd.Style, [cmd.style.Green, cmd.style.Invert]), - ' Done ', - cmd(cmd.Style, cmd.style.NoInvert), - Chars.Space, - 'You are all set and ready to go.', - ]); - } catch (error: any) { - if (cancel) { - cancel(); - } - - if (current) { - printFailedTask(current.text); - } + const tty = t.initTTY(); + + const print = concat([ + printTitle(config.title), + printLine(config.description), + pipe( + fromAsyncIterable(printTask(config.task)), + delay(700), + scan((state: PrintState, task) => ({ task, prev: state.task as PrintState['prev'] }), { + prev: null, + task: null, + } as any), + switchMap(({ prev, task }) => { + const sources = [fromValue(clearLine())]; + if (task.kind === 'complete') { + if (prev) sources.push(printCompletedTask(prev.text, true)); + sources.push(printSuccess()); + } else if (task.kind === 'error') { + if (prev) sources.push(printFailedTask(prev.text)); + sources.push(printError(task.error)); + } else { + if (prev) sources.push(printCompletedTask(prev.text)); + sources.push(printRunningTask(task.text)); + } + return concat(sources); + }) + ), + ]); - if (error instanceof CLIError) { - log([ - Chars.Newline, - cmd(cmd.Style, [cmd.style.BrightRed, cmd.style.Invert]), - ' Error ', - cmd(cmd.Style, cmd.style.NoInvert), - Chars.Newline, - `${error.output}`, - ]); - } else { - log([ - Chars.Newline, - cmd(cmd.Style, [cmd.style.BrightRed, cmd.style.Invert]), - ' Unexpected Error ', - cmd(cmd.Style, cmd.style.NoInvert), - Chars.Newline, - `${error}`, - ]); - } - } + return await tty.pipeOutput(print); } From c144f213b1f926f8e66854cda76c64dbc4077a19 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 12 Apr 2024 01:02:47 +0100 Subject: [PATCH 07/10] Refactor utilities for composing output streams --- packages/cli-utils/src/commands/doctor.ts | 93 ++++++--- packages/cli-utils/src/loggers/check.ts | 232 +++++++--------------- packages/cli-utils/src/term/csi.ts | 4 +- packages/cli-utils/src/term/index.ts | 3 +- packages/cli-utils/src/term/tty.ts | 21 +- packages/cli-utils/src/term/write.ts | 68 ++++++- 6 files changed, 217 insertions(+), 204 deletions(-) diff --git a/packages/cli-utils/src/commands/doctor.ts b/packages/cli-utils/src/commands/doctor.ts index 13bd62ff..c6698903 100644 --- a/packages/cli-utils/src/commands/doctor.ts +++ b/packages/cli-utils/src/commands/doctor.ts @@ -7,7 +7,21 @@ import { resolveTypeScriptRootDir } from '@gql.tada/internal'; import { existsSync } from 'node:fs'; import { getGraphQLSPConfig } from '../lsp'; -import { print, error } from '../loggers/check'; +import { initTTY } from '../term'; +import * as logger from '../loggers/check'; + +const delay = (ms = 700) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +const enum Messages { + TITLE = 'Doctor', + DESCRIPTION = 'Detects problems with your setup', + CHECK_TS_VERSION = 'Checking TypeScript version', + CHECK_DEPENDENCIES = 'Checking installed dependencies', + CHECK_TSCONFIG = 'Checking tsconfig.json', +} const MINIMUM_VERSIONS = { typescript: '4.1.0', @@ -15,8 +29,14 @@ const MINIMUM_VERSIONS = { lsp: '1.0.0', }; -async function* task() { - yield { text: 'Checking TypeScript version' }; +export async function executeTadaDoctor() { + await initTTY().start(run()); +} + +async function* run() { + yield logger.title(Messages.TITLE, Messages.DESCRIPTION); + yield logger.runningTask(Messages.CHECK_TS_VERSION); + await delay(); // Check TypeScript version const cwd = process.cwd(); @@ -30,7 +50,8 @@ async function* task() { const file = path.resolve(packageJsonPath); packageJsonContents = JSON.parse(await fs.readFile(file, 'utf-8')); } catch (_error) { - throw error( + yield logger.failedTask(Messages.CHECK_TS_VERSION); + throw logger.errorMessage( 'Failed to read package.json in current working directory\n' + 'Try running the doctor command in your workspace folder.' ); @@ -43,35 +64,45 @@ async function* task() { const typeScriptVersion = deps.find((x) => x[0] === 'typescript'); if (!typeScriptVersion) { - throw error('Failed to find a "typescript" installation, try installing one.'); + yield logger.failedTask(Messages.CHECK_TS_VERSION); + throw logger.errorMessage('Failed to find a "typescript" installation, try installing one.'); } else if (semiver(typeScriptVersion[1], MINIMUM_VERSIONS.typescript) === -1) { // TypeScript version lower than v4.1 which is when they introduced template lits - throw error( + yield logger.failedTask(Messages.CHECK_TS_VERSION); + throw logger.errorMessage( `Found an outdated "TypeScript" version.\ngql.tada requires at least ${MINIMUM_VERSIONS.typescript}.` ); } - yield { text: 'Checking installed dependencies' }; + yield logger.completedTask(Messages.CHECK_TS_VERSION); + yield logger.runningTask(Messages.CHECK_DEPENDENCIES); const gqlspVersion = deps.find((x) => x[0] === "'@0no-co/graphqlsp'"); if (!gqlspVersion) { - throw error('Failed to find a "@0no-co/graphqlsp" installation, try installing one.'); + yield logger.failedTask(Messages.CHECK_DEPENDENCIES); + throw logger.errorMessage( + 'Failed to find a "@0no-co/graphqlsp" installation, try installing one.' + ); } else if (semiver(gqlspVersion[1], MINIMUM_VERSIONS.lsp) === -1) { - throw error( + yield logger.failedTask(Messages.CHECK_DEPENDENCIES); + throw logger.errorMessage( `Found an outdated "@0no-co/graphqlsp" version, gql.tada requires at least ${MINIMUM_VERSIONS.lsp}.` ); } const gqlTadaVersion = deps.find((x) => x[0] === "'gql.tada'"); if (!gqlTadaVersion) { - throw error('Failed to find a "gql.tada" installation, try installing one.'); + yield logger.failedTask(Messages.CHECK_DEPENDENCIES); + throw logger.errorMessage('Failed to find a "gql.tada" installation, try installing one.'); } else if (semiver(gqlTadaVersion[1], '1.0.0') === -1) { - throw error( + yield logger.failedTask(Messages.CHECK_DEPENDENCIES); + throw logger.errorMessage( `Found an outdated "gql.tada" version, gql.tada requires at least ${MINIMUM_VERSIONS.tada}.` ); } - yield { text: 'Checking tsconfig.json' }; + yield logger.completedTask(Messages.CHECK_DEPENDENCIES); + yield logger.runningTask(Messages.CHECK_TSCONFIG); const tsconfigpath = path.resolve(cwd, 'tsconfig.json'); const root = (await resolveTypeScriptRootDir(tsconfigpath)) || cwd; @@ -81,7 +112,8 @@ async function* task() { const file = path.resolve(root, 'tsconfig.json'); tsconfigContents = await fs.readFile(file, 'utf-8'); } catch (_error) { - throw error( + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( 'Failed to read tsconfig.json in current working directory, try adding a "tsconfig.json".' ); } @@ -89,24 +121,32 @@ async function* task() { let tsConfig: TsConfigJson; try { tsConfig = parse(tsconfigContents) as TsConfigJson; - } catch (_error: any) { - throw error(['Unable to parse tsconfig.json in current working directory.\n', `${_error}`]); + } catch (error) { + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `Unable to parse tsconfig.json in current working directory.\n${error}` + ); } // Check GraphQLSP version, later on we can check if a ts version is > 5.5.0 to use gql.tada/lsp instead of // the LSP package. const config = getGraphQLSPConfig(tsConfig); if (!config) { - throw error(`Missing a "@0no-co/graphqlsp" plugin in your tsconfig.`); + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage(`Missing a "@0no-co/graphqlsp" plugin in your tsconfig.`); } // TODO: this is optional I guess with the CLI being there and all if (!config.tadaOutputLocation) { - throw error(`Missing a "tadaOutputLocation" setting in your GraphQLSP configuration.`); + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `Missing a "tadaOutputLocation" setting in your GraphQLSP configuration.` + ); } if (!config.schema) { - throw error(`Missing a "schema" setting in your GraphQLSP configuration.`); + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage(`Missing a "schema" setting in your GraphQLSP configuration.`); } else { const isFile = typeof config.schema === 'string' && @@ -114,24 +154,23 @@ async function* task() { if (isFile) { const resolvedFile = path.resolve(root, config.schema as string); if (!existsSync(resolvedFile)) { - throw error(`The schema setting does not point at an existing file "${resolvedFile}"`); + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `The schema setting does not point at an existing file "${resolvedFile}"` + ); } } else { try { typeof config.schema === 'string' ? new URL(config.schema) : new URL(config.schema.url); } catch (e) { - throw error( + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( `The schema setting does not point at a valid URL: "${JSON.stringify(config.schema)}"` ); } } } -} -export async function executeTadaDoctor() { - await print({ - title: 'doctor', - description: 'Detecting problems in your setup', - task, - }); + yield logger.completedTask(Messages.CHECK_DEPENDENCIES); + yield logger.success(); } diff --git a/packages/cli-utils/src/loggers/check.ts b/packages/cli-utils/src/loggers/check.ts index e1ab752c..90c5441f 100644 --- a/packages/cli-utils/src/loggers/check.ts +++ b/packages/cli-utils/src/loggers/check.ts @@ -1,188 +1,94 @@ -import { - pipe, - interval, - fromValue, - fromAsyncIterable, - switchMap, - concat, - delay, - map, - scan, -} from 'wonka'; +import { pipe, interval, map } from 'wonka'; import * as t from '../term'; -export { error } from '../term'; -const clearLine = () => t.cmd(t.CSI.DeleteLines, 1) + t.cmd(t.CSI.ToColumn, 1); - -function printTitle(title: string) { - return fromValue( - t.text([ - t.cmd(t.CSI.Style, t.Style.BrightBlack), - t.HeavyBox.TopLeft, - ' ', - t.cmd(t.CSI.Style, [t.Style.Magenta, t.Style.Invert]), - ` ${title.trim()} `, - t.cmd(t.CSI.Style, t.Style.Reset), - '\n', - ]) - ); +export function emptyLine() { + return t.text([t.cmd(t.CSI.Style, t.Style.BrightBlack), t.HeavyBox.Vertical, '\n']); } -function printLine(text?: string) { - let out = t.text([t.cmd(t.CSI.Style, t.Style.BrightBlack), t.HeavyBox.Vertical]); - if (text) out += t.text` ${text}\n${t.HeavyBox.Vertical}`; - out += t.text([t.cmd(t.CSI.Style, t.Style.Reset), '\n']); - return fromValue(out); +export function title(title: string, description?: string) { + let out = t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + t.HeavyBox.TopLeft, + ' ', + t.cmd(t.CSI.Style, [t.Style.Magenta, t.Style.Invert]), + ` ${title.trim()} `, + t.cmd(t.CSI.Style, [t.Style.NoInvert]), + '\n', + ]); + if (description) { + out += t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + t.HeavyBox.Vertical, + ` ${description}\n`, + ]); + } + return out; } -function printCompletedTask(description: string, isLast = false) { - return fromValue( - t.text([ - t.cmd(t.CSI.Style, t.Style.BrightBlack), - isLast ? t.HeavyBox.BottomLeft : t.HeavyBox.VerticalRight, - ' ', - t.cmd(t.CSI.Style, t.Style.Green), - t.Icons.TickSwoosh, - ' ', - t.cmd(t.CSI.Style, t.Style.Foreground), - description, - t.cmd(t.CSI.Style, t.Style.Reset), - '\n', - ]) - ); +export function completedTask(description: string, isLast = false) { + return t.text([ + emptyLine(), + t.cmd(t.CSI.Style, t.Style.BrightBlack), + isLast ? t.HeavyBox.BottomLeft : t.HeavyBox.VerticalRight, + ' ', + t.cmd(t.CSI.Style, t.Style.Green), + t.Icons.TickSwoosh, + ' ', + t.cmd(t.CSI.Style, t.Style.Foreground), + description, + '\n', + ]); } -function printFailedTask(description: string) { - return fromValue( - t.text([ - t.cmd(t.CSI.Style, t.Style.BrightBlack), - t.HeavyBox.BottomLeft, - ' ', - t.cmd(t.CSI.Style, t.Style.BrightRed), - t.Icons.CrossSwoosh, - ' ', - t.cmd(t.CSI.Style, t.Style.Foreground), - description, - t.cmd(t.CSI.Style, t.Style.Reset), - '\n', - ]) - ); +export function failedTask(description: string) { + return t.text([ + emptyLine(), + t.cmd(t.CSI.Style, t.Style.BrightBlack), + t.HeavyBox.BottomLeft, + ' ', + t.cmd(t.CSI.Style, t.Style.BrightRed), + t.Icons.CrossSwoosh, + ' ', + t.cmd(t.CSI.Style, t.Style.Foreground), + description, + '\n', + ]); } -function printRunningTask(description: string) { +export function runningTask(description: string) { return pipe( interval(150), - map((state) => - t.text([ - clearLine(), + map((state) => { + return t.text([ + emptyLine(), t.cmd(t.CSI.Style, t.Style.Magenta), t.circleSpinner[state % t.circleSpinner.length], ' ', t.cmd(t.CSI.Style, t.Style.Foreground), description, - t.cmd(t.CSI.Style, t.Style.Reset), - ]) - ) + ]); + }) ); } -function printSuccess() { - return fromValue( - t.text([ - t.Chars.Newline, - t.cmd(t.CSI.Style, [t.Style.Green, t.Style.Invert]), - ' Done ', - t.cmd(t.CSI.Style, t.Style.NoInvert), - t.Chars.Space, - 'You are all set and ready to go.', - ]) - ); -} - -function printError(error: Error) { - if (error instanceof t.CLIError) { - return fromValue( - t.text([ - t.Chars.Newline, - t.cmd(t.CSI.Style, [t.Style.BrightRed, t.Style.Invert]), - ' Error ', - t.cmd(t.CSI.Style, t.Style.NoInvert), - t.Chars.Newline, - `${error.output}`, - ]) - ); - } else { - return fromValue( - t.text([ - t.Chars.Newline, - t.cmd(t.CSI.Style, [t.Style.BrightRed, t.Style.Invert]), - ' Unexpected Error ', - t.cmd(t.CSI.Style, t.Style.NoInvert), - t.Chars.Newline, - `${error}`, - ]) - ); - } -} - -async function* printTask(task: () => AsyncIterable): AsyncIterable { - try { - for await (const signal of task()) { - yield signal; - if (signal.kind && signal.kind !== 'update') return; - } - yield { kind: 'complete' }; - } catch (error: any) { - yield { kind: 'error', error }; - } -} - -type PrintSignal = - | { kind: 'complete' } - | { kind: 'error'; error: Error } - | { kind?: 'update'; text: string }; - -interface PrintState { - prev: { kind?: 'update'; text: string } | null; - task: PrintSignal; -} - -interface PrintConfig { - title: string; - description?: string; - task(): AsyncIterable; +export function success() { + return t.text([ + '\n', + t.cmd(t.CSI.Style, [t.Style.Green, t.Style.Invert]), + ' Done ', + t.cmd(t.CSI.Style, t.Style.NoInvert), + t.Chars.Space, + 'You are all set and ready to go.\n', + ]); } -export async function print(config: PrintConfig) { - const tty = t.initTTY(); - - const print = concat([ - printTitle(config.title), - printLine(config.description), - pipe( - fromAsyncIterable(printTask(config.task)), - delay(700), - scan((state: PrintState, task) => ({ task, prev: state.task as PrintState['prev'] }), { - prev: null, - task: null, - } as any), - switchMap(({ prev, task }) => { - const sources = [fromValue(clearLine())]; - if (task.kind === 'complete') { - if (prev) sources.push(printCompletedTask(prev.text, true)); - sources.push(printSuccess()); - } else if (task.kind === 'error') { - if (prev) sources.push(printFailedTask(prev.text)); - sources.push(printError(task.error)); - } else { - if (prev) sources.push(printCompletedTask(prev.text)); - sources.push(printRunningTask(task.text)); - } - return concat(sources); - }) - ), +export function errorMessage(message: string) { + return t.error([ + '\n', + t.cmd(t.CSI.Style, [t.Style.BrightRed, t.Style.Invert]), + ' Error ', + t.cmd(t.CSI.Style, t.Style.NoInvert), + `\n${message.trim()}\n`, ]); - - return await tty.pipeOutput(print); } diff --git a/packages/cli-utils/src/term/csi.ts b/packages/cli-utils/src/term/csi.ts index e31a8422..67b06219 100644 --- a/packages/cli-utils/src/term/csi.ts +++ b/packages/cli-utils/src/term/csi.ts @@ -13,8 +13,8 @@ const enum CSI { Down = 'B', Forward = 'C', Backward = 'D', - PrevLine = 'E', - NextLine = 'F', + NextLine = 'E', + PrevLine = 'F', ToColumn = 'G', ToPosition = 'H', Tab = 'I', diff --git a/packages/cli-utils/src/term/index.ts b/packages/cli-utils/src/term/index.ts index 84583337..b0f6852d 100644 --- a/packages/cli-utils/src/term/index.ts +++ b/packages/cli-utils/src/term/index.ts @@ -1,4 +1,5 @@ -export { CLIError, text, error } from './write'; +export type { ComposeInput } from './write'; +export { CLIError, text, error, compose } from './write'; export * from './csi'; export * from './symbols'; export * from './tty'; diff --git a/packages/cli-utils/src/term/tty.ts b/packages/cli-utils/src/term/tty.ts index fba012eb..a3d99aac 100644 --- a/packages/cli-utils/src/term/tty.ts +++ b/packages/cli-utils/src/term/tty.ts @@ -16,8 +16,9 @@ import type { Source } from 'wonka'; import type { WriteStream, ReadStream } from 'node:tty'; import { emitKeypressEvents } from 'node:readline'; +import type { ComposeInput } from './write'; +import { text, compose } from './write'; import { cmd, _setColor, CSI, Mode, PrivateMode } from './csi'; -import { text } from './write'; export interface KeypressEvent { data?: string; @@ -37,7 +38,7 @@ export interface TTY { write(input: readonly string[], ...args: readonly string[]): void; write(...input: readonly string[]): void; - pipeOutput(input: Source): Promise; + start(outputs: AsyncIterable): Promise; mode(...modes: readonly (Mode | PrivateMode)[]): void; modeOff(...modes: readonly (Mode | PrivateMode)[]): void; @@ -124,11 +125,11 @@ export function initTTY(): TTY { output.write(text(...input)); } - const pipeOutput = (input: Source) => { - return pipe(input, onPush(write), takeUntil(cancelSource), toPromise); - }; + function start(outputs: AsyncIterable) { + return pipe(compose(outputs), onPush(write), takeUntil(cancelSource), toPromise); + } - const mode = (...modes: readonly (Mode | PrivateMode)[]): void => { + function mode(...modes: readonly (Mode | PrivateMode)[]): void { if (isTTY) { const normalModes: Mode[] = []; const privateModes: PrivateMode[] = []; @@ -142,9 +143,9 @@ export function initTTY(): TTY { if (normalModes.length) output.write(cmd(CSI.SetMode, normalModes)); if (privateModes.length) output.write(cmd(CSI.SetPrivateMode, privateModes)); } - }; + } - const modeOff = (...modes: readonly (Mode | PrivateMode)[]): void => { + function modeOff(...modes: readonly (Mode | PrivateMode)[]): void { if (isTTY) { const normalModes: Mode[] = []; const privateModes: PrivateMode[] = []; @@ -158,7 +159,7 @@ export function initTTY(): TTY { if (normalModes.length) output.write(cmd(CSI.UnsetMode, normalModes)); if (privateModes.length) output.write(cmd(CSI.UnsetPrivateMode, privateModes)); } - }; + } return { output, @@ -166,7 +167,7 @@ export function initTTY(): TTY { inputSource, cancelSource, write, - pipeOutput, + start, mode, modeOff, }; diff --git a/packages/cli-utils/src/term/write.ts b/packages/cli-utils/src/term/write.ts index e3df362c..e22ef157 100644 --- a/packages/cli-utils/src/term/write.ts +++ b/packages/cli-utils/src/term/write.ts @@ -1,4 +1,24 @@ +import type { Source } from 'wonka'; + +import { + pipe, + fromAsyncIterable, + fromValue, + concatMap, + sample, + merge, + takeUntil, + filter, + share, + take, + scan, + map, +} from 'wonka'; + +import { cmd, CSI, Style } from './csi'; + const ansiRegex = /([\x1B\x9B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])/g; + export const stripAnsi = (input: string) => input.replace(ansiRegex, ''); export class CLIError extends Error { @@ -34,4 +54,50 @@ function error(...input: any[]): CLIError { return new CLIError(text(...input)); } -export { text, error }; +function clear(text: string) { + if (!text) return ''; + let lines = 0; + for (let index = 0; index < text.length; index++) + if (text.charCodeAt(index) === 10 /*'\n'*/) lines++; + return (lines > 0 ? cmd(CSI.PrevLine, lines) : '') + cmd(CSI.DeleteLines, lines + 1); +} + +type ComposeInput = undefined | string | Source | AsyncIterable; + +async function* convertError(outputs: AsyncIterable): AsyncIterable { + try { + yield* outputs; + } catch (error) { + yield error instanceof CLIError ? error.output : '' + error; + } +} + +function compose(outputs: AsyncIterable): Source { + const outputs$ = share(fromAsyncIterable(convertError(outputs))); + const reset = cmd(CSI.Style, [Style.Reset, Style.NoInvert]); + return pipe( + outputs$, + filter((x: T): x is Exclude => x != null), + concatMap((output) => { + if (typeof output === 'object') { + return compose(output); + } + const output$ = share(typeof output === 'string' ? fromValue(output) : output); + return pipe( + merge([ + pipe( + output$, + sample(outputs$), + map((output) => (output.endsWith('\n') ? output : '')), + take(1) + ), + pipe(output$, takeUntil(outputs$)), + ]), + scan((prev, output) => clear(prev) + output + reset, '') + ); + }) + ); +} + +export type { ComposeInput }; +export { text, error, compose, clear }; From da2063695bcf3cb1c676f619e9ec244f3592eefd Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 12 Apr 2024 02:26:04 +0100 Subject: [PATCH 08/10] Update formatting of doctor output --- packages/cli-utils/src/commands/doctor.ts | 162 ++++++++++++++++------ packages/cli-utils/src/loggers/check.ts | 34 ++++- packages/cli-utils/src/term/write.ts | 4 +- 3 files changed, 155 insertions(+), 45 deletions(-) diff --git a/packages/cli-utils/src/commands/doctor.ts b/packages/cli-utils/src/commands/doctor.ts index c6698903..3f0b3ef3 100644 --- a/packages/cli-utils/src/commands/doctor.ts +++ b/packages/cli-utils/src/commands/doctor.ts @@ -6,10 +6,11 @@ import type { TsConfigJson } from 'type-fest'; import { resolveTypeScriptRootDir } from '@gql.tada/internal'; import { existsSync } from 'node:fs'; -import { getGraphQLSPConfig } from '../lsp'; import { initTTY } from '../term'; import * as logger from '../loggers/check'; +// NOTE: Currently, most tasks in this command complete too quickly +// We slow them down to make the CLI output easier to follow along to const delay = (ms = 700) => new Promise((resolve) => { setTimeout(resolve, ms); @@ -21,6 +22,7 @@ const enum Messages { CHECK_TS_VERSION = 'Checking TypeScript version', CHECK_DEPENDENCIES = 'Checking installed dependencies', CHECK_TSCONFIG = 'Checking tsconfig.json', + CHECK_SCHEMA = 'Checking schema', } const MINIMUM_VERSIONS = { @@ -52,8 +54,8 @@ async function* run() { } catch (_error) { yield logger.failedTask(Messages.CHECK_TS_VERSION); throw logger.errorMessage( - 'Failed to read package.json in current working directory\n' + - 'Try running the doctor command in your workspace folder.' + `A ${logger.code('package.json')} file was not found in the current working directory.\n` + + logger.hint('Try running the doctor command in your workspace folder.') ); } @@ -65,112 +67,188 @@ async function* run() { const typeScriptVersion = deps.find((x) => x[0] === 'typescript'); if (!typeScriptVersion) { yield logger.failedTask(Messages.CHECK_TS_VERSION); - throw logger.errorMessage('Failed to find a "typescript" installation, try installing one.'); + throw logger.errorMessage( + `A version of ${logger.code('typescript')} was not found in your dependencies.\n` + + logger.hint(`Is ${logger.code('typescript')} installed in this package?`) + ); } else if (semiver(typeScriptVersion[1], MINIMUM_VERSIONS.typescript) === -1) { // TypeScript version lower than v4.1 which is when they introduced template lits yield logger.failedTask(Messages.CHECK_TS_VERSION); throw logger.errorMessage( - `Found an outdated "TypeScript" version.\ngql.tada requires at least ${MINIMUM_VERSIONS.typescript}.` + `The version of ${logger.code('typescript')} in your dependencies is out of date.\n` + + logger.hint( + `${logger.code('gql.tada')} requires at least ${logger.bold(MINIMUM_VERSIONS.typescript)}` + ) ); } yield logger.completedTask(Messages.CHECK_TS_VERSION); yield logger.runningTask(Messages.CHECK_DEPENDENCIES); + await delay(); - const gqlspVersion = deps.find((x) => x[0] === "'@0no-co/graphqlsp'"); + const gqlspVersion = deps.find((x) => x[0] === '@0no-co/graphqlsp'); if (!gqlspVersion) { yield logger.failedTask(Messages.CHECK_DEPENDENCIES); throw logger.errorMessage( - 'Failed to find a "@0no-co/graphqlsp" installation, try installing one.' + `A version of ${logger.code('@0no-co/graphqlsp')} was not found in your dependencies.\n` + + logger.hint(`Is ${logger.code('@0no-co/graphqlsp')} installed?`) ); } else if (semiver(gqlspVersion[1], MINIMUM_VERSIONS.lsp) === -1) { yield logger.failedTask(Messages.CHECK_DEPENDENCIES); throw logger.errorMessage( - `Found an outdated "@0no-co/graphqlsp" version, gql.tada requires at least ${MINIMUM_VERSIONS.lsp}.` + `The version of ${logger.code('@0no-co/graphqlsp')} in your dependencies is out of date.\n` + + logger.hint( + `${logger.code('gql.tada')} requires at least ${logger.bold(MINIMUM_VERSIONS.lsp)}` + ) ); } - const gqlTadaVersion = deps.find((x) => x[0] === "'gql.tada'"); + const gqlTadaVersion = deps.find((x) => x[0] === 'gql.tada'); if (!gqlTadaVersion) { yield logger.failedTask(Messages.CHECK_DEPENDENCIES); - throw logger.errorMessage('Failed to find a "gql.tada" installation, try installing one.'); + throw logger.errorMessage( + `A version of ${logger.code('gql.tada')} was not found in your dependencies.\n` + + logger.hint(`Is ${logger.code('gql.tada')} installed?`) + ); } else if (semiver(gqlTadaVersion[1], '1.0.0') === -1) { yield logger.failedTask(Messages.CHECK_DEPENDENCIES); throw logger.errorMessage( - `Found an outdated "gql.tada" version, gql.tada requires at least ${MINIMUM_VERSIONS.tada}.` + `The version of ${logger.code('gql.tada')} in your dependencies is out of date.\n` + + logger.hint( + `It's recommended to upgrade ${logger.code('gql.tada')} to at least ${logger.bold( + MINIMUM_VERSIONS.lsp + )}` + ) ); } yield logger.completedTask(Messages.CHECK_DEPENDENCIES); yield logger.runningTask(Messages.CHECK_TSCONFIG); + await delay(); const tsconfigpath = path.resolve(cwd, 'tsconfig.json'); - const root = (await resolveTypeScriptRootDir(tsconfigpath)) || cwd; let tsconfigContents: string; try { - const file = path.resolve(root, 'tsconfig.json'); - tsconfigContents = await fs.readFile(file, 'utf-8'); + tsconfigContents = await fs.readFile(tsconfigpath, 'utf-8'); } catch (_error) { yield logger.failedTask(Messages.CHECK_TSCONFIG); throw logger.errorMessage( - 'Failed to read tsconfig.json in current working directory, try adding a "tsconfig.json".' + `A ${logger.code('tsconfig.json')} file was not found in the current working directory.\n` + + logger.hint( + `Set up a new ${logger.code('tsconfig.json')} containing ${logger.code( + '@0no-co/graphqlp' + )}.` + ) ); } let tsConfig: TsConfigJson; try { tsConfig = parse(tsconfigContents) as TsConfigJson; - } catch (error) { + } catch (error: any) { + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `Your ${logger.code('tsconfig.json')} file could not be parsed.\n` + + logger.console(error.message) + ); + } + + let root: string; + try { + root = (await resolveTypeScriptRootDir(tsconfigpath)) || cwd; + } catch (error: any) { yield logger.failedTask(Messages.CHECK_TSCONFIG); throw logger.errorMessage( - `Unable to parse tsconfig.json in current working directory.\n${error}` + `Failed to resolve a ${logger.code('"extends"')} reference in your ${logger.code( + 'tsconfig.json' + )}.\n` + logger.console(error.message) ); } + if (root !== cwd) { + try { + tsconfigContents = await fs.readFile(path.resolve(root, 'tsconfig.json'), 'utf-8'); + tsConfig = parse(tsconfigContents) as TsConfigJson; + } catch (error: any) { + const relative = path.relative(process.cwd(), root); + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `The ${logger.code('tsconfig.json')} file at ${logger.code( + relative + )} could not be loaded.\n` + logger.console(error.message) + ); + } + } + // Check GraphQLSP version, later on we can check if a ts version is > 5.5.0 to use gql.tada/lsp instead of // the LSP package. - const config = getGraphQLSPConfig(tsConfig); + const config = + tsConfig && + tsConfig.compilerOptions && + tsConfig.compilerOptions.plugins && + (tsConfig.compilerOptions.plugins.find( + (plugin) => plugin.name === '@0no-co/graphqlsp' || plugin.name === 'gql.tada/lsp' + ) as any); if (!config) { yield logger.failedTask(Messages.CHECK_TSCONFIG); - throw logger.errorMessage(`Missing a "@0no-co/graphqlsp" plugin in your tsconfig.`); + throw logger.errorMessage( + `No ${logger.code('"@0no-co/graphqlsp"')} plugin was found in your ${logger.code( + 'tsconfig.json' + )}.\n` + logger.hint(`Have you set up ${logger.code('"@0no-co/graphqlsp"')} yet?`) + ); } // TODO: this is optional I guess with the CLI being there and all if (!config.tadaOutputLocation) { yield logger.failedTask(Messages.CHECK_TSCONFIG); throw logger.errorMessage( - `Missing a "tadaOutputLocation" setting in your GraphQLSP configuration.` + `No ${logger.code('"tadaOutputLocation"')} option was found in your configuration.\n` + + logger.hint( + `Have you chosen an output path for ${logger.code('gql.tada')}'s declaration file yet?` + ) ); } if (!config.schema) { yield logger.failedTask(Messages.CHECK_TSCONFIG); - throw logger.errorMessage(`Missing a "schema" setting in your GraphQLSP configuration.`); + throw logger.errorMessage( + `No ${logger.code('"schema"')} option was found in your configuration.\n` + + logger.hint(`Have you specified your SDL file or URL in your configuration yet?`) + ); + } + + yield logger.completedTask(Messages.CHECK_TSCONFIG); + yield logger.runningTask(Messages.CHECK_SCHEMA); + await delay(); + + // TODO: This doesn't match laoders. Should we just use loaders here? + const isFile = + typeof config.schema === 'string' && + (config.schema.endsWith('.json') || config.schema.endsWith('.graphql')); + if (isFile) { + const resolvedFile = path.resolve(root, config.schema as string); + if (!existsSync(resolvedFile)) { + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `Could not find the SDL file that ${logger.code('"schema"')} is specifying.\n` + + logger.hint(`Have you specified a valid SDL file in your configuration?`) + ); + } } else { - const isFile = - typeof config.schema === 'string' && - (config.schema.endsWith('.json') || config.schema.endsWith('.graphql')); - if (isFile) { - const resolvedFile = path.resolve(root, config.schema as string); - if (!existsSync(resolvedFile)) { - yield logger.failedTask(Messages.CHECK_TSCONFIG); - throw logger.errorMessage( - `The schema setting does not point at an existing file "${resolvedFile}"` - ); - } - } else { - try { - typeof config.schema === 'string' ? new URL(config.schema) : new URL(config.schema.url); - } catch (e) { - yield logger.failedTask(Messages.CHECK_TSCONFIG); - throw logger.errorMessage( - `The schema setting does not point at a valid URL: "${JSON.stringify(config.schema)}"` - ); - } + try { + typeof config.schema === 'string' ? new URL(config.schema) : new URL(config.schema.url); + } catch (_error) { + yield logger.failedTask(Messages.CHECK_TSCONFIG); + throw logger.errorMessage( + `The ${logger.code('"schema"')} option is neither a valid URL nor a valid file.\n` + + logger.hint(`Have you specified a valid URL in your configuration?`) + ); } } - yield logger.completedTask(Messages.CHECK_DEPENDENCIES); + yield logger.completedTask(Messages.CHECK_SCHEMA, true); + await delay(); + yield logger.success(); } diff --git a/packages/cli-utils/src/loggers/check.ts b/packages/cli-utils/src/loggers/check.ts index 90c5441f..2bfa9087 100644 --- a/packages/cli-utils/src/loggers/check.ts +++ b/packages/cli-utils/src/loggers/check.ts @@ -2,6 +2,36 @@ import { pipe, interval, map } from 'wonka'; import * as t from '../term'; +export function code(text: string) { + return t.text`${t.cmd(t.CSI.Style, t.Style.Underline)}${text}${t.cmd( + t.CSI.Style, + t.Style.NoUnderline + )}`; +} + +export function bold(text: string) { + return t.text`${t.cmd(t.CSI.Style, t.Style.Bold)}${text}${t.cmd(t.CSI.Style, t.Style.Normal)}`; +} + +export function hint(text: string) { + return t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + `${t.HeavyBox.BottomLeft} `, + t.cmd(t.CSI.Style, t.Style.BrightBlue), + `${t.Icons.Info} `, + t.cmd(t.CSI.Style, t.Style.Blue), + text, + ]); +} + +export function console(error: any) { + return t.text([ + t.cmd(t.CSI.Style, t.Style.BrightBlack), + `${t.HeavyBox.BottomLeft} `, + error && error instanceof Error ? error.message : `${error}`, + ]); +} + export function emptyLine() { return t.text([t.cmd(t.CSI.Style, t.Style.BrightBlack), t.HeavyBox.Vertical, '\n']); } @@ -86,8 +116,8 @@ export function success() { export function errorMessage(message: string) { return t.error([ '\n', - t.cmd(t.CSI.Style, [t.Style.BrightRed, t.Style.Invert]), - ' Error ', + t.cmd(t.CSI.Style, [t.Style.Red, t.Style.Invert]), + ` ${t.Icons.Warning} Error `, t.cmd(t.CSI.Style, t.Style.NoInvert), `\n${message.trim()}\n`, ]); diff --git a/packages/cli-utils/src/term/write.ts b/packages/cli-utils/src/term/write.ts index e22ef157..1ede7933 100644 --- a/packages/cli-utils/src/term/write.ts +++ b/packages/cli-utils/src/term/write.ts @@ -59,7 +59,9 @@ function clear(text: string) { let lines = 0; for (let index = 0; index < text.length; index++) if (text.charCodeAt(index) === 10 /*'\n'*/) lines++; - return (lines > 0 ? cmd(CSI.PrevLine, lines) : '') + cmd(CSI.DeleteLines, lines + 1); + return ( + (lines > 0 ? cmd(CSI.PrevLine, lines) : cmd(CSI.ToColumn, 1)) + cmd(CSI.DeleteLines, lines + 1) + ); } type ComposeInput = undefined | string | Source | AsyncIterable; From 27f1f4db7210a891e0f57c3c8a6723fe1e84f89b Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 12 Apr 2024 11:25:08 +0100 Subject: [PATCH 09/10] Add changeset --- .changeset/strange-ways-poke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strange-ways-poke.md diff --git a/.changeset/strange-ways-poke.md b/.changeset/strange-ways-poke.md new file mode 100644 index 00000000..fb0a72ab --- /dev/null +++ b/.changeset/strange-ways-poke.md @@ -0,0 +1,5 @@ +--- +"@gql.tada/cli-utils": patch +--- + +Improve log output of `doctor` command. From a5b6d4844923713a82285bf4b2053f536ac8cfbc Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 12 Apr 2024 11:25:25 +0100 Subject: [PATCH 10/10] Update LICENSE --- packages/cli-utils/LICENSE.md | 114 +++++++++++++++++----------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/cli-utils/LICENSE.md b/packages/cli-utils/LICENSE.md index fe4e2de2..2010c09d 100644 --- a/packages/cli-utils/LICENSE.md +++ b/packages/cli-utils/LICENSE.md @@ -44,11 +44,11 @@ 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. -## @0no-co/graphql.web +## json5 MIT License -Copyright (c) 0no.co +Copyright (c) 2012-2018 Aseem Kishore, and [others]. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -68,11 +68,25 @@ 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. -## json5 +[others]: https://github.com/json5/json5/contributors + +## semiver MIT License -Copyright (c) 2012-2018 Aseem Kishore, and [others]. +Copyright (c) Luke Edwards (lukeed.com) + +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. + +## @0no-co/graphql.web + +MIT License + +Copyright (c) 0no.co Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -92,8 +106,6 @@ 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. -[others]: https://github.com/json5/json5/contributors - ## @clack/prompts MIT License @@ -120,18 +132,6 @@ The above copyright notice and this permission notice shall be included in all c 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. -## semiver - -MIT License - -Copyright (c) Luke Edwards (lukeed.com) - -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. - ## mri The MIT License (MIT) @@ -156,6 +156,30 @@ 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. +## wonka + +MIT License + +Copyright (c) 0no.co + +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. + ## cross-spawn The MIT License (MIT) @@ -274,29 +298,23 @@ 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. -## wonka - -MIT License +## which -Copyright (c) 0no.co +The ISC License -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: +Copyright (c) Isaac Z. Schlueter and Contributors -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -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. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ## shebang-command @@ -322,7 +340,7 @@ 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. -## which +## isexe The ISC License @@ -350,22 +368,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of 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. - -## isexe - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file +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. \ No newline at end of file