From b4a49a70196fb828d263c694e7d3017a42f34388 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Sun, 5 Nov 2023 18:08:31 -0300 Subject: [PATCH 01/52] feat: create type for storing theme colors --- src/config/config.ts | 3 ++- src/interfaces/config.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index 8ef88ac70..2036595ec 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -9,7 +9,7 @@ import {Command} from '../command' import {CLIError, error, exit, warn} from '../errors' import {getHelpFlagAdditions} from '../help/util' import {Hook, Hooks, PJSON, Topic} from '../interfaces' -import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, VersionDetails} from '../interfaces/config' +import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, Theme, VersionDetails} from '../interfaces/config' import {Plugin as IPlugin, Options} from '../interfaces/plugin' import {loadWithData} from '../module-loader' import {OCLIF_MARKER_OWNER, Performance} from '../performance' @@ -91,6 +91,7 @@ export class Config implements IConfig { public plugins: Map = new Map() public root!: string public shell!: string + public theme: Theme public topicSeparator: ' ' | ':' = ':' public userAgent!: string public userPJSON?: PJSON.User diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 9a2e73c55..f508fffc8 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -24,6 +24,21 @@ export type VersionDetails = { shell?: string } +export type Theme = { + header: string + flagSeparator: string + flagType: string + flagDefaultValue: string + flagRequired: string + flagDescription: string + command: string + topic: string + commandDescription: string + topicDescription: string + usageDescription: string + versionDescription: string +} + export interface Config { /** * process.arch @@ -122,6 +137,7 @@ export interface Config { * active shell */ readonly shell: string + theme: Theme topicSeparator: ' ' | ':' readonly topics: Topic[] /** From 095c6a7a583c97ab54dd9358265ee39abb5dfb0c Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Mon, 6 Nov 2023 16:29:44 -0300 Subject: [PATCH 02/52] feat: include color and @types/color, and simiplify theme schema --- package.json | 2 ++ src/config/config.ts | 2 +- src/interfaces/config.ts | 26 ++++++++++++-------------- src/interfaces/pjson.ts | 2 ++ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 63dab1a0c..57a9c1cbd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "chalk": "^4.1.2", "clean-stack": "^3.0.1", "cli-progress": "^3.12.0", + "color": "^4.2.3", "debug": "^4.3.4", "ejs": "^3.1.9", "get-package-type": "^0.1.0", @@ -44,6 +45,7 @@ "@types/chai-as-promised": "^7.1.5", "@types/clean-stack": "^2.1.1", "@types/cli-progress": "^3.11.0", + "@types/color": "^3.0.5", "@types/debug": "^4.1.10", "@types/ejs": "^3.1.3", "@types/indent-string": "^4.0.1", diff --git a/src/config/config.ts b/src/config/config.ts index 2036595ec..a8f76b539 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -91,7 +91,7 @@ export class Config implements IConfig { public plugins: Map = new Map() public root!: string public shell!: string - public theme: Theme + public theme!: Theme public topicSeparator: ' ' | ':' = ':' public userAgent!: string public userPJSON?: PJSON.User diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index f508fffc8..201d74535 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -1,3 +1,5 @@ +import Color from 'color' + import {Command} from '../command' import {Hook, Hooks} from './hooks' import {PJSON} from './pjson' @@ -24,19 +26,15 @@ export type VersionDetails = { shell?: string } -export type Theme = { - header: string - flagSeparator: string - flagType: string - flagDefaultValue: string - flagRequired: string - flagDescription: string - command: string - topic: string - commandDescription: string - topicDescription: string - usageDescription: string - versionDescription: string +export interface Theme { + command: Color + flagDefaultValue: Color + flagRequired: Color + flagSeparator: Color + flagType: Color + sectionDescription: Color + sectionHeader: Color + topic: Color } export interface Config { @@ -137,7 +135,7 @@ export interface Config { * active shell */ readonly shell: string - theme: Theme + readonly theme: Theme topicSeparator: ' ' | ':' readonly topics: Topic[] /** diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index 3f82f5fd4..d82979fae 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -1,3 +1,4 @@ +import {Theme} from './config' import {HelpOptions} from './help' export interface PJSON { @@ -43,6 +44,7 @@ export namespace PJSON { repositoryPrefix?: string schema?: number state?: 'beta' | 'deprecated' | string + theme?: Theme topicSeparator?: ' ' | ':' topics?: { [k: string]: { From ad5f32428420b35b0f5e0c3de5a36e7fdaa6595a Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Mon, 6 Nov 2023 16:48:07 -0300 Subject: [PATCH 03/52] feat: set theme's default colors to white --- src/config/config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/config/config.ts b/src/config/config.ts index a8f76b539..f81f61cbb 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,3 +1,4 @@ +import Color from 'color' import * as ejs from 'ejs' import WSL from 'is-wsl' import {arch, userInfo as osUserInfo, release, tmpdir, type} from 'node:os' @@ -313,6 +314,16 @@ export class Config implements IConfig { if (this.pjson.oclif.topicSeparator && [' ', ':'].includes(this.pjson.oclif.topicSeparator)) this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') + this.theme = this.pjson.oclif.theme ?? { + command: new Color('white'), + flagDefaultValue: new Color('white'), + flagRequired: new Color('white'), + flagSeparator: new Color('white'), + flagType: new Color('white'), + sectionDescription: new Color('white'), + sectionHeader: new Color('white'), + topic: new Color('white'), + } this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}` this.shell = this._shell() this.debug = this._debug() From 26a418a518204e7bdf8c7be276ee8a73d5c15eb5 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Mon, 6 Nov 2023 17:22:14 -0300 Subject: [PATCH 04/52] feat: set FORCE_COLOR to 0||3 to follow the NO_COLOR manifest --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index 03787eb00..ea387c060 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,9 @@ import write from './cli-ux/write' +// learn about the no-color manifest in this link: https://no-color.org/ +// learn about how to disable chalks colors in this link: https://github.com/chalk/chalk#supportscolor +process.env.FORCE_COLOR = (Number(!process.env.NO_COLOR) * 3).toString() + function checkCWD() { try { process.cwd() From 4c587b9dc7af992f23ec4d85bc244e5e3a623d0f Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 07:39:18 -0300 Subject: [PATCH 05/52] feat: create DEFAULT_THEME to store default colors --- src/config/config.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index f81f61cbb..02a778a42 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -29,6 +29,17 @@ const debug = Debug() const _pjson = requireJson(__dirname, '..', '..', 'package.json') const BASE = `${_pjson.name}@${_pjson.version}` +const DEFAULT_THEME: Theme = { + command: new Color('white'), + flagDefaultValue: new Color('white'), + flagRequired: new Color('white'), + flagSeparator: new Color('white'), + flagType: new Color('white'), + sectionDescription: new Color('white'), + sectionHeader: new Color('white'), + topic: new Color('white'), +} + function channelFromVersion(version: string) { const m = version.match(/[^-]+(?:-([^.]+))?/) return (m && m[1]) || 'stable' @@ -314,16 +325,9 @@ export class Config implements IConfig { if (this.pjson.oclif.topicSeparator && [' ', ':'].includes(this.pjson.oclif.topicSeparator)) this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') - this.theme = this.pjson.oclif.theme ?? { - command: new Color('white'), - flagDefaultValue: new Color('white'), - flagRequired: new Color('white'), - flagSeparator: new Color('white'), - flagType: new Color('white'), - sectionDescription: new Color('white'), - sectionHeader: new Color('white'), - topic: new Color('white'), - } + + this.theme = this.pjson.oclif.theme ?? DEFAULT_THEME + this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}` this.shell = this._shell() this.debug = this._debug() From f7c59129429ec9ee4b404069d89f0c759f4a8821 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 07:57:36 -0300 Subject: [PATCH 06/52] revert: remove NO_COLOR manifest --- src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index ea387c060..03787eb00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,5 @@ import write from './cli-ux/write' -// learn about the no-color manifest in this link: https://no-color.org/ -// learn about how to disable chalks colors in this link: https://github.com/chalk/chalk#supportscolor -process.env.FORCE_COLOR = (Number(!process.env.NO_COLOR) * 3).toString() - function checkCWD() { try { process.cwd() From 8dbaebd73dc163901661a3d058b539cd5ca824e9 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 11:08:05 -0300 Subject: [PATCH 07/52] feat: add new theme variables to style $, flag, and flag options --- src/interfaces/config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 201d74535..f6852277a 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -28,7 +28,10 @@ export type VersionDetails = { export interface Theme { command: Color + dollarSign: Color + flag: Color flagDefaultValue: Color + flagOptions: Color flagRequired: Color flagSeparator: Color flagType: Color From cbdf4ca68b4bd84a095275e7ba839431d985acae Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 12:21:10 -0300 Subject: [PATCH 08/52] feat: add color to section headers --- src/help/formatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/help/formatter.ts b/src/help/formatter.ts index 9a4b0d9e0..b68a2275f 100644 --- a/src/help/formatter.ts +++ b/src/help/formatter.ts @@ -176,7 +176,7 @@ export class HelpFormatter { } const output = [ - chalk.bold(header), + chalk.hex(this.config.theme.sectionHeader.hex()).bold(header), this.indent( Array.isArray(newBody) ? this.renderList(newBody, {indentation: 2, stripAnsi: this.opts.stripAnsi}) : newBody, ), From 0e823f2fa1a21881ba82fc1adb46d8db627972ff Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 12:24:26 -0300 Subject: [PATCH 09/52] feat: configure default colors --- src/config/config.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 02a778a42..73ae27cf9 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -30,11 +30,14 @@ const _pjson = requireJson(__dirname, '..', '..', 'package.json') const BASE = `${_pjson.name}@${_pjson.version}` const DEFAULT_THEME: Theme = { - command: new Color('white'), - flagDefaultValue: new Color('white'), - flagRequired: new Color('white'), + command: new Color('green'), + dollarSign: new Color('yellow'), + flag: new Color('green'), + flagDefaultValue: new Color('blue'), + flagOptions: new Color('green'), + flagRequired: new Color('red'), flagSeparator: new Color('white'), - flagType: new Color('white'), + flagType: new Color('#0EAEE8'), sectionDescription: new Color('white'), sectionHeader: new Color('white'), topic: new Color('white'), From 0f916958c8872e7d581aeb22284969fd7b1b9cd9 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 13:46:50 -0300 Subject: [PATCH 10/52] feat: add colors for bin, command summary and version --- src/config/config.ts | 5 ++++- src/interfaces/config.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index 73ae27cf9..d1f5f6b4d 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -30,7 +30,9 @@ const _pjson = requireJson(__dirname, '..', '..', 'package.json') const BASE = `${_pjson.name}@${_pjson.version}` const DEFAULT_THEME: Theme = { + bin: new Color('#1798C1'), command: new Color('green'), + commandSummary: new Color('white'), dollarSign: new Color('yellow'), flag: new Color('green'), flagDefaultValue: new Color('blue'), @@ -40,7 +42,8 @@ const DEFAULT_THEME: Theme = { flagType: new Color('#0EAEE8'), sectionDescription: new Color('white'), sectionHeader: new Color('white'), - topic: new Color('white'), + topic: new Color('green'), + version: new Color('green'), } function channelFromVersion(version: string) { diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index f6852277a..713deca07 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -27,7 +27,9 @@ export type VersionDetails = { } export interface Theme { + bin: Color command: Color + commandSummary: Color dollarSign: Color flag: Color flagDefaultValue: Color @@ -38,6 +40,7 @@ export interface Theme { sectionDescription: Color sectionHeader: Color topic: Color + version: Color } export interface Config { From 94ea7af29de3aa187546610e892ba029adc1254a Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 14:24:48 -0300 Subject: [PATCH 11/52] feat: topics, commands, bin, version, sections, dollar sign are colorized --- src/help/command.ts | 57 ++++++++++++++++++++++++++++----------------- src/help/index.ts | 29 +++++++++++++++++------ src/help/root.ts | 21 +++++++++++++---- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/help/command.ts b/src/help/command.ts index 4755a121a..55a8b2eb4 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -13,13 +13,6 @@ import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter' // split on any platform, not just the os specific EOL at runtime. const POSSIBLE_LINE_FEED = /\r\n|\n/ -let {dim} = chalk - -if (process.env.ConEmuANSI === 'ON') { - // eslint-disable-next-line unicorn/consistent-destructuring - dim = chalk.gray -} - export class CommandHelp extends HelpFormatter { constructor( public command: Command.Loadable, @@ -31,7 +24,15 @@ export class CommandHelp extends HelpFormatter { protected aliases(aliases: string[] | undefined): string | undefined { if (!aliases || aliases.length === 0) return - const body = aliases.map((a) => ['$', this.config.bin, a].join(' ')).join('\n') + const body = aliases + .map((a) => + [ + chalk.hex(this.config.theme.dollarSign.hex())('$'), + chalk.hex(this.config.theme.bin.hex())(this.config.bin), + a, + ].join(' '), + ) + .join('\n') return body } @@ -47,9 +48,11 @@ export class CommandHelp extends HelpFormatter { return args.map((a) => { const name = a.name.toUpperCase() let description = a.description || '' - if (a.default) description = `[default: ${a.default}] ${description}` - if (a.options) description = `(${a.options.join('|')}) ${description}` - return [name, description ? dim(description) : undefined] + if (a.default) + description = `${chalk.hex(this.config.theme.flagDefaultValue.hex())(`[default: ${a.default}]`)} ${description}` + if (a.options) + description = `${chalk.hex(this.config.theme.flagOptions.hex())(`(${a.options.join('|')}`)} ${description}` + return [name, description ? chalk.hex(this.config.theme.sectionDescription.hex())(description) : undefined] }) } @@ -122,7 +125,10 @@ export class CommandHelp extends HelpFormatter { ) .join('\n') - return `${this.wrap(description, finalIndentedSpacing)}\n\n${multilineCommands}` + return `${this.wrap( + chalk.hex(this.config.theme.sectionDescription.hex())(description), + finalIndentedSpacing, + )}\n\n${multilineCommands}` }) .join('\n\n') return body @@ -142,7 +148,7 @@ export class CommandHelp extends HelpFormatter { } } - label = labels.join(', ') + label = labels.join(chalk.hex(this.config.theme.flagSeparator.hex())(', ')) } if (flag.type === 'option') { @@ -163,20 +169,20 @@ export class CommandHelp extends HelpFormatter { if (flags.length === 0) return return flags.map((flag) => { - const left = this.flagHelpLabel(flag) + const left = chalk.hex(this.config.theme.flag.hex())(this.flagHelpLabel(flag)) let right = flag.summary || flag.description || '' if (flag.type === 'option' && flag.default) { - right = `[default: ${flag.default}] ${right}` + right = `${chalk.hex(this.config.theme.flagDefaultValue.hex())(`[default: '${flag.default}']`)} ${right}` } - if (flag.required) right = `(required) ${right}` + if (flag.required) right = `${chalk.hex(this.config.theme.flagRequired.hex())('(required)')} ${right}` if (flag.type === 'option' && flag.options && !flag.helpValue && !this.opts.showFlagOptionsInTitle) { - right += `\n` + right += chalk.hex(this.config.theme.flagOptions.hex())(`\n`) } - return [left, dim(right.trim())] + return [left, chalk.hex(this.config.theme.sectionDescription.hex())(right.trim())] }) } @@ -305,7 +311,11 @@ export class CommandHelp extends HelpFormatter { const body = (usage ? castArray(usage) : [this.defaultUsage()]) .map((u) => { const allowedSpacing = this.opts.maxWidth - this.indentSpacing - const line = `$ ${this.config.bin} ${u}`.trim() + const line = `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${chalk.hex(this.config.theme.bin.hex())( + this.config.bin, + )} ${chalk.hex(this.config.theme.command.hex())('<%= command.id %>')}${chalk.hex( + this.config.theme.sectionDescription.hex(), + )(u.replace('<%= command.id %>', ''))}`.trim() if (line.length > allowedSpacing) { const splitIndex = line.slice(0, Math.max(0, allowedSpacing)).lastIndexOf(' ') return ( @@ -323,13 +333,16 @@ export class CommandHelp extends HelpFormatter { private formatIfCommand(example: string): string { example = this.render(example) - if (example.startsWith(this.config.bin)) return dim(`$ ${example}`) - if (example.startsWith(`$ ${this.config.bin}`)) return dim(example) + const dollarSign = chalk.hex(this.config.theme.dollarSign.hex())('$') + if (example.startsWith(this.config.bin)) return `${dollarSign} ${example}` + if (example.startsWith(`${dollarSign} ${this.config.bin}`)) return example return example } private isCommand(example: string): boolean { - return stripAnsi(this.formatIfCommand(example)).startsWith(`$ ${this.config.bin}`) + return stripAnsi(this.formatIfCommand(example)).startsWith( + `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${this.config.bin}`, + ) } } export default CommandHelp diff --git a/src/help/index.ts b/src/help/index.ts index f43f17dd4..389c83d21 100644 --- a/src/help/index.ts +++ b/src/help/index.ts @@ -1,3 +1,4 @@ +import chalk from 'chalk' import {format} from 'node:util' import stripAnsi from 'strip-ansi' @@ -103,7 +104,11 @@ export class Help extends HelpBase { .filter((c) => (this.opts.hideAliasesFromRoot ? !c.aliases?.includes(c.id) : true)) .map((c) => { if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator) - return [c.id, this.summary(c)] + const summary = this.summary(c) + return [ + chalk.hex(this.config.theme.command.hex())(c.id), + summary && chalk.hex(this.config.theme.sectionDescription.hex())(summary), + ] }), { indentation: 2, @@ -127,9 +132,15 @@ export class Help extends HelpBase { let topicID = `${topic.name}:COMMAND` if (this.config.topicSeparator !== ':') topicID = topicID.replaceAll(':', this.config.topicSeparator) let output = compact([ - summary, - this.section(this.opts.usageHeader || 'USAGE', `$ ${this.config.bin} ${topicID}`), - description && this.section('DESCRIPTION', this.wrap(description)), + chalk.hex(this.config.theme.commandSummary.hex())(summary), + this.section( + this.opts.usageHeader || 'USAGE', + `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${chalk.hex(this.config.theme.bin.hex())( + this.config.bin, + )} ${topicID}`, + ), + description && + this.section('DESCRIPTION', this.wrap(chalk.hex(this.config.theme.sectionDescription.hex())(description))), ]).join('\n\n') if (this.opts.stripAnsi) output = stripAnsi(output) return output + '\n' @@ -140,7 +151,11 @@ export class Help extends HelpBase { const body = this.renderList( topics.map((c) => { if (this.config.topicSeparator !== ':') c.name = c.name.replaceAll(':', this.config.topicSeparator) - return [c.name, c.description && this.render(c.description.split('\n')[0])] + return [ + chalk.hex(this.config.theme.topic.hex())(c.name), + c.description && + this.render(chalk.hex(this.config.theme.sectionDescription.hex())(c.description.split('\n')[0])), + ] }), { indentation: 2, @@ -334,9 +349,9 @@ export class Help extends HelpBase { } protected summary(c: Command.Loadable): string | undefined { - if (c.summary) return this.render(c.summary.split('\n')[0]) + if (c.summary) return chalk.hex(this.config.theme.commandSummary.hex())(this.render(c.summary.split('\n')[0])) - return c.description && this.render(c.description).split('\n')[0] + return c.description && chalk.hex(this.config.theme.commandSummary.hex())(this.render(c.description).split('\n')[0]) } /* diff --git a/src/help/root.ts b/src/help/root.ts index 3955dc68c..809d67074 100644 --- a/src/help/root.ts +++ b/src/help/root.ts @@ -1,3 +1,4 @@ +import chalk from 'chalk' import stripAnsi from 'strip-ansi' import * as Interfaces from '../interfaces' @@ -17,23 +18,35 @@ export default class RootHelp extends HelpFormatter { description = this.render(description) description = description.split('\n').slice(1).join('\n') if (!description) return - return this.section('DESCRIPTION', this.wrap(description)) + return this.section('DESCRIPTION', this.wrap(chalk.hex(this.config.theme.sectionDescription.hex())(description))) } root(): string { let description = this.config.pjson.oclif.description || this.config.pjson.description || '' description = this.render(description) description = description.split('\n')[0] - let output = compact([description, this.version(), this.usage(), this.description()]).join('\n\n') + let output = compact([ + chalk.hex(this.config.theme.sectionDescription.hex())(description), + this.version(), + this.usage(), + this.description(), + ]).join('\n\n') if (this.opts.stripAnsi) output = stripAnsi(output) return output } protected usage(): string { - return this.section(this.opts.usageHeader || 'USAGE', this.wrap(`$ ${this.config.bin} [COMMAND]`)) + return this.section( + this.opts.usageHeader || 'USAGE', + this.wrap( + `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${chalk.hex(this.config.theme.bin.hex())( + this.config.bin, + )} ${chalk.hex(this.config.theme.sectionDescription.hex())('[COMMAND]')}`, + ), + ) } protected version(): string { - return this.section('VERSION', this.wrap(this.config.userAgent)) + return this.section('VERSION', this.wrap(chalk.hex(this.config.theme.version.hex())(this.config.userAgent))) } } From 3208ea310fa16573ebf638aa0b0ab005251e4b39 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 14:25:23 -0300 Subject: [PATCH 12/52] feat: configure default colors --- src/config/config.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index d1f5f6b4d..04208aeb0 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -30,20 +30,20 @@ const _pjson = requireJson(__dirname, '..', '..', 'package.json') const BASE = `${_pjson.name}@${_pjson.version}` const DEFAULT_THEME: Theme = { - bin: new Color('#1798C1'), - command: new Color('green'), - commandSummary: new Color('white'), - dollarSign: new Color('yellow'), - flag: new Color('green'), - flagDefaultValue: new Color('blue'), - flagOptions: new Color('green'), - flagRequired: new Color('red'), - flagSeparator: new Color('white'), + bin: new Color('#1AB9FF'), + command: new Color('#45C65A'), + commandSummary: new Color('#FFFFFF'), + dollarSign: new Color('#FFFF00'), + flag: new Color('#45C65A'), + flagDefaultValue: new Color('#1AB9FF'), + flagOptions: new Color('#45C65A'), + flagRequired: new Color('#FE5C4C'), + flagSeparator: new Color('#FFFFFF'), flagType: new Color('#0EAEE8'), - sectionDescription: new Color('white'), - sectionHeader: new Color('white'), - topic: new Color('green'), - version: new Color('green'), + sectionDescription: new Color('#FFFFFF'), + sectionHeader: new Color('#FFFF00'), + topic: new Color('#45C65A'), + version: new Color('#45C65A'), } function channelFromVersion(version: string) { From 20a150c48083405d11d615f7c51804df987399a1 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 15:28:12 -0300 Subject: [PATCH 13/52] feat: add feature flag to enabled/disable theme --- src/config/config.ts | 6 +++++- src/interfaces/config.ts | 1 + src/interfaces/pjson.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index 04208aeb0..c3f362bf7 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -98,6 +98,7 @@ export class Config implements IConfig { public dataDir!: string public debug = 0 public dirname!: string + public enableTheme: boolean = false public errlog!: string public flexibleTaxonomy!: boolean public home!: string @@ -332,7 +333,10 @@ export class Config implements IConfig { this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') - this.theme = this.pjson.oclif.theme ?? DEFAULT_THEME + this.enableTheme = this.pjson.oclif.enableTheme ?? process.env.OCLIF_ENABLE_THEME ?? false + if (this.enableTheme) { + this.theme = this.pjson.oclif.theme ?? DEFAULT_THEME + } this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}` this.shell = this._shell() diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 713deca07..97fedad40 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -88,6 +88,7 @@ export interface Config { * base dirname to use in cacheDir/configDir/dataDir */ readonly dirname: string + enableTheme: boolean /** * points to a file that should be appended to for error logs * diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index d82979fae..85318b1c1 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -26,6 +26,7 @@ export namespace PJSON { default?: string description?: string devPlugins?: string[] + enableTheme: boolean flexibleTaxonomy?: boolean helpClass?: string helpOptions?: HelpOptions From 07d7501caf39d3b7054624282f02993c2fd06bcd Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 15:28:33 -0300 Subject: [PATCH 14/52] feat: add colorize function to simplify the way colors are added to strings --- src/help/util.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/help/util.ts b/src/help/util.ts index 82db784f5..4b7859f2a 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -1,3 +1,5 @@ +import chalk from 'chalk' +import Color from 'color' import * as ejs from 'ejs' import {collectUsableIds} from '../config/util' @@ -107,3 +109,7 @@ export function normalizeArgv(config: IConfig, argv = process.argv.slice(2)): st if (config.topicSeparator !== ':' && !argv[0]?.includes(':')) argv = standardizeIDFromArgv(argv, config) return argv } + +export function colorize(color: Color, text: string) { + return color ? chalk.hex(color.hex())(text) : text +} From 1e91233dffb7d8b26b7b3fffb54e1332b8d96497 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 15:30:08 -0300 Subject: [PATCH 15/52] feat: change all chalk.hex calls to colorize --- src/help/command.ts | 43 ++++++++++++++++++++++--------------------- src/help/formatter.ts | 4 ++-- src/help/index.ts | 29 +++++++++++++++++------------ src/help/root.ts | 13 +++++++------ 4 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/help/command.ts b/src/help/command.ts index 55a8b2eb4..35d99b78c 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -7,6 +7,7 @@ import {ensureArgObject} from '../util/ensure-arg-object' import {castArray, compact, sortBy} from '../util/util' import {DocOpts} from './docopts' import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter' +import {colorize} from './util' // Don't use os.EOL because we need to ensure that a string // written on any platform, that may use \r\n or \n, will be @@ -26,11 +27,9 @@ export class CommandHelp extends HelpFormatter { if (!aliases || aliases.length === 0) return const body = aliases .map((a) => - [ - chalk.hex(this.config.theme.dollarSign.hex())('$'), - chalk.hex(this.config.theme.bin.hex())(this.config.bin), - a, - ].join(' '), + [colorize(this.config?.theme?.dollarSign, '$'), colorize(this.config?.theme?.bin, this.config.bin), a].join( + ' ', + ), ) .join('\n') return body @@ -49,10 +48,10 @@ export class CommandHelp extends HelpFormatter { const name = a.name.toUpperCase() let description = a.description || '' if (a.default) - description = `${chalk.hex(this.config.theme.flagDefaultValue.hex())(`[default: ${a.default}]`)} ${description}` + description = `${colorize(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}` if (a.options) - description = `${chalk.hex(this.config.theme.flagOptions.hex())(`(${a.options.join('|')}`)} ${description}` - return [name, description ? chalk.hex(this.config.theme.sectionDescription.hex())(description) : undefined] + description = `${colorize(this.config?.theme?.flagOptions, `(${a.options.join('|')}`)} ${description}` + return [name, description ? colorize(this.config?.theme?.sectionDescription, description) : undefined] }) } @@ -126,7 +125,7 @@ export class CommandHelp extends HelpFormatter { .join('\n') return `${this.wrap( - chalk.hex(this.config.theme.sectionDescription.hex())(description), + colorize(this.config?.theme?.sectionDescription, description), finalIndentedSpacing, )}\n\n${multilineCommands}` }) @@ -148,7 +147,7 @@ export class CommandHelp extends HelpFormatter { } } - label = labels.join(chalk.hex(this.config.theme.flagSeparator.hex())(', ')) + label = labels.join(colorize(this.config?.theme?.flagSeparator, ', ')) } if (flag.type === 'option') { @@ -169,20 +168,20 @@ export class CommandHelp extends HelpFormatter { if (flags.length === 0) return return flags.map((flag) => { - const left = chalk.hex(this.config.theme.flag.hex())(this.flagHelpLabel(flag)) + const left = colorize(this.config?.theme?.flag, this.flagHelpLabel(flag)) let right = flag.summary || flag.description || '' if (flag.type === 'option' && flag.default) { - right = `${chalk.hex(this.config.theme.flagDefaultValue.hex())(`[default: '${flag.default}']`)} ${right}` + right = `${colorize(this.config?.theme?.flagDefaultValue, `[default: '${flag.default}']`)} ${right}` } - if (flag.required) right = `${chalk.hex(this.config.theme.flagRequired.hex())('(required)')} ${right}` + if (flag.required) right = `${colorize(this.config?.theme?.flagRequired, '(required)')} ${right}` if (flag.type === 'option' && flag.options && !flag.helpValue && !this.opts.showFlagOptionsInTitle) { - right += chalk.hex(this.config.theme.flagOptions.hex())(`\n`) + right += colorize(this.config?.theme?.flagOptions, `\n`) } - return [left, chalk.hex(this.config.theme.sectionDescription.hex())(right.trim())] + return [left, colorize(this.config?.theme?.sectionDescription, right.trim())] }) } @@ -311,11 +310,13 @@ export class CommandHelp extends HelpFormatter { const body = (usage ? castArray(usage) : [this.defaultUsage()]) .map((u) => { const allowedSpacing = this.opts.maxWidth - this.indentSpacing - const line = `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${chalk.hex(this.config.theme.bin.hex())( + const line = `${colorize(this.config?.theme?.dollarSign, '$')} ${colorize( + this.config?.theme?.bin, this.config.bin, - )} ${chalk.hex(this.config.theme.command.hex())('<%= command.id %>')}${chalk.hex( - this.config.theme.sectionDescription.hex(), - )(u.replace('<%= command.id %>', ''))}`.trim() + )} ${colorize(this.config?.theme?.command, '<%= command.id %>')}${colorize( + this.config?.theme?.sectionDescription, + u.replace('<%= command.id %>', ''), + )}`.trim() if (line.length > allowedSpacing) { const splitIndex = line.slice(0, Math.max(0, allowedSpacing)).lastIndexOf(' ') return ( @@ -333,7 +334,7 @@ export class CommandHelp extends HelpFormatter { private formatIfCommand(example: string): string { example = this.render(example) - const dollarSign = chalk.hex(this.config.theme.dollarSign.hex())('$') + const dollarSign = colorize(this.config?.theme?.dollarSign, '$') if (example.startsWith(this.config.bin)) return `${dollarSign} ${example}` if (example.startsWith(`${dollarSign} ${this.config.bin}`)) return example return example @@ -341,7 +342,7 @@ export class CommandHelp extends HelpFormatter { private isCommand(example: string): boolean { return stripAnsi(this.formatIfCommand(example)).startsWith( - `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${this.config.bin}`, + `${colorize(this.config?.theme?.dollarSign, '$')} ${this.config.bin}`, ) } } diff --git a/src/help/formatter.ts b/src/help/formatter.ts index b68a2275f..67ea0da76 100644 --- a/src/help/formatter.ts +++ b/src/help/formatter.ts @@ -8,7 +8,7 @@ import wrap from 'wrap-ansi' import {Command} from '../command' import * as Interfaces from '../interfaces' import {stdtermwidth} from '../screen' -import {template} from './util' +import {colorize, template} from './util' export type HelpSectionKeyValueTable = {description: string; name: string}[] export type HelpSection = @@ -176,7 +176,7 @@ export class HelpFormatter { } const output = [ - chalk.hex(this.config.theme.sectionHeader.hex()).bold(header), + colorize(this.config?.theme?.sectionHeader, chalk.bold(header)), this.indent( Array.isArray(newBody) ? this.renderList(newBody, {indentation: 2, stripAnsi: this.opts.stripAnsi}) : newBody, ), diff --git a/src/help/index.ts b/src/help/index.ts index 389c83d21..1851c7c08 100644 --- a/src/help/index.ts +++ b/src/help/index.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk' import {format} from 'node:util' import stripAnsi from 'strip-ansi' @@ -12,7 +11,13 @@ import {compact, sortBy, uniqBy} from '../util/util' import {CommandHelp} from './command' import {HelpFormatter} from './formatter' import RootHelp from './root' -import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFromArgv, toConfiguredId} from './util' +import { + colorize, + formatCommandDeprecationWarning, + getHelpFlagAdditions, + standardizeIDFromArgv, + toConfiguredId, +} from './util' export {CommandHelp} from './command' export {getHelpFlagAdditions, normalizeArgv, standardizeIDFromArgv} from './util' @@ -106,8 +111,8 @@ export class Help extends HelpBase { if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator) const summary = this.summary(c) return [ - chalk.hex(this.config.theme.command.hex())(c.id), - summary && chalk.hex(this.config.theme.sectionDescription.hex())(summary), + colorize(this.config?.theme?.command, c.id), + summary && colorize(this.config?.theme?.sectionDescription, summary), ] }), { @@ -132,15 +137,16 @@ export class Help extends HelpBase { let topicID = `${topic.name}:COMMAND` if (this.config.topicSeparator !== ':') topicID = topicID.replaceAll(':', this.config.topicSeparator) let output = compact([ - chalk.hex(this.config.theme.commandSummary.hex())(summary), + colorize(this.config?.theme?.commandSummary, summary), this.section( this.opts.usageHeader || 'USAGE', - `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${chalk.hex(this.config.theme.bin.hex())( + `${colorize(this.config?.theme?.dollarSign, '$')} ${colorize( + this.config?.theme?.bin, this.config.bin, )} ${topicID}`, ), description && - this.section('DESCRIPTION', this.wrap(chalk.hex(this.config.theme.sectionDescription.hex())(description))), + this.section('DESCRIPTION', this.wrap(colorize(this.config?.theme?.sectionDescription, description))), ]).join('\n\n') if (this.opts.stripAnsi) output = stripAnsi(output) return output + '\n' @@ -152,9 +158,8 @@ export class Help extends HelpBase { topics.map((c) => { if (this.config.topicSeparator !== ':') c.name = c.name.replaceAll(':', this.config.topicSeparator) return [ - chalk.hex(this.config.theme.topic.hex())(c.name), - c.description && - this.render(chalk.hex(this.config.theme.sectionDescription.hex())(c.description.split('\n')[0])), + colorize(this.config?.theme?.topic, c.name), + c.description && this.render(colorize(this.config?.theme?.sectionDescription, c.description.split('\n')[0])), ] }), { @@ -349,9 +354,9 @@ export class Help extends HelpBase { } protected summary(c: Command.Loadable): string | undefined { - if (c.summary) return chalk.hex(this.config.theme.commandSummary.hex())(this.render(c.summary.split('\n')[0])) + if (c.summary) return colorize(this.config?.theme?.commandSummary, this.render(c.summary.split('\n')[0])) - return c.description && chalk.hex(this.config.theme.commandSummary.hex())(this.render(c.description).split('\n')[0]) + return c.description && colorize(this.config?.theme?.commandSummary, this.render(c.description).split('\n')[0]) } /* diff --git a/src/help/root.ts b/src/help/root.ts index 809d67074..b3300b1de 100644 --- a/src/help/root.ts +++ b/src/help/root.ts @@ -1,9 +1,9 @@ -import chalk from 'chalk' import stripAnsi from 'strip-ansi' import * as Interfaces from '../interfaces' import {compact} from '../util/util' import {HelpFormatter} from './formatter' +import {colorize} from './util' export default class RootHelp extends HelpFormatter { constructor( @@ -18,7 +18,7 @@ export default class RootHelp extends HelpFormatter { description = this.render(description) description = description.split('\n').slice(1).join('\n') if (!description) return - return this.section('DESCRIPTION', this.wrap(chalk.hex(this.config.theme.sectionDescription.hex())(description))) + return this.section('DESCRIPTION', this.wrap(colorize(this.config?.theme?.sectionDescription, description))) } root(): string { @@ -26,7 +26,7 @@ export default class RootHelp extends HelpFormatter { description = this.render(description) description = description.split('\n')[0] let output = compact([ - chalk.hex(this.config.theme.sectionDescription.hex())(description), + colorize(this.config?.theme?.sectionDescription, description), this.version(), this.usage(), this.description(), @@ -39,14 +39,15 @@ export default class RootHelp extends HelpFormatter { return this.section( this.opts.usageHeader || 'USAGE', this.wrap( - `${chalk.hex(this.config.theme.dollarSign.hex())('$')} ${chalk.hex(this.config.theme.bin.hex())( + `${colorize(this.config?.theme?.dollarSign, '$')} ${colorize( + this.config?.theme?.bin, this.config.bin, - )} ${chalk.hex(this.config.theme.sectionDescription.hex())('[COMMAND]')}`, + )} ${colorize(this.config?.theme?.sectionDescription, '[COMMAND]')}`, ), ) } protected version(): string { - return this.section('VERSION', this.wrap(chalk.hex(this.config.theme.version.hex())(this.config.userAgent))) + return this.section('VERSION', this.wrap(colorize(this.config?.theme?.version, this.config.userAgent))) } } From 27987e407ffa2ea01b9a5677025b76705aa66acf Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 15:38:35 -0300 Subject: [PATCH 16/52] feat: configure OCLIF_ENABLE_THEME to have precence over PJSON prop --- src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index c3f362bf7..cd2d88a61 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -333,7 +333,7 @@ export class Config implements IConfig { this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') - this.enableTheme = this.pjson.oclif.enableTheme ?? process.env.OCLIF_ENABLE_THEME ?? false + this.enableTheme = process.env.OCLIF_ENABLE_THEME ?? this.pjson.oclif.enableTheme ?? false if (this.enableTheme) { this.theme = this.pjson.oclif.theme ?? DEFAULT_THEME } From 180b179fb4e99e2a170ace907e526279c71c4f19 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 15:44:14 -0300 Subject: [PATCH 17/52] feat: all theme colors are optional --- src/interfaces/config.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 97fedad40..111f2d94e 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -27,20 +27,20 @@ export type VersionDetails = { } export interface Theme { - bin: Color - command: Color - commandSummary: Color - dollarSign: Color - flag: Color - flagDefaultValue: Color - flagOptions: Color - flagRequired: Color - flagSeparator: Color - flagType: Color - sectionDescription: Color - sectionHeader: Color - topic: Color - version: Color + bin?: Color + command?: Color + commandSummary?: Color + dollarSign?: Color + flag?: Color + flagDefaultValue?: Color + flagOptions?: Color + flagRequired?: Color + flagSeparator?: Color + flagType?: Color + sectionDescription?: Color + sectionHeader?: Color + topic?: Color + version?: Color } export interface Config { From 8d561f73364aa80f47ea316a66722f8b4d0412f0 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 15:53:45 -0300 Subject: [PATCH 18/52] fix: error TS2322: Type 'string' is not assignable to type 'boolean' --- src/config/config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index cd2d88a61..616d0d2c7 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -333,7 +333,8 @@ export class Config implements IConfig { this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') - this.enableTheme = process.env.OCLIF_ENABLE_THEME ?? this.pjson.oclif.enableTheme ?? false + const OCLIF_ENABLE_THEME = process.env.OCLIF_ENABLE_THEME === 'true' + this.enableTheme = OCLIF_ENABLE_THEME ?? this.pjson.oclif.enableTheme ?? false if (this.enableTheme) { this.theme = this.pjson.oclif.theme ?? DEFAULT_THEME } From 22d3e6ddade69cfc326408bba347f6d81f8e373a Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 16:06:52 -0300 Subject: [PATCH 19/52] fix: error TS2345: arg of type 'Color|undefined' not assignable to 'Color' --- src/help/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/help/util.ts b/src/help/util.ts index 4b7859f2a..9a13bed15 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -110,6 +110,6 @@ export function normalizeArgv(config: IConfig, argv = process.argv.slice(2)): st return argv } -export function colorize(color: Color, text: string) { +export function colorize(color: Color | undefined, text: string) { return color ? chalk.hex(color.hex())(text) : text } From 39bf5b2e995a5d08e8a71c05443a6d46b01ba1b6 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 16:08:12 -0300 Subject: [PATCH 20/52] fix: runtime error TypeError: color.hex is not a function --- src/config/config.ts | 12 ++++++++++-- src/interfaces/config.ts | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 616d0d2c7..fa6f9ff50 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -10,7 +10,15 @@ import {Command} from '../command' import {CLIError, error, exit, warn} from '../errors' import {getHelpFlagAdditions} from '../help/util' import {Hook, Hooks, PJSON, Topic} from '../interfaces' -import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, Theme, VersionDetails} from '../interfaces/config' +import { + ArchTypes, + Config as IConfig, + LoadOptions, + PlatformTypes, + Theme, + VersionDetails, + parseTheme, +} from '../interfaces/config' import {Plugin as IPlugin, Options} from '../interfaces/plugin' import {loadWithData} from '../module-loader' import {OCLIF_MARKER_OWNER, Performance} from '../performance' @@ -336,7 +344,7 @@ export class Config implements IConfig { const OCLIF_ENABLE_THEME = process.env.OCLIF_ENABLE_THEME === 'true' this.enableTheme = OCLIF_ENABLE_THEME ?? this.pjson.oclif.enableTheme ?? false if (this.enableTheme) { - this.theme = this.pjson.oclif.theme ?? DEFAULT_THEME + this.theme = this.pjson.oclif?.theme ? parseTheme(this.pjson.oclif?.theme) : DEFAULT_THEME } this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}` diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 111f2d94e..53ff1674c 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -43,6 +43,18 @@ export interface Theme { version?: Color } +export function parseTheme(json: any): Theme { + const theme: Theme = {} + + for (const prop in json) { + if (Object.prototype.hasOwnProperty.call(json, prop)) { + theme[prop as keyof Theme] = new Color(json[prop]) + } + } + + return theme +} + export interface Config { /** * process.arch From 7be8136f9ccf6b79ddfbd4c721c1128b6edcb114 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 16:16:35 -0300 Subject: [PATCH 21/52] fix: this.pjson.oclif.enableTheme was not being evaluated when OCLIF_ENABLE_THEME was unset --- src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index fa6f9ff50..c5430efb2 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -342,7 +342,7 @@ export class Config implements IConfig { if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') const OCLIF_ENABLE_THEME = process.env.OCLIF_ENABLE_THEME === 'true' - this.enableTheme = OCLIF_ENABLE_THEME ?? this.pjson.oclif.enableTheme ?? false + this.enableTheme = OCLIF_ENABLE_THEME || this.pjson.oclif.enableTheme if (this.enableTheme) { this.theme = this.pjson.oclif?.theme ? parseTheme(this.pjson.oclif?.theme) : DEFAULT_THEME } From 653c1c2e68881e49f06b0413173d060d40e7899d Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 16:21:45 -0300 Subject: [PATCH 22/52] chore(deps): update yarn.lock --- yarn.lock | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index c319598bc..d4240b431 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1012,11 +1012,25 @@ dependencies: "@types/node" "*" +"@types/color-convert@*": + version "2.0.2" + resolved "http://localhost:4873/@types/color-convert/-/color-convert-2.0.2.tgz#a5fa5da9b866732f8bf86b01964869011e2a2356" + integrity sha512-KGRIgCxwcgazts4MXRCikPbIMzBpjfdgEZSy8TRHU/gtg+f9sOfHdtK8unPfxIoBtyd2aTTwINVLSNENlC8U8A== + dependencies: + "@types/color-name" "*" + "@types/color-name@*": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/color@^3.0.5": + version "3.0.5" + resolved "http://localhost:4873/@types/color/-/color-3.0.5.tgz#658fd9286a44c21dabaa56c2e2f63da3ac15f063" + integrity sha512-T9yHCNtd8ap9L/r8KEESu5RDMLkoWXHo7dTureNoI1dbp25NsCN054vOu09iniIjR21MXUL+LU9bkIWrbyg8gg== + dependencies: + "@types/color-convert" "*" + "@types/debug@^4.1.10": version "4.1.10" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.10.tgz#f23148a6eb771a34c466a4fc28379d8101e84494" @@ -2091,16 +2105,32 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.1.4, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.1" + resolved "http://localhost:4873/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^4.2.3: + version "4.2.3" + resolved "http://localhost:4873/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colorette@^2.0.20: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -3844,6 +3874,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "http://localhost:4873/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -6282,6 +6317,13 @@ sigstore@^1.3.0, sigstore@^1.4.0, sigstore@^1.7.0: "@sigstore/tuf" "^1.0.3" make-fetch-happen "^11.0.1" +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "http://localhost:4873/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + sinon@^16.0.0, sinon@^16.1.0: version "16.1.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-16.1.0.tgz#645b836563c9bedb21defdbe48831cb2afb687f2" From 6673ec5f1b26535628a87e68178f926b9b1701bf Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 16:31:49 -0300 Subject: [PATCH 23/52] refactor: simplified code removing OCLIF_ENABLE_THEME constant --- src/config/config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index c5430efb2..19d4896f3 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -341,8 +341,7 @@ export class Config implements IConfig { this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') - const OCLIF_ENABLE_THEME = process.env.OCLIF_ENABLE_THEME === 'true' - this.enableTheme = OCLIF_ENABLE_THEME || this.pjson.oclif.enableTheme + this.enableTheme = process.env.OCLIF_ENABLE_THEME === 'true' || this.pjson.oclif.enableTheme if (this.enableTheme) { this.theme = this.pjson.oclif?.theme ? parseTheme(this.pjson.oclif?.theme) : DEFAULT_THEME } From 80421eea8d45afcf03a3697cc070b6ef45c4818f Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 16:57:57 -0300 Subject: [PATCH 24/52] fix: command summary was not changing its color when running the root command --- src/help/root.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/help/root.ts b/src/help/root.ts index b3300b1de..a62a1bf37 100644 --- a/src/help/root.ts +++ b/src/help/root.ts @@ -26,7 +26,7 @@ export default class RootHelp extends HelpFormatter { description = this.render(description) description = description.split('\n')[0] let output = compact([ - colorize(this.config?.theme?.sectionDescription, description), + colorize(this.config?.theme?.commandSummary, description), this.version(), this.usage(), this.description(), From fd3d17ed24035484d151d3431a568f21c1deacc8 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 22:18:47 -0300 Subject: [PATCH 25/52] feat: theme is now read from ~/config//theme.json if one exists --- src/config/config.ts | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 19d4896f3..57f4db0e2 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,8 +1,7 @@ -import Color from 'color' import * as ejs from 'ejs' import WSL from 'is-wsl' import {arch, userInfo as osUserInfo, release, tmpdir, type} from 'node:os' -import {join, sep} from 'node:path' +import path, {join, sep} from 'node:path' import {URL, fileURLToPath} from 'node:url' import {ux} from '../cli-ux' @@ -23,7 +22,7 @@ import {Plugin as IPlugin, Options} from '../interfaces/plugin' import {loadWithData} from '../module-loader' import {OCLIF_MARKER_OWNER, Performance} from '../performance' import {settings} from '../settings' -import {requireJson} from '../util/fs' +import {existsSync, readJsonSync, requireJson} from '../util/fs' import {getHomeDir, getPlatform} from '../util/os' import {compact, isProd} from '../util/util' import Cache from './cache' @@ -37,23 +36,6 @@ const debug = Debug() const _pjson = requireJson(__dirname, '..', '..', 'package.json') const BASE = `${_pjson.name}@${_pjson.version}` -const DEFAULT_THEME: Theme = { - bin: new Color('#1AB9FF'), - command: new Color('#45C65A'), - commandSummary: new Color('#FFFFFF'), - dollarSign: new Color('#FFFF00'), - flag: new Color('#45C65A'), - flagDefaultValue: new Color('#1AB9FF'), - flagOptions: new Color('#45C65A'), - flagRequired: new Color('#FE5C4C'), - flagSeparator: new Color('#FFFFFF'), - flagType: new Color('#0EAEE8'), - sectionDescription: new Color('#FFFFFF'), - sectionHeader: new Color('#FFFF00'), - topic: new Color('#45C65A'), - version: new Color('#45C65A'), -} - function channelFromVersion(version: string) { const m = version.match(/[^-]+(?:-([^.]+))?/) return (m && m[1]) || 'stable' @@ -341,11 +323,6 @@ export class Config implements IConfig { this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') - this.enableTheme = process.env.OCLIF_ENABLE_THEME === 'true' || this.pjson.oclif.enableTheme - if (this.enableTheme) { - this.theme = this.pjson.oclif?.theme ? parseTheme(this.pjson.oclif?.theme) : DEFAULT_THEME - } - this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}` this.shell = this._shell() this.debug = this._debug() @@ -359,6 +336,14 @@ export class Config implements IConfig { this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry + this.enableTheme = this.scopedEnvVarTrue('ENABLE_THEME') ?? this.pjson.oclif.enableTheme + if (this.enableTheme) { + const jsonTheme = path.resolve(this.configDir, 'theme.json') + if (existsSync(jsonTheme)) { + this.theme = parseTheme(readJsonSync(jsonTheme)) + } + } + this.pjson.oclif.update = this.pjson.oclif.update || {} this.pjson.oclif.update.node = this.pjson.oclif.update.node || {} const s3 = this.pjson.oclif.update.s3 || {} From f6c7744a82509fd9db08f5d38f3455280839dc25 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 23:08:53 -0300 Subject: [PATCH 26/52] fix: add colors to other ARGUMENTS, EXAMPLES, DESCRIPTION, FLAGS DESCRIPTIONS sections --- src/help/command.ts | 26 ++++++++++++++------------ src/help/index.ts | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/help/command.ts b/src/help/command.ts index 35d99b78c..608f9988f 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -27,9 +27,11 @@ export class CommandHelp extends HelpFormatter { if (!aliases || aliases.length === 0) return const body = aliases .map((a) => - [colorize(this.config?.theme?.dollarSign, '$'), colorize(this.config?.theme?.bin, this.config.bin), a].join( - ' ', - ), + [ + colorize(this.config?.theme?.dollarSign, '$'), + colorize(this.config?.theme?.bin, this.config.bin), + colorize(this.config?.theme?.sectionDescription, a), + ].join(' '), ) .join('\n') return body @@ -51,7 +53,10 @@ export class CommandHelp extends HelpFormatter { description = `${colorize(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}` if (a.options) description = `${colorize(this.config?.theme?.flagOptions, `(${a.options.join('|')}`)} ${description}` - return [name, description ? colorize(this.config?.theme?.sectionDescription, description) : undefined] + return [ + colorize(this.config?.theme?.flag, name), + description ? colorize(this.config?.theme?.sectionDescription, description) : undefined, + ] }) } @@ -84,7 +89,7 @@ export class CommandHelp extends HelpFormatter { } if (description) { - return this.wrap(description.join('\n')) + return this.wrap(colorize(this.config?.theme?.commandSummary, description.join('\n'))) } } @@ -124,13 +129,10 @@ export class CommandHelp extends HelpFormatter { ) .join('\n') - return `${this.wrap( - colorize(this.config?.theme?.sectionDescription, description), - finalIndentedSpacing, - )}\n\n${multilineCommands}` + return `${this.wrap(description, finalIndentedSpacing)}\n\n${multilineCommands}` }) .join('\n\n') - return body + return colorize(this.config?.theme?.sectionDescription, body) } protected flagHelpLabel(flag: Command.Flag.Any, showOptions = false): string { @@ -202,7 +204,7 @@ export class CommandHelp extends HelpFormatter { }) .join('\n\n') - return body + return colorize(this.config?.theme?.sectionDescription, body) } generate(): string { @@ -336,7 +338,7 @@ export class CommandHelp extends HelpFormatter { example = this.render(example) const dollarSign = colorize(this.config?.theme?.dollarSign, '$') if (example.startsWith(this.config.bin)) return `${dollarSign} ${example}` - if (example.startsWith(`${dollarSign} ${this.config.bin}`)) return example + if (example.startsWith(`$ ${this.config.bin}`)) return `${dollarSign}${example.replace(`$`, '')}` return example } diff --git a/src/help/index.ts b/src/help/index.ts index 1851c7c08..3dced2591 100644 --- a/src/help/index.ts +++ b/src/help/index.ts @@ -86,10 +86,10 @@ export class Help extends HelpBase { protected description(c: Command.Loadable): string { const description = this.render(c.description || '') if (c.summary) { - return description + return colorize(this.config?.theme?.sectionDescription, description) } - return description.split('\n').slice(1).join('\n') + return colorize(this.config?.theme?.sectionDescription, description.split('\n').slice(1).join('\n')) } protected formatCommand(command: Command.Loadable): string { From 7a35ca7b163ce91d03c4542fde2f370de612051d Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Tue, 7 Nov 2023 23:15:35 -0300 Subject: [PATCH 27/52] feat: add color token for aliases --- src/help/command.ts | 2 +- src/interfaces/config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/help/command.ts b/src/help/command.ts index 608f9988f..1d0b000a8 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -30,7 +30,7 @@ export class CommandHelp extends HelpFormatter { [ colorize(this.config?.theme?.dollarSign, '$'), colorize(this.config?.theme?.bin, this.config.bin), - colorize(this.config?.theme?.sectionDescription, a), + colorize(this.config?.theme?.alias, a), ].join(' '), ) .join('\n') diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 53ff1674c..c378d8ad1 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -27,6 +27,7 @@ export type VersionDetails = { } export interface Theme { + alias?: Color bin?: Color command?: Color commandSummary?: Color From 92ff5ff0ffbddb215f5ef3e57ce32fccbf47b6cc Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 00:04:37 -0300 Subject: [PATCH 28/52] fix(test): change template strings to avoid expected results printing bin twice --- src/help/command.ts | 15 +++++++++------ test/help/format-command-with-options.test.ts | 8 ++++---- test/help/format-command.test.ts | 8 ++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/help/command.ts b/src/help/command.ts index 1d0b000a8..47a1a47c5 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -312,13 +312,16 @@ export class CommandHelp extends HelpFormatter { const body = (usage ? castArray(usage) : [this.defaultUsage()]) .map((u) => { const allowedSpacing = this.opts.maxWidth - this.indentSpacing - const line = `${colorize(this.config?.theme?.dollarSign, '$')} ${colorize( - this.config?.theme?.bin, - this.config.bin, - )} ${colorize(this.config?.theme?.command, '<%= command.id %>')}${colorize( + + const dollarSign = colorize(this.config?.theme?.dollarSign, '$') + const bin = colorize(this.config?.theme?.bin, this.config.bin) + const command = colorize(this.config?.theme?.command, '<%= command.id %>') + const commandDescription = colorize( this.config?.theme?.sectionDescription, - u.replace('<%= command.id %>', ''), - )}`.trim() + u.replace('<%= command.id %>', '').trim(), + ) + + const line = `${dollarSign} ${bin} ${command} ${commandDescription}`.trim() if (line.length > allowedSpacing) { const splitIndex = line.slice(0, Math.max(0, allowedSpacing)).lastIndexOf(' ') return ( diff --git a/test/help/format-command-with-options.test.ts b/test/help/format-command-with-options.test.ts index 5e0abf7bf..24c45718a 100644 --- a/test/help/format-command-with-options.test.ts +++ b/test/help/format-command-with-options.test.ts @@ -321,24 +321,24 @@ ARGUMENTS const cmd = await makeLoadable( makeCommandClass({ id: 'apps:create', - usage: '<%= config.bin %> <%= command.id %> usage', + usage: '<%= command.id %> usage', }), ) const output = help.formatCommand(cmd) expect(output).to.equal(`USAGE - $ oclif oclif apps:create usage`) + $ oclif apps:create usage`) }) it('should output usage arrays with templates', async () => { const cmd = await makeLoadable( makeCommandClass({ id: 'apps:create', - usage: ['<%= config.bin %>', '<%= command.id %> usage'], + usage: ['<%= config.id %>', '<%= command.id %> usage'], }), ) const output = help.formatCommand(cmd) expect(output).to.equal(`USAGE - $ oclif oclif + $ oclif apps:create $ oclif apps:create usage`) }) diff --git a/test/help/format-command.test.ts b/test/help/format-command.test.ts index b9f466ae0..7365db709 100644 --- a/test/help/format-command.test.ts +++ b/test/help/format-command.test.ts @@ -524,26 +524,26 @@ ARGUMENTS const cmd = await makeLoadable( makeCommandClass({ id: 'apps:create', - usage: '<%= config.bin %> <%= command.id %> usage', + usage: '<%= command.id %> usage', }), ) const output = help.formatCommand(cmd) expect(output).to.equal(`USAGE - $ oclif oclif apps:create usage`) + $ oclif apps:create usage`) }) it('should output usage arrays with templates', async () => { const cmd = await makeLoadable( makeCommandClass({ id: 'apps:create', - usage: ['<%= config.bin %>', '<%= command.id %> usage'], + usage: ['<%= command.id %>', '<%= command.id %> usage'], }), ) const output = help.formatCommand(cmd) expect(output).to.equal(`USAGE - $ oclif oclif + $ oclif apps:create $ oclif apps:create usage`) }) From 769153584306b6227aefb42537ed33839e6a0887 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 00:06:07 -0300 Subject: [PATCH 29/52] fix(test): add a missing parenthesis back so that all options are wraped by () --- src/help/command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/help/command.ts b/src/help/command.ts index 47a1a47c5..092652ef5 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -52,7 +52,7 @@ export class CommandHelp extends HelpFormatter { if (a.default) description = `${colorize(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}` if (a.options) - description = `${colorize(this.config?.theme?.flagOptions, `(${a.options.join('|')}`)} ${description}` + description = `${colorize(this.config?.theme?.flagOptions, `(${a.options.join('|')})`)} ${description}` return [ colorize(this.config?.theme?.flag, name), description ? colorize(this.config?.theme?.sectionDescription, description) : undefined, From c252a1399377c5b80d336e08ead3367ad589bf63 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 00:07:14 -0300 Subject: [PATCH 30/52] fix(test): remove single quotes wraping default flag values --- src/help/command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/help/command.ts b/src/help/command.ts index 092652ef5..f1a587387 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -174,7 +174,7 @@ export class CommandHelp extends HelpFormatter { let right = flag.summary || flag.description || '' if (flag.type === 'option' && flag.default) { - right = `${colorize(this.config?.theme?.flagDefaultValue, `[default: '${flag.default}']`)} ${right}` + right = `${colorize(this.config?.theme?.flagDefaultValue, `[default: ${flag.default}]`)} ${right}` } if (flag.required) right = `${colorize(this.config?.theme?.flagRequired, '(required)')} ${right}` From 9f8591b7266d5553c16d036de7dc1b0b7f62bd78 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 00:11:42 -0300 Subject: [PATCH 31/52] feat: add test:debug script to ease debuging --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 57a9c1cbd..f88ece624 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "prepare": "husky install", "pretest": "yarn build && tsc -p test --noEmit --skipLibCheck", "test:circular-deps": "madge lib/ -c", + "test:debug": "nyc mocha --debug-brk --inspect \"test/**/*.test.ts\"", "test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --parallel --timeout 1200000", "test:esm-cjs": "cross-env DEBUG=e2e:* ts-node test/integration/esm-cjs.ts", "test:perf": "ts-node test/perf/parser.perf.ts", From 02fc7aaf2efda4c962012acceeb433125d57ae5b Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 12:46:22 -0300 Subject: [PATCH 32/52] refactor: add a constant with all possible THEME_KEYS --- src/interfaces/config.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index c378d8ad1..74834ed07 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -44,12 +44,30 @@ export interface Theme { version?: Color } -export function parseTheme(json: any): Theme { - const theme: Theme = {} +// TODO: find a way to create this array at build time based on the interface keys +export const THEME_KEYS = [ + 'alias', + 'bin', + 'command', + 'commandSummary', + 'dollarSign', + 'flag', + 'flagDefaultValue', + 'flagOptions', + 'flagRequired', + 'flagSeparator', + 'flagType', + 'sectionDescription', + 'sectionHeader', + 'topic', + 'version', +] - for (const prop in json) { - if (Object.prototype.hasOwnProperty.call(json, prop)) { - theme[prop as keyof Theme] = new Color(json[prop]) +export function parseTheme(untypedTheme: any): Theme { + const theme: Theme = {} + for (const prop in untypedTheme) { + if (Object.prototype.hasOwnProperty.call(untypedTheme, prop) && THEME_KEYS.includes(prop)) { + theme[prop as keyof Theme] = new Color(untypedTheme[prop]) } } From 6b365224de87aab61921585ab4d8abce1620b7af Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 12:48:30 -0300 Subject: [PATCH 33/52] test: add tests to prove parsing untyped json object with color strings work --- test/interfaces/config.test.ts | 203 +++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 test/interfaces/config.test.ts diff --git a/test/interfaces/config.test.ts b/test/interfaces/config.test.ts new file mode 100644 index 000000000..0eda883a9 --- /dev/null +++ b/test/interfaces/config.test.ts @@ -0,0 +1,203 @@ +import {expect} from 'chai' + +import {Config} from '../../src' +import {Options} from '../../src/interfaces' +import {THEME_KEYS, parseTheme} from '../../src/interfaces/config' + +describe('theme', () => { + describe('config', () => { + it('should be created with enableTheme equals to false', () => { + const config: Config = new Config({} as Options) + + expect(config.enableTheme).to.be.false + }) + }) + + describe('parsing', () => { + it('should parse untyped theme json to theme', () => { + const untypedTheme = { + alias: '#FFFFFF', + bin: '#FFFFFF', + command: '#FFFFFF', + commandSummary: '#FFFFFF', + dollarSign: '#FFFFFF', + flag: '#FFFFFF', + flagDefaultValue: '#FFFFFF', + flagOptions: '#FFFFFF', + flagRequired: '#FFFFFF', + flagSeparator: '#FFFFFF', + flagType: '#FFFFFF', + sectionDescription: '#FFFFFF', + sectionHeader: '#FFFFFF', + topic: '#FFFFFF', + version: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + for (const key of Object.keys(theme)) { + expect(THEME_KEYS.includes(key)).to.be.true + } + }) + + it('should parse alias', () => { + const untypedTheme = { + alias: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.alias?.hex()).to.equal('#FFFFFF') + }) + + it('should parse bin', () => { + const untypedTheme = { + bin: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.bin?.hex()).to.equal('#FFFFFF') + }) + + it('should parse command', () => { + const untypedTheme = { + command: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.command?.hex()).to.equal('#FFFFFF') + }) + + it('should parse commandSummary', () => { + const untypedTheme = { + commandSummary: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.commandSummary?.hex()).to.equal('#FFFFFF') + }) + + it('should parse dollarSign', () => { + const untypedTheme = { + dollarSign: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.dollarSign?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flag', () => { + const untypedTheme = { + flag: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flag?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagDefaultValue', () => { + const untypedTheme = { + flagDefaultValue: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagDefaultValue?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagOptions', () => { + const untypedTheme = { + flagOptions: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagOptions?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagRequired', () => { + const untypedTheme = { + flagRequired: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagRequired?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagSeparator', () => { + const untypedTheme = { + flagSeparator: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagSeparator?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagType', () => { + const untypedTheme = { + flagType: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagType?.hex()).to.equal('#FFFFFF') + }) + + it('should parse sectionDescription', () => { + const untypedTheme = { + sectionDescription: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.sectionDescription?.hex()).to.equal('#FFFFFF') + }) + + it('should parse sectionHeader', () => { + const untypedTheme = { + sectionHeader: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.sectionHeader?.hex()).to.equal('#FFFFFF') + }) + + it('should parse topic', () => { + const untypedTheme = { + topic: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.topic?.hex()).to.equal('#FFFFFF') + }) + + it('should parse version', () => { + const untypedTheme = { + version: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.version?.hex()).to.equal('#FFFFFF') + }) + + it('should not parse color key that is not part of Theme', () => { + const untypedTheme = { + batman: '#000000', + } + + const theme = parseTheme(untypedTheme) + + expect(Object.keys(theme).includes('batman')).to.be.false + }) + }) +}) From 66c7cde376380788e32ee048b3333333eef3553e Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 12:49:26 -0300 Subject: [PATCH 34/52] chore(package): add new scripts to ease development while writing tests --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index f88ece624..46e157e5d 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "access": "public" }, "scripts": { + "build:dev": "shx rm -rf lib && tsc --sourceMap", "build": "shx rm -rf lib && tsc", "commitlint": "commitlint", "compile": "tsc", @@ -123,6 +124,7 @@ "test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --parallel --timeout 1200000", "test:esm-cjs": "cross-env DEBUG=e2e:* ts-node test/integration/esm-cjs.ts", "test:perf": "ts-node test/perf/parser.perf.ts", + "test:dev": "nyc mocha \"test/**/*.test.ts\"", "test": "nyc mocha --forbid-only \"test/**/*.test.ts\"" }, "types": "lib/index.d.ts" From 1a6b462e92ef6a377e0e9cff00fd01571565ba1d Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 12:52:19 -0300 Subject: [PATCH 35/52] revert: remove theme from PJSON because a theme will be loaded from config_dir//theme.json --- src/interfaces/pjson.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index 85318b1c1..6c8d30971 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -1,4 +1,3 @@ -import {Theme} from './config' import {HelpOptions} from './help' export interface PJSON { @@ -45,7 +44,6 @@ export namespace PJSON { repositoryPrefix?: string schema?: number state?: 'beta' | 'deprecated' | string - theme?: Theme topicSeparator?: ' ' | ':' topics?: { [k: string]: { From 7124de1e255720a656dbb76f1a6a3fcd24e7a803 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 14:57:54 -0300 Subject: [PATCH 36/52] feat: add scopedEnvVarBoolean because scopedEnvVarTrue does not consider unset env vars --- src/config/config.ts | 8 +++++++- src/interfaces/config.ts | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index 57f4db0e2..955cfc3ca 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -336,7 +336,7 @@ export class Config implements IConfig { this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry - this.enableTheme = this.scopedEnvVarTrue('ENABLE_THEME') ?? this.pjson.oclif.enableTheme + this.enableTheme = this.scopedEnvVarBoolean('ENABLE_THEME') ?? this.pjson.oclif?.enableTheme ?? false if (this.enableTheme) { const jsonTheme = path.resolve(this.configDir, 'theme.json') if (existsSync(jsonTheme)) { @@ -600,6 +600,12 @@ export class Config implements IConfig { return process.env[this.scopedEnvVarKeys(k).find((k) => process.env[k]) as string] } + public scopedEnvVarBoolean(k: string): boolean | undefined { + const v = this.scopedEnvVar(k) + if (v === undefined) return undefined + return v === '1' || v === 'true' + } + /** * this DOES NOT account for bin aliases, use scopedEnvVarKeys instead which will account for bin aliases * @param {string} k, the unscoped key you want to get the value for diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 74834ed07..01a3baf61 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -166,6 +166,7 @@ export interface Config { s3Key(type: keyof PJSON.S3.Templates, options?: Config.s3Key.Options): string s3Url(key: string): string scopedEnvVar(key: string): string | undefined + scopedEnvVarBoolean(key: string): boolean | undefined scopedEnvVarKey(key: string): string scopedEnvVarKeys(key: string): string[] scopedEnvVarTrue(key: string): boolean From dce013d3c5799189a358fb7ee708c26dc153167d Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 14:59:39 -0300 Subject: [PATCH 37/52] feat(test): add tests to prove the behavior that enables theme --- test/config/config.test.ts | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 76cf0cb9f..610e628af 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -401,4 +401,71 @@ describe('Config', () => { }, ) }) + + describe('theme', () => { + describe('enableTheme', () => { + testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: true}}, env: {FOO_ENABLE_THEME: 'true'}}).it( + 'should be true when ENABLE_THEME is true and this.config.pjson.oclif.enableTheme is true', + (config) => { + expect(config).to.have.property('enableTheme', true) + }, + ) + + testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: true}}, env: {FOO_ENABLE_THEME: 'false'}}).it( + 'should be false when ENABLE_THEME is false and this.config.pjson.oclif.enableTheme is true', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) + + testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: true}}, env: {}}).it( + 'should be true when ENABLE_THEME is unset and this.config.pjson.oclif.enableTheme is true', + (config) => { + expect(config).to.have.property('enableTheme', true) + }, + ) + + testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: false}}, env: {FOO_ENABLE_THEME: 'true'}}).it( + 'should be true when ENABLE_THEME is true and this.config.pjson.oclif.enableTheme is false', + (config) => { + expect(config).to.have.property('enableTheme', true) + }, + ) + + testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: false}}, env: {FOO_ENABLE_THEME: 'false'}}).it( + 'should be false when ENABLE_THEME is false and this.config.pjson.oclif.enableTheme is false', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) + + testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: false}}, env: {}}).it( + 'should be false when ENABLE_THEME is unset and this.config.pjson.oclif.enableTheme is false', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) + + testConfig({pjson, env: {FOO_ENABLE_THEME: 'true'}}).it( + 'should be true when ENABLE_THEME is true and this.config.pjson.oclif.enableTheme is unset', + (config) => { + expect(config).to.have.property('enableTheme', true) + }, + ) + + testConfig({pjson, env: {FOO_ENABLE_THEME: 'false'}}).it( + 'should be false when ENABLE_THEME is false and this.config.pjson.oclif.enableTheme is unset', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) + + testConfig({pjson, env: {}}).it( + 'should be false when ENABLE_THEME is unset and this.config.pjson.oclif.enableTheme is unset', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) + }) + }) }) From 28345174682f5dfdaedf2933e22a33f29b37e824 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 15:09:10 -0300 Subject: [PATCH 38/52] refactor: replace all scopedEnvVarTrue by scopedEnvVarBoolean, which considers unset env variables --- src/config/config.ts | 7 +------ src/interfaces/config.ts | 1 - test/config/config.test.ts | 16 ++++++++-------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 955cfc3ca..56d76f4af 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -629,11 +629,6 @@ export class Config implements IConfig { .map((alias) => [alias.replaceAll('@', '').replaceAll(/[/-]/g, '_'), k].join('_').toUpperCase()) } - public scopedEnvVarTrue(k: string): boolean { - const v = process.env[this.scopedEnvVarKeys(k).find((k) => process.env[k]) as string] - return v === '1' || v === 'true' - } - protected warn(err: {detail: string; name: string} | Error | string, scope?: string): void { if (this.warned) return @@ -685,7 +680,7 @@ export class Config implements IConfig { } protected _debug(): number { - if (this.scopedEnvVarTrue('DEBUG')) return 1 + if (this.scopedEnvVarBoolean('DEBUG')) return 1 try { const {enabled} = require('debug')(this.bin) if (enabled) return 1 diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 01a3baf61..45677577a 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -169,7 +169,6 @@ export interface Config { scopedEnvVarBoolean(key: string): boolean | undefined scopedEnvVarKey(key: string): string scopedEnvVarKeys(key: string): string[] - scopedEnvVarTrue(key: string): boolean /** * active shell */ diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 610e628af..fe77c6294 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -125,27 +125,27 @@ describe('Config', () => { expect(config.scopedEnvVarKey('abc')).to.equal('FOO_ABC') }) - testConfig({pjson}).it('will get scopedEnvVarTrue', (config) => { + testConfig({pjson}).it('will get scopedEnvVarBoolean', (config) => { process.env.FOO_ABC = 'true' - expect(config.scopedEnvVarTrue('abc')).to.equal(true) + expect(config.scopedEnvVarBoolean('abc')).to.equal(true) delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue via alias', (config) => { + testConfig({pjson}).it('will get scopedEnvVarBoolean via alias', (config) => { process.env.BAR_ABC = 'true' - expect(config.scopedEnvVarTrue('abc')).to.equal(true) + expect(config.scopedEnvVarBoolean('abc')).to.equal(true) delete process.env.BAR_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue=1', (config) => { + testConfig({pjson}).it('will get scopedEnvVarBoolean=1', (config) => { process.env.FOO_ABC = '1' - expect(config.scopedEnvVarTrue('abc')).to.equal(true) + expect(config.scopedEnvVarBoolean('abc')).to.equal(true) delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue=1 via alias', (config) => { + testConfig({pjson}).it('will get scopedEnvVarBoolean=1 via alias', (config) => { process.env.BAR_ABC = '1' - expect(config.scopedEnvVarTrue('abc')).to.equal(true) + expect(config.scopedEnvVarBoolean('abc')).to.equal(true) delete process.env.BAR_ABC }) }) From c8d671de93ca0a7d20290f14cf17dbbe572a174c Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 19:25:56 -0300 Subject: [PATCH 39/52] test: add tests to prove the enableTheme behavior --- src/config/config.ts | 16 +++--- test/config/config.test.ts | 101 +++++++++++++++---------------------- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 56d76f4af..edd761c3f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,7 +1,7 @@ import * as ejs from 'ejs' import WSL from 'is-wsl' import {arch, userInfo as osUserInfo, release, tmpdir, type} from 'node:os' -import path, {join, sep} from 'node:path' +import {join, resolve, sep} from 'node:path' import {URL, fileURLToPath} from 'node:url' import {ux} from '../cli-ux' @@ -22,7 +22,7 @@ import {Plugin as IPlugin, Options} from '../interfaces/plugin' import {loadWithData} from '../module-loader' import {OCLIF_MARKER_OWNER, Performance} from '../performance' import {settings} from '../settings' -import {existsSync, readJsonSync, requireJson} from '../util/fs' +import {requireJson, safeReadJson} from '../util/fs' import {getHomeDir, getPlatform} from '../util/os' import {compact, isProd} from '../util/util' import Cache from './cache' @@ -336,13 +336,10 @@ export class Config implements IConfig { this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry - this.enableTheme = this.scopedEnvVarBoolean('ENABLE_THEME') ?? this.pjson.oclif?.enableTheme ?? false - if (this.enableTheme) { - const jsonTheme = path.resolve(this.configDir, 'theme.json') - if (existsSync(jsonTheme)) { - this.theme = parseTheme(readJsonSync(jsonTheme)) - } - } + const themeFilePath = resolve(this.configDir, 'theme.json') + const theme = this.scopedEnvVarBoolean('DISABLE_THEME') ? undefined : await safeReadJson(themeFilePath) + this.enableTheme = Boolean(theme) + if (this.enableTheme) this.theme = parseTheme(theme) this.pjson.oclif.update = this.pjson.oclif.update || {} this.pjson.oclif.update.node = this.pjson.oclif.update.node || {} @@ -602,6 +599,7 @@ export class Config implements IConfig { public scopedEnvVarBoolean(k: string): boolean | undefined { const v = this.scopedEnvVar(k) + // we might want to do something when env variable is unset but not false if (v === undefined) return undefined return v === '1' || v === 'true' } diff --git a/test/config/config.test.ts b/test/config/config.test.ts index fe77c6294..2559ba4d5 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -45,12 +45,14 @@ const pjson = { } describe('Config', () => { - const testConfig = ({pjson, homedir = '/my/home', platform = 'darwin', env = {}}: Options = {}) => { + const testConfig = ({pjson, homedir = '/my/home', platform = 'darwin', env = {}}: Options = {}, theme?: any) => { let test = fancy .resetConfig() .env(env, {clear: true}) .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir))) .stub(os, 'getPlatform', (stub) => stub.returns(platform)) + + if (theme) test = test.stub(fs, 'safeReadJson', (stub) => stub.resolves(theme)) if (pjson) test = test.stub(fs, 'readJson', (stub) => stub.resolves(pjson)) test = test.add('config', () => Config.load()) @@ -402,70 +404,47 @@ describe('Config', () => { ) }) - describe('theme', () => { - describe('enableTheme', () => { - testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: true}}, env: {FOO_ENABLE_THEME: 'true'}}).it( - 'should be true when ENABLE_THEME is true and this.config.pjson.oclif.enableTheme is true', - (config) => { - expect(config).to.have.property('enableTheme', true) - }, - ) - - testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: true}}, env: {FOO_ENABLE_THEME: 'false'}}).it( - 'should be false when ENABLE_THEME is false and this.config.pjson.oclif.enableTheme is true', - (config) => { - expect(config).to.have.property('enableTheme', false) - }, - ) - - testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: true}}, env: {}}).it( - 'should be true when ENABLE_THEME is unset and this.config.pjson.oclif.enableTheme is true', - (config) => { - expect(config).to.have.property('enableTheme', true) - }, - ) - - testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: false}}, env: {FOO_ENABLE_THEME: 'true'}}).it( - 'should be true when ENABLE_THEME is true and this.config.pjson.oclif.enableTheme is false', - (config) => { - expect(config).to.have.property('enableTheme', true) - }, - ) + describe('enableTheme', () => { + testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}, {bin: 'red'}).it( + 'should be false when DISABLE_THEME is true and theme.json exists', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) - testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: false}}, env: {FOO_ENABLE_THEME: 'false'}}).it( - 'should be false when ENABLE_THEME is false and this.config.pjson.oclif.enableTheme is false', - (config) => { - expect(config).to.have.property('enableTheme', false) - }, - ) + testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}, {bin: 'red'}).it( + 'should be true when DISABLE_THEME is false and theme.json exists', + (config) => { + expect(config).to.have.property('enableTheme', true) + }, + ) - testConfig({pjson: {...pjson, oclif: {...pjson.oclif, enableTheme: false}}, env: {}}).it( - 'should be false when ENABLE_THEME is unset and this.config.pjson.oclif.enableTheme is false', - (config) => { - expect(config).to.have.property('enableTheme', false) - }, - ) + testConfig({pjson, env: {}}, {bin: 'red'}).it( + 'should be true when DISABLE_THEME is unset and theme.json exists', + (config) => { + expect(config).to.have.property('enableTheme', true) + }, + ) - testConfig({pjson, env: {FOO_ENABLE_THEME: 'true'}}).it( - 'should be true when ENABLE_THEME is true and this.config.pjson.oclif.enableTheme is unset', - (config) => { - expect(config).to.have.property('enableTheme', true) - }, - ) + testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}).it( + 'should be false when DISABLE_THEME is true and theme.json does not exist', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) - testConfig({pjson, env: {FOO_ENABLE_THEME: 'false'}}).it( - 'should be false when ENABLE_THEME is false and this.config.pjson.oclif.enableTheme is unset', - (config) => { - expect(config).to.have.property('enableTheme', false) - }, - ) + testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}).it( + 'should be false when DISABLE_THEME is false and theme.json does not exist', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) - testConfig({pjson, env: {}}).it( - 'should be false when ENABLE_THEME is unset and this.config.pjson.oclif.enableTheme is unset', - (config) => { - expect(config).to.have.property('enableTheme', false) - }, - ) - }) + testConfig({pjson, env: {}}).it( + 'should be false when DISABLE_THEME is unset and theme.json does not exist', + (config) => { + expect(config).to.have.property('enableTheme', false) + }, + ) }) }) From 24ca8f5dc8bd4cfc1ef89b51a48719aab9fadc13 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 23:37:16 -0300 Subject: [PATCH 40/52] refactor: simplify parseTheme method --- src/config/config.ts | 2 +- src/interfaces/config.ts | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index edd761c3f..6389a80ca 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -339,7 +339,7 @@ export class Config implements IConfig { const themeFilePath = resolve(this.configDir, 'theme.json') const theme = this.scopedEnvVarBoolean('DISABLE_THEME') ? undefined : await safeReadJson(themeFilePath) this.enableTheme = Boolean(theme) - if (this.enableTheme) this.theme = parseTheme(theme) + if (this.enableTheme) this.theme = parseTheme(theme as Record) this.pjson.oclif.update = this.pjson.oclif.update || {} this.pjson.oclif.update.node = this.pjson.oclif.update.node || {} diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 45677577a..d0c1f088d 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -63,15 +63,12 @@ export const THEME_KEYS = [ 'version', ] -export function parseTheme(untypedTheme: any): Theme { - const theme: Theme = {} - for (const prop in untypedTheme) { - if (Object.prototype.hasOwnProperty.call(untypedTheme, prop) && THEME_KEYS.includes(prop)) { - theme[prop as keyof Theme] = new Color(untypedTheme[prop]) - } - } - - return theme +export function parseTheme(untypedTheme: Record): Theme { + return Object.fromEntries( + Object.entries(untypedTheme) + .filter(([key]) => THEME_KEYS.includes(key)) + .map(([key, value]) => [key, new Color(value)]), + ) } export interface Config { From 84db405c20a29265a1925a269a2c0843b7e2fccf Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 23:40:22 -0300 Subject: [PATCH 41/52] chore(package): remove localhost:4873 from lock file --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index d4240b431..2845fdfb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1014,7 +1014,7 @@ "@types/color-convert@*": version "2.0.2" - resolved "http://localhost:4873/@types/color-convert/-/color-convert-2.0.2.tgz#a5fa5da9b866732f8bf86b01964869011e2a2356" + resolved "http://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.2.tgz#a5fa5da9b866732f8bf86b01964869011e2a2356" integrity sha512-KGRIgCxwcgazts4MXRCikPbIMzBpjfdgEZSy8TRHU/gtg+f9sOfHdtK8unPfxIoBtyd2aTTwINVLSNENlC8U8A== dependencies: "@types/color-name" "*" @@ -1026,7 +1026,7 @@ "@types/color@^3.0.5": version "3.0.5" - resolved "http://localhost:4873/@types/color/-/color-3.0.5.tgz#658fd9286a44c21dabaa56c2e2f63da3ac15f063" + resolved "http://registry.yarnpkg.com/@types/color/-/color-3.0.5.tgz#658fd9286a44c21dabaa56c2e2f63da3ac15f063" integrity sha512-T9yHCNtd8ap9L/r8KEESu5RDMLkoWXHo7dTureNoI1dbp25NsCN054vOu09iniIjR21MXUL+LU9bkIWrbyg8gg== dependencies: "@types/color-convert" "*" @@ -2112,7 +2112,7 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: color-string@^1.9.0: version "1.9.1" - resolved "http://localhost:4873/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + resolved "http://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== dependencies: color-name "^1.0.0" @@ -2125,7 +2125,7 @@ color-support@^1.1.3: color@^4.2.3: version "4.2.3" - resolved "http://localhost:4873/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + resolved "http://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: color-convert "^2.0.1" @@ -3876,7 +3876,7 @@ is-arrayish@^0.2.1: is-arrayish@^0.3.1: version "0.3.2" - resolved "http://localhost:4873/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + resolved "http://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-bigint@^1.0.1: @@ -6319,7 +6319,7 @@ sigstore@^1.3.0, sigstore@^1.4.0, sigstore@^1.7.0: simple-swizzle@^0.2.2: version "0.2.2" - resolved "http://localhost:4873/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + resolved "http://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== dependencies: is-arrayish "^0.3.1" From 4927e47df29aa7c3b92f8d85cb5de17d11a96f37 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 23:42:55 -0300 Subject: [PATCH 42/52] revert: rever scopedEnvVarBoolean back to scopedEnvVarTrue --- src/config/config.ts | 16 +++++++--------- src/interfaces/config.ts | 2 +- test/config/config.test.ts | 16 ++++++++-------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 6389a80ca..b9b59c9ba 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -337,7 +337,7 @@ export class Config implements IConfig { this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry const themeFilePath = resolve(this.configDir, 'theme.json') - const theme = this.scopedEnvVarBoolean('DISABLE_THEME') ? undefined : await safeReadJson(themeFilePath) + const theme = this.scopedEnvVarTrue('DISABLE_THEME') ? undefined : await safeReadJson(themeFilePath) this.enableTheme = Boolean(theme) if (this.enableTheme) this.theme = parseTheme(theme as Record) @@ -597,13 +597,6 @@ export class Config implements IConfig { return process.env[this.scopedEnvVarKeys(k).find((k) => process.env[k]) as string] } - public scopedEnvVarBoolean(k: string): boolean | undefined { - const v = this.scopedEnvVar(k) - // we might want to do something when env variable is unset but not false - if (v === undefined) return undefined - return v === '1' || v === 'true' - } - /** * this DOES NOT account for bin aliases, use scopedEnvVarKeys instead which will account for bin aliases * @param {string} k, the unscoped key you want to get the value for @@ -627,6 +620,11 @@ export class Config implements IConfig { .map((alias) => [alias.replaceAll('@', '').replaceAll(/[/-]/g, '_'), k].join('_').toUpperCase()) } + public scopedEnvVarTrue(k: string): boolean | undefined { + const v = this.scopedEnvVar(k) + return v === '1' || v === 'true' + } + protected warn(err: {detail: string; name: string} | Error | string, scope?: string): void { if (this.warned) return @@ -678,7 +676,7 @@ export class Config implements IConfig { } protected _debug(): number { - if (this.scopedEnvVarBoolean('DEBUG')) return 1 + if (this.scopedEnvVarTrue('DEBUG')) return 1 try { const {enabled} = require('debug')(this.bin) if (enabled) return 1 diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index d0c1f088d..319996da3 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -163,9 +163,9 @@ export interface Config { s3Key(type: keyof PJSON.S3.Templates, options?: Config.s3Key.Options): string s3Url(key: string): string scopedEnvVar(key: string): string | undefined - scopedEnvVarBoolean(key: string): boolean | undefined scopedEnvVarKey(key: string): string scopedEnvVarKeys(key: string): string[] + scopedEnvVarTrue(key: string): boolean | undefined /** * active shell */ diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 2559ba4d5..52e18f23a 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -127,27 +127,27 @@ describe('Config', () => { expect(config.scopedEnvVarKey('abc')).to.equal('FOO_ABC') }) - testConfig({pjson}).it('will get scopedEnvVarBoolean', (config) => { + testConfig({pjson}).it('will get scopedEnvVarTrue', (config) => { process.env.FOO_ABC = 'true' - expect(config.scopedEnvVarBoolean('abc')).to.equal(true) + expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarBoolean via alias', (config) => { + testConfig({pjson}).it('will get scopedEnvVarTrue via alias', (config) => { process.env.BAR_ABC = 'true' - expect(config.scopedEnvVarBoolean('abc')).to.equal(true) + expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.BAR_ABC }) - testConfig({pjson}).it('will get scopedEnvVarBoolean=1', (config) => { + testConfig({pjson}).it('will get scopedEnvVarTrue=1', (config) => { process.env.FOO_ABC = '1' - expect(config.scopedEnvVarBoolean('abc')).to.equal(true) + expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarBoolean=1 via alias', (config) => { + testConfig({pjson}).it('will get scopedEnvVarTrue=1 via alias', (config) => { process.env.BAR_ABC = '1' - expect(config.scopedEnvVarBoolean('abc')).to.equal(true) + expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.BAR_ABC }) }) From c7488bb68a1005a3a0b313ec26ad4c0e18289f2c Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Wed, 8 Nov 2023 23:49:27 -0300 Subject: [PATCH 43/52] test: add tests to prove when this.theme is set --- test/config/config.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 52e18f23a..1a854ec56 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -447,4 +447,20 @@ describe('Config', () => { }, ) }) + + describe('theme', () => { + testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}, {bin: 'red'}).it( + 'should be set if this.enableTheme is true', + (config) => { + expect(config.theme).to.be.not.undefined + }, + ) + + testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}, {bin: 'red'}).it( + 'should not be set if this.enableTheme is false', + (config) => { + expect(config.theme).to.be.undefined + }, + ) + }) }) From 37588cedbc23de6275e98fef5eb3570862eae32e Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 00:16:24 -0300 Subject: [PATCH 44/52] feat: ensure colorize returns string only --- src/help/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/help/util.ts b/src/help/util.ts index 9a13bed15..1b5d23d39 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -110,6 +110,6 @@ export function normalizeArgv(config: IConfig, argv = process.argv.slice(2)): st return argv } -export function colorize(color: Color | undefined, text: string) { +export function colorize(color: Color | undefined, text: string): string { return color ? chalk.hex(color.hex())(text) : text } From 714d9898cf87c802b1b3c9e6c7bb59e3aecc2f54 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 00:16:55 -0300 Subject: [PATCH 45/52] test: add tests to prove colorize works as expected --- test/help/util.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/help/util.test.ts b/test/help/util.test.ts index d2a27e19a..9adb47cd1 100644 --- a/test/help/util.test.ts +++ b/test/help/util.test.ts @@ -1,10 +1,13 @@ import {test} from '@oclif/test' import {expect} from 'chai' +import chalk from 'chalk' +import Color from 'color' import {resolve} from 'node:path' import {Config, Interfaces} from '../../src' import * as util from '../../src/config/util' import {loadHelpClass, standardizeIDFromArgv} from '../../src/help' +import {colorize} from '../../src/help/util' import configuredHelpClass from './_test-help-class' describe('util', () => { @@ -183,4 +186,28 @@ describe('util', () => { }, ) }) + + describe('colorize', () => { + const color = new Color('red') + + it('should return text with ansi characters when given color', () => { + const text = colorize(color, 'brazil') + expect(text).to.equal(chalk.hex(color.hex())('brazil')) + }) + + it('should return text without ansi characters when given undefined', () => { + const text = colorize(undefined, 'brazil') + expect(text).to.equal('brazil') + }) + + it('should return empty text without ansi characters when given color', () => { + const text = colorize(color, '') + expect(text).to.equal('') + }) + + it('should return empty text without ansi characters when given undefined', () => { + const text = colorize(undefined, '') + expect(text).to.equal('') + }) + }) }) From fa1f414f23c207f223185a3c856a331a768dc571 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 14:06:12 -0300 Subject: [PATCH 46/52] revert: rollback scopedEnvVarTrue as it was before --- src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index b9b59c9ba..02981f334 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -620,7 +620,7 @@ export class Config implements IConfig { .map((alias) => [alias.replaceAll('@', '').replaceAll(/[/-]/g, '_'), k].join('_').toUpperCase()) } - public scopedEnvVarTrue(k: string): boolean | undefined { + public scopedEnvVarTrue(k: string): boolean { const v = this.scopedEnvVar(k) return v === '1' || v === 'true' } From 2c9da22a7f6c932e52892367211b3caff0a16fff Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 14:12:30 -0300 Subject: [PATCH 47/52] refactor: move parseTheme to src/util/util.js --- src/config/config.ts | 12 +- src/interfaces/config.ts | 8 -- src/util/util.ts | 12 ++ test/interfaces/config.test.ts | 189 ------------------------------- test/util/util.test.ts | 201 ++++++++++++++++++++++++++++++++- 5 files changed, 214 insertions(+), 208 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 02981f334..d97f8610e 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -9,22 +9,14 @@ import {Command} from '../command' import {CLIError, error, exit, warn} from '../errors' import {getHelpFlagAdditions} from '../help/util' import {Hook, Hooks, PJSON, Topic} from '../interfaces' -import { - ArchTypes, - Config as IConfig, - LoadOptions, - PlatformTypes, - Theme, - VersionDetails, - parseTheme, -} from '../interfaces/config' +import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, Theme, VersionDetails} from '../interfaces/config' import {Plugin as IPlugin, Options} from '../interfaces/plugin' import {loadWithData} from '../module-loader' import {OCLIF_MARKER_OWNER, Performance} from '../performance' import {settings} from '../settings' import {requireJson, safeReadJson} from '../util/fs' import {getHomeDir, getPlatform} from '../util/os' -import {compact, isProd} from '../util/util' +import {compact, isProd, parseTheme} from '../util/util' import Cache from './cache' import PluginLoader from './plugin-loader' import {tsPath} from './ts-node' diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 319996da3..df2097cfa 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -63,14 +63,6 @@ export const THEME_KEYS = [ 'version', ] -export function parseTheme(untypedTheme: Record): Theme { - return Object.fromEntries( - Object.entries(untypedTheme) - .filter(([key]) => THEME_KEYS.includes(key)) - .map(([key, value]) => [key, new Color(value)]), - ) -} - export interface Config { /** * process.arch diff --git a/src/util/util.ts b/src/util/util.ts index 6b3048a27..18df7b6ea 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,3 +1,7 @@ +import Color from 'color' + +import {THEME_KEYS, Theme} from '../interfaces/config' + export function pickBy>( obj: T, fn: (i: T[keyof T]) => boolean, @@ -105,3 +109,11 @@ function get(obj: Record, path: string): unknown { export function mergeNestedObjects(objs: Record[], path: string): Record { return Object.fromEntries(objs.flatMap((o) => Object.entries(get(o, path) ?? {})).reverse()) } + +export function parseTheme(untypedTheme: Record): Theme { + return Object.fromEntries( + Object.entries(untypedTheme) + .filter(([key]) => THEME_KEYS.includes(key)) + .map(([key, value]) => [key, new Color(value)]), + ) +} diff --git a/test/interfaces/config.test.ts b/test/interfaces/config.test.ts index 0eda883a9..73bcf8e59 100644 --- a/test/interfaces/config.test.ts +++ b/test/interfaces/config.test.ts @@ -2,7 +2,6 @@ import {expect} from 'chai' import {Config} from '../../src' import {Options} from '../../src/interfaces' -import {THEME_KEYS, parseTheme} from '../../src/interfaces/config' describe('theme', () => { describe('config', () => { @@ -12,192 +11,4 @@ describe('theme', () => { expect(config.enableTheme).to.be.false }) }) - - describe('parsing', () => { - it('should parse untyped theme json to theme', () => { - const untypedTheme = { - alias: '#FFFFFF', - bin: '#FFFFFF', - command: '#FFFFFF', - commandSummary: '#FFFFFF', - dollarSign: '#FFFFFF', - flag: '#FFFFFF', - flagDefaultValue: '#FFFFFF', - flagOptions: '#FFFFFF', - flagRequired: '#FFFFFF', - flagSeparator: '#FFFFFF', - flagType: '#FFFFFF', - sectionDescription: '#FFFFFF', - sectionHeader: '#FFFFFF', - topic: '#FFFFFF', - version: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - for (const key of Object.keys(theme)) { - expect(THEME_KEYS.includes(key)).to.be.true - } - }) - - it('should parse alias', () => { - const untypedTheme = { - alias: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.alias?.hex()).to.equal('#FFFFFF') - }) - - it('should parse bin', () => { - const untypedTheme = { - bin: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.bin?.hex()).to.equal('#FFFFFF') - }) - - it('should parse command', () => { - const untypedTheme = { - command: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.command?.hex()).to.equal('#FFFFFF') - }) - - it('should parse commandSummary', () => { - const untypedTheme = { - commandSummary: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.commandSummary?.hex()).to.equal('#FFFFFF') - }) - - it('should parse dollarSign', () => { - const untypedTheme = { - dollarSign: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.dollarSign?.hex()).to.equal('#FFFFFF') - }) - - it('should parse flag', () => { - const untypedTheme = { - flag: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.flag?.hex()).to.equal('#FFFFFF') - }) - - it('should parse flagDefaultValue', () => { - const untypedTheme = { - flagDefaultValue: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.flagDefaultValue?.hex()).to.equal('#FFFFFF') - }) - - it('should parse flagOptions', () => { - const untypedTheme = { - flagOptions: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.flagOptions?.hex()).to.equal('#FFFFFF') - }) - - it('should parse flagRequired', () => { - const untypedTheme = { - flagRequired: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.flagRequired?.hex()).to.equal('#FFFFFF') - }) - - it('should parse flagSeparator', () => { - const untypedTheme = { - flagSeparator: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.flagSeparator?.hex()).to.equal('#FFFFFF') - }) - - it('should parse flagType', () => { - const untypedTheme = { - flagType: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.flagType?.hex()).to.equal('#FFFFFF') - }) - - it('should parse sectionDescription', () => { - const untypedTheme = { - sectionDescription: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.sectionDescription?.hex()).to.equal('#FFFFFF') - }) - - it('should parse sectionHeader', () => { - const untypedTheme = { - sectionHeader: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.sectionHeader?.hex()).to.equal('#FFFFFF') - }) - - it('should parse topic', () => { - const untypedTheme = { - topic: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.topic?.hex()).to.equal('#FFFFFF') - }) - - it('should parse version', () => { - const untypedTheme = { - version: '#FFFFFF', - } - - const theme = parseTheme(untypedTheme) - - expect(theme.version?.hex()).to.equal('#FFFFFF') - }) - - it('should not parse color key that is not part of Theme', () => { - const untypedTheme = { - batman: '#000000', - } - - const theme = parseTheme(untypedTheme) - - expect(Object.keys(theme).includes('batman')).to.be.false - }) - }) }) diff --git a/test/util/util.test.ts b/test/util/util.test.ts index d2c00b57b..42c318ab7 100644 --- a/test/util/util.test.ts +++ b/test/util/util.test.ts @@ -1,6 +1,17 @@ import {expect} from 'chai' -import {capitalize, castArray, isNotFalsy, isTruthy, last, maxBy, mergeNestedObjects, sumBy} from '../../src/util/util' +import {THEME_KEYS} from '../../src/interfaces/config' +import { + capitalize, + castArray, + isNotFalsy, + isTruthy, + last, + maxBy, + mergeNestedObjects, + parseTheme, + sumBy, +} from '../../src/util/util' describe('capitalize', () => { it('capitalizes the string', () => { @@ -128,3 +139,191 @@ describe('mergeNestedObjects', () => { }) }) }) + +describe('theme parsing', () => { + it('should parse untyped theme json to theme', () => { + const untypedTheme = { + alias: '#FFFFFF', + bin: '#FFFFFF', + command: '#FFFFFF', + commandSummary: '#FFFFFF', + dollarSign: '#FFFFFF', + flag: '#FFFFFF', + flagDefaultValue: '#FFFFFF', + flagOptions: '#FFFFFF', + flagRequired: '#FFFFFF', + flagSeparator: '#FFFFFF', + flagType: '#FFFFFF', + sectionDescription: '#FFFFFF', + sectionHeader: '#FFFFFF', + topic: '#FFFFFF', + version: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + for (const key of Object.keys(theme)) { + expect(THEME_KEYS.includes(key)).to.be.true + } + }) + + it('should parse alias', () => { + const untypedTheme = { + alias: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.alias?.hex()).to.equal('#FFFFFF') + }) + + it('should parse bin', () => { + const untypedTheme = { + bin: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.bin?.hex()).to.equal('#FFFFFF') + }) + + it('should parse command', () => { + const untypedTheme = { + command: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.command?.hex()).to.equal('#FFFFFF') + }) + + it('should parse commandSummary', () => { + const untypedTheme = { + commandSummary: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.commandSummary?.hex()).to.equal('#FFFFFF') + }) + + it('should parse dollarSign', () => { + const untypedTheme = { + dollarSign: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.dollarSign?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flag', () => { + const untypedTheme = { + flag: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flag?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagDefaultValue', () => { + const untypedTheme = { + flagDefaultValue: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagDefaultValue?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagOptions', () => { + const untypedTheme = { + flagOptions: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagOptions?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagRequired', () => { + const untypedTheme = { + flagRequired: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagRequired?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagSeparator', () => { + const untypedTheme = { + flagSeparator: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagSeparator?.hex()).to.equal('#FFFFFF') + }) + + it('should parse flagType', () => { + const untypedTheme = { + flagType: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.flagType?.hex()).to.equal('#FFFFFF') + }) + + it('should parse sectionDescription', () => { + const untypedTheme = { + sectionDescription: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.sectionDescription?.hex()).to.equal('#FFFFFF') + }) + + it('should parse sectionHeader', () => { + const untypedTheme = { + sectionHeader: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.sectionHeader?.hex()).to.equal('#FFFFFF') + }) + + it('should parse topic', () => { + const untypedTheme = { + topic: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.topic?.hex()).to.equal('#FFFFFF') + }) + + it('should parse version', () => { + const untypedTheme = { + version: '#FFFFFF', + } + + const theme = parseTheme(untypedTheme) + + expect(theme.version?.hex()).to.equal('#FFFFFF') + }) + + it('should not parse color key that is not part of Theme', () => { + const untypedTheme = { + batman: '#000000', + } + + const theme = parseTheme(untypedTheme) + + expect(Object.keys(theme).includes('batman')).to.be.false + }) +}) From 49a144662d9b6caaef0058b4bea765d7d00bca57 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 14:24:56 -0300 Subject: [PATCH 48/52] refactor: make Theme type dinamically based on the values of THEME_KEYS at runtime --- src/interfaces/config.ts | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index df2097cfa..8c639a070 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -26,25 +26,6 @@ export type VersionDetails = { shell?: string } -export interface Theme { - alias?: Color - bin?: Color - command?: Color - commandSummary?: Color - dollarSign?: Color - flag?: Color - flagDefaultValue?: Color - flagOptions?: Color - flagRequired?: Color - flagSeparator?: Color - flagType?: Color - sectionDescription?: Color - sectionHeader?: Color - topic?: Color - version?: Color -} - -// TODO: find a way to create this array at build time based on the interface keys export const THEME_KEYS = [ 'alias', 'bin', @@ -63,6 +44,8 @@ export const THEME_KEYS = [ 'version', ] +export type Theme = Record + export interface Config { /** * process.arch From bc369b3e87c2cc931f3148afe5d0be192247cac2 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 14:47:23 -0300 Subject: [PATCH 49/52] fix: err TS1259: Module /@types/color/index can only be default-imported using 'esModuleInterop' --- src/help/util.ts | 2 +- src/interfaces/config.ts | 2 +- src/util/util.ts | 9 +++++++-- test/help/util.test.ts | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/help/util.ts b/src/help/util.ts index 1b5d23d39..12b5f0dbf 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -1,5 +1,5 @@ import chalk from 'chalk' -import Color from 'color' +import * as Color from 'color' import * as ejs from 'ejs' import {collectUsableIds} from '../config/util' diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 8c639a070..bc948f417 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -1,4 +1,4 @@ -import Color from 'color' +import * as Color from 'color' import {Command} from '../command' import {Hook, Hooks} from './hooks' diff --git a/src/util/util.ts b/src/util/util.ts index 18df7b6ea..4849f6128 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,4 +1,4 @@ -import Color from 'color' +import * as Color from 'color' import {THEME_KEYS, Theme} from '../interfaces/config' @@ -114,6 +114,11 @@ export function parseTheme(untypedTheme: Record): Theme { return Object.fromEntries( Object.entries(untypedTheme) .filter(([key]) => THEME_KEYS.includes(key)) - .map(([key, value]) => [key, new Color(value)]), + .map(([key, value]) => [key, getColor(value)]), ) } + +export function getColor(color: string) { + // eslint-disable-next-line new-cap + return new Color.default(color) +} diff --git a/test/help/util.test.ts b/test/help/util.test.ts index 9adb47cd1..8cb233822 100644 --- a/test/help/util.test.ts +++ b/test/help/util.test.ts @@ -1,13 +1,13 @@ import {test} from '@oclif/test' import {expect} from 'chai' import chalk from 'chalk' -import Color from 'color' import {resolve} from 'node:path' import {Config, Interfaces} from '../../src' import * as util from '../../src/config/util' import {loadHelpClass, standardizeIDFromArgv} from '../../src/help' import {colorize} from '../../src/help/util' +import {getColor} from '../../src/util/util' import configuredHelpClass from './_test-help-class' describe('util', () => { @@ -188,7 +188,7 @@ describe('util', () => { }) describe('colorize', () => { - const color = new Color('red') + const color = getColor('red') it('should return text with ansi characters when given color', () => { const text = colorize(color, 'brazil') From f9dbcbbaa66e56bd907f541bca6ba5d85cb92c84 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 15:26:52 -0300 Subject: [PATCH 50/52] revert: remove enableTheme from pjson because we don't want to cli devs to force users to use theme --- src/interfaces/pjson.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index 6c8d30971..3f82f5fd4 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -25,7 +25,6 @@ export namespace PJSON { default?: string description?: string devPlugins?: string[] - enableTheme: boolean flexibleTaxonomy?: boolean helpClass?: string helpOptions?: HelpOptions From caaa35348f9dae8977004d4cbb8ccae494fe99e1 Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 15:27:53 -0300 Subject: [PATCH 51/52] revert: remove undefined as a return type for scopedEnvVarTrue --- src/interfaces/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index bc948f417..1f3196372 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -140,7 +140,7 @@ export interface Config { scopedEnvVar(key: string): string | undefined scopedEnvVarKey(key: string): string scopedEnvVarKeys(key: string): string[] - scopedEnvVarTrue(key: string): boolean | undefined + scopedEnvVarTrue(key: string): boolean /** * active shell */ From 00a6ccdde08f13b44eec55b57a6d6e584f02d9fd Mon Sep 17 00:00:00 2001 From: Allan Oricil Date: Thu, 9 Nov 2023 16:01:48 -0300 Subject: [PATCH 52/52] refactor: remove config.enableTheme --- src/config/config.ts | 12 +++++----- src/interfaces/config.ts | 3 +-- test/config/config.test.ts | 42 +++++++++++----------------------- test/interfaces/config.test.ts | 14 ------------ 4 files changed, 20 insertions(+), 51 deletions(-) delete mode 100644 test/interfaces/config.test.ts diff --git a/src/config/config.ts b/src/config/config.ts index d97f8610e..9eed4ec40 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -80,7 +80,6 @@ export class Config implements IConfig { public dataDir!: string public debug = 0 public dirname!: string - public enableTheme: boolean = false public errlog!: string public flexibleTaxonomy!: boolean public home!: string @@ -92,7 +91,7 @@ export class Config implements IConfig { public plugins: Map = new Map() public root!: string public shell!: string - public theme!: Theme + public theme?: Theme public topicSeparator: ' ' | ':' = ':' public userAgent!: string public userPJSON?: PJSON.User @@ -328,10 +327,11 @@ export class Config implements IConfig { this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry - const themeFilePath = resolve(this.configDir, 'theme.json') - const theme = this.scopedEnvVarTrue('DISABLE_THEME') ? undefined : await safeReadJson(themeFilePath) - this.enableTheme = Boolean(theme) - if (this.enableTheme) this.theme = parseTheme(theme as Record) + if (!this.scopedEnvVarTrue('DISABLE_THEME')) { + const themeFilePath = resolve(this.configDir, 'theme.json') + const theme = await safeReadJson>(themeFilePath) + this.theme = theme ? parseTheme(theme) : undefined + } this.pjson.oclif.update = this.pjson.oclif.update || {} this.pjson.oclif.update.node = this.pjson.oclif.update.node || {} diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 1f3196372..e2bae560c 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -91,7 +91,6 @@ export interface Config { * base dirname to use in cacheDir/configDir/dataDir */ readonly dirname: string - enableTheme: boolean /** * points to a file that should be appended to for error logs * @@ -145,7 +144,7 @@ export interface Config { * active shell */ readonly shell: string - readonly theme: Theme + readonly theme?: Theme topicSeparator: ' ' | ':' readonly topics: Topic[] /** diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 1a854ec56..26378a590 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -404,62 +404,46 @@ describe('Config', () => { ) }) - describe('enableTheme', () => { + describe('theme', () => { testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}, {bin: 'red'}).it( - 'should be false when DISABLE_THEME is true and theme.json exists', + 'should not be set when DISABLE_THEME is true and theme.json exists', (config) => { - expect(config).to.have.property('enableTheme', false) + expect(config).to.have.property('theme', undefined) }, ) testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}, {bin: 'red'}).it( - 'should be true when DISABLE_THEME is false and theme.json exists', + 'should be set when DISABLE_THEME is false and theme.json exists', (config) => { - expect(config).to.have.property('enableTheme', true) + expect(config).to.nested.include({'theme.bin.color[0]': 255}) }, ) testConfig({pjson, env: {}}, {bin: 'red'}).it( - 'should be true when DISABLE_THEME is unset and theme.json exists', + 'should be set when DISABLE_THEME is unset and theme.json exists', (config) => { - expect(config).to.have.property('enableTheme', true) + expect(config).to.nested.include({'theme.bin.color[0]': 255}) }, ) testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}).it( - 'should be false when DISABLE_THEME is true and theme.json does not exist', + 'should not be set when DISABLE_THEME is true and theme.json does not exist', (config) => { - expect(config).to.have.property('enableTheme', false) + expect(config).to.have.property('theme', undefined) }, ) testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}).it( - 'should be false when DISABLE_THEME is false and theme.json does not exist', + 'should not be set when DISABLE_THEME is false and theme.json does not exist', (config) => { - expect(config).to.have.property('enableTheme', false) + expect(config).to.have.property('theme', undefined) }, ) testConfig({pjson, env: {}}).it( - 'should be false when DISABLE_THEME is unset and theme.json does not exist', - (config) => { - expect(config).to.have.property('enableTheme', false) - }, - ) - }) - - describe('theme', () => { - testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}, {bin: 'red'}).it( - 'should be set if this.enableTheme is true', - (config) => { - expect(config.theme).to.be.not.undefined - }, - ) - - testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}, {bin: 'red'}).it( - 'should not be set if this.enableTheme is false', + 'should not be set when DISABLE_THEME is unset and theme.json does not exist', (config) => { - expect(config.theme).to.be.undefined + expect(config).to.have.property('theme', undefined) }, ) }) diff --git a/test/interfaces/config.test.ts b/test/interfaces/config.test.ts deleted file mode 100644 index 73bcf8e59..000000000 --- a/test/interfaces/config.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {expect} from 'chai' - -import {Config} from '../../src' -import {Options} from '../../src/interfaces' - -describe('theme', () => { - describe('config', () => { - it('should be created with enableTheme equals to false', () => { - const config: Config = new Config({} as Options) - - expect(config.enableTheme).to.be.false - }) - }) -})