Skip to content
This repository has been archived by the owner on Jun 28, 2024. It is now read-only.

Commit

Permalink
feat: Better [ACTION REQUIRED] warning (#335)
Browse files Browse the repository at this point in the history
* feat(ux): Using more saturated color for warnings

* feat(common): Adding `chalk` templating utility

* feat(ux): Using chalk templating in UX messages

* feat: Better message when provider needs attention
  • Loading branch information
kysely authored Jul 17, 2023
1 parent f738683 commit 9387766
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 8 deletions.
11 changes: 7 additions & 4 deletions src/commands/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,15 @@ This command prepares a Provider JSON metadata definition that can be used to ge
(providerJson.services.length === 1 &&
providerJson.services[0].baseUrl.includes('TODO'))
) {
ux.warn(`[ACTION REQUIRED]: Provider definition requires attention. Please check and edit '${formatPath(
ux.warn(`{inverse ACTION REQUIRED }
Documentation was indexed but the Provider definition requires attention.
{bold 1) Edit '{underline ${formatPath(
providerJsonPath
)}' (reference: https://sfc.is/editing-providers).
)}}'. See {underline https://sfc.is/editing-providers}}
Create a new Comlink profile using:
superface new ${providerJson.name} "use case description"`);
2) Create a new Comlink profile using:
superface new ${providerJson.name} "use case description"`);
} else {
ux.succeed(
`Provider definition saved to '${formatPath(providerJsonPath)}'.
Expand Down
212 changes: 212 additions & 0 deletions src/common/chalk-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// This is taken 1:1 from `chalk-template` npm package.
// Due to the package being ESModule only, it couldn't be imported.
// Source: https://github.com/chalk/chalk-template
// =============================================================================

/* eslint-disable */
// @ts-nocheck
import chalk from 'chalk';

const TEMPLATE_REGEX =
/(?:\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.))|(?:{(~)?(#?[\w:]+(?:\([^)]*\))?(?:\.#?[\w:]+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(})|((?:.|[\r\n\f])+?)/gi;
const STYLE_REGEX =
/(?:^|\.)(?:(?:(\w+)(?:\(([^)]*)\))?)|(?:#(?=[:a-fA-F\d]{2,})([a-fA-F\d]{6})?(?::([a-fA-F\d]{6}))?))/g;
const STRING_REGEX = /^(['"])((?:\\.|(?!\1)[^\\])*)\1$/;
const ESCAPE_REGEX =
/\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi;

const ESCAPES = new Map([
['n', '\n'],
['r', '\r'],
['t', '\t'],
['b', '\b'],
['f', '\f'],
['v', '\v'],
['0', '\0'],
['\\', '\\'],
['e', '\u001B'],
['a', '\u0007'],
]);

function unescape(c) {
const u = c[0] === 'u';
const bracket = c[1] === '{';

if ((u && !bracket && c.length === 5) || (c[0] === 'x' && c.length === 3)) {
return String.fromCodePoint(Number.parseInt(c.slice(1), 16));
}

if (u && bracket) {
return String.fromCodePoint(Number.parseInt(c.slice(2, -1), 16));
}

return ESCAPES.get(c) || c;
}

function parseArguments(name, arguments_) {
const results = [];
const chunks = arguments_.trim().split(/\s*,\s*/g);
let matches;

for (const chunk of chunks) {
const number = Number(chunk);
if (!Number.isNaN(number)) {
results.push(number);
} else if ((matches = chunk.match(STRING_REGEX))) {
results.push(
matches[2].replace(ESCAPE_REGEX, (_, escape, character) =>
escape ? unescape(escape) : character
)
);
} else {
throw new Error(
`Invalid Chalk template style argument: ${chunk} (in style '${name}')`
);
}
}

return results;
}

function parseHex(hex) {
const n = Number.parseInt(hex, 16);

return [
// eslint-disable-next-line no-bitwise
(n >> 16) & 0xff,
// eslint-disable-next-line no-bitwise
(n >> 8) & 0xff,
// eslint-disable-next-line no-bitwise
n & 0xff,
];
}

function parseStyle(style) {
STYLE_REGEX.lastIndex = 0;

const results = [];
let matches;

while ((matches = STYLE_REGEX.exec(style)) !== null) {
const name = matches[1];

if (matches[2]) {
results.push([name, ...parseArguments(name, matches[2])]);
} else if (matches[3] || matches[4]) {
if (matches[3]) {
results.push(['rgb', ...parseHex(matches[3])]);
}

if (matches[4]) {
results.push(['bgRgb', ...parseHex(matches[4])]);
}
} else {
results.push([name]);
}
}

return results;
}

export function makeTemplate(chalk) {
function buildStyle(styles) {
const enabled = {};

for (const layer of styles) {
for (const style of layer.styles) {
enabled[style[0]] = layer.inverse ? null : style.slice(1);
}
}

let current = chalk;
for (const [styleName, styles] of Object.entries(enabled)) {
if (!Array.isArray(styles)) {
continue;
}

if (!(styleName in current)) {
throw new Error(`Unknown Chalk style: ${styleName}`);
}

current =
styles.length > 0 ? current[styleName](...styles) : current[styleName];
}

return current;
}

function template(string) {
const styles = [];
const chunks = [];
let chunk = [];

// eslint-disable-next-line max-params
string.replace(
TEMPLATE_REGEX,
(_, escapeCharacter, inverse, style, close, character) => {
if (escapeCharacter) {
chunk.push(unescape(escapeCharacter));
} else if (style) {
const string = chunk.join('');
chunk = [];
chunks.push(
styles.length === 0 ? string : buildStyle(styles)(string)
);
styles.push({ inverse, styles: parseStyle(style) });
} else if (close) {
if (styles.length === 0) {
throw new Error('Found extraneous } in Chalk template literal');
}

chunks.push(buildStyle(styles)(chunk.join('')));
chunk = [];
styles.pop();
} else {
chunk.push(character);
}
}
);

chunks.push(chunk.join(''));

if (styles.length > 0) {
throw new Error(
`Chalk template literal is missing ${styles.length} closing bracket${
styles.length === 1 ? '' : 's'
} (\`}\`)`
);
}

return chunks.join('');
}

return template;
}

function makeChalkTemplate(template) {
function chalkTemplate(firstString, ...arguments_) {
if (!Array.isArray(firstString) || !Array.isArray(firstString.raw)) {
// If chalkTemplate() was called by itself or with a string
throw new TypeError('A tagged template literal must be provided');
}

const parts = [firstString.raw[0]];

for (let index = 1; index < firstString.raw.length; index++) {
parts.push(
String(arguments_[index - 1]).replace(/[{}\\]/g, '\\$&'),
String(firstString.raw[index])
);
}

return template(parts.join(''));
}

return chalkTemplate;
}

export const makeTaggedTemplate = chalkInstance =>
makeChalkTemplate(makeTemplate(chalkInstance));

export const template = makeTemplate(chalk);
export default makeChalkTemplate(template);
15 changes: 11 additions & 4 deletions src/common/ux.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { green, red, yellow } from 'chalk';
import { green, hex, red } from 'chalk';
import type { Spinner } from 'nanospinner';
import { createSpinner } from 'nanospinner';

import { template } from '../common/chalk-template';

const WARN_COLOR = '#B48817';

export class UX {
private static instance: UX | undefined;

Expand All @@ -20,11 +24,11 @@ export class UX {
}

public succeed(text: string): void {
this.spinner.success({ text: green(text), mark: green('✔') });
this.spinner.success({ text: green(template(text)), mark: green('✔') });
}

public fail(text: string): void {
this.spinner.error({ text: red(text), mark: red('✖') });
this.spinner.error({ text: red(template(text)), mark: red('✖') });
}

public info(text: string): void {
Expand All @@ -36,7 +40,10 @@ export class UX {
}

public warn(text: string): void {
this.spinner.warn({ text: yellow(text), mark: yellow('⚠') });
this.spinner.warn({
text: hex(WARN_COLOR)(template(text)),
mark: hex(WARN_COLOR)('⚠'),
});
}

public static create(): UX {
Expand Down

0 comments on commit 9387766

Please sign in to comment.