xref: /expo/tools/src/Formatter.ts (revision a272999e)
1import chalk from 'chalk';
2import path from 'path';
3import terminalLink from 'terminal-link';
4
5import { GitLog, GitFileLog } from './Git';
6
7const { white, cyan, red, green, yellow, blue, gray, dim, reset } = chalk;
8
9/**
10 * Uses `terminal-link` to create clickable link in the terminal.
11 * If the terminal doesn't support this feature, fallback just to provided text, it would look ugly on the CI.
12 */
13export function link(text: string, url: string) {
14  return terminalLink(text, url, { fallback: (text) => text });
15}
16
17/**
18 * Formats an entry from git log.
19 */
20export function formatCommitLog(log: GitLog): string {
21  const authorName = green(log.authorName);
22  const commitHash = formatCommitHash(log.hash);
23  const title = formatCommitTitle(log.title);
24  const date = log.committerRelativeDate;
25
26  return `${commitHash} ${title} ${gray(`by ${authorName} ${date}`)}`;
27}
28
29/**
30 * Formats commit title. So far it only makes closing PR number a link to the PR on GitHub.
31 */
32export function formatCommitTitle(title: string): string {
33  return title.replace(
34    /\(#(\d+)\)$/g,
35    `(${blue.bold(link('#$1', 'https://github.com/expo/expo/pull/$1'))})`
36  );
37}
38
39/**
40 * Formats commit hash to display it as a link pointing to the commit on GitHub.
41 */
42export function formatCommitHash(hash?: string): string {
43  if (!hash) {
44    return gray('undefined');
45  }
46  const url = `https://github.com/expo/expo/commit/${hash}`;
47  const abbreviatedHash = hash.substring(0, 6);
48
49  return yellow.bold(`(${link(abbreviatedHash, url)})`);
50}
51
52/**
53 * Formats markdown changelog entry to be displayed nicely in the terminal.
54 * Replaces links to terminal chars sequence that prints clickable text pointing to given URL.
55 */
56export function formatChangelogEntry(entry: string): string {
57  return entry
58    .replace(/\[(#\d+|@\w+)\]\(([^)]+?)\)/g, blue.bold(link('$1', '$2')))
59    .replace(/(\W)([_*]{2})([^\2]*?)\2(\W)/g, '$1' + reset.bold('$3') + '$4')
60    .replace(/(\W)([_*])([^\2]*?)\2(\W)/g, '$1' + reset.italic('$3') + '$4')
61    .replace(/`([^`]+?)`/g, dim('$1'));
62}
63
64/**
65 * Formats file log - that is a relative file path and the status (modified, added, etc.).
66 */
67export function formatFileLog(fileLog: GitFileLog): string {
68  const uri = `vscode://file/${fileLog.path}`;
69  return `${link(fileLog.relativePath, uri)} ${gray(`(${fileLog.status})`)}`;
70}
71
72/**
73 * Removes all non-ascii characters (characters with unicode number between `0` and `127` are left untouched) from the string.
74 * https://www.utf8-chartable.de/unicode-utf8-table.pl?number=128
75 */
76export function stripNonAsciiChars(str: string): string {
77  return str.replace(/[^\x00-\x7F]/gu, '');
78}
79
80/**
81 * Makes Xcode logs pretty as xcpretty :)
82 */
83export function formatXcodeBuildOutput(output: string): string {
84  return output
85    .replace(/^(\/.*)(:\d+:\d+): (error|warning|note)(:.*)$/gm, (_, p1, p2, p3, p4) => {
86      if (p3 === 'note') {
87        return gray.bold(p3) + white.bold(p4);
88      }
89      const relativePath = path.relative(process.cwd(), p1);
90      const logColor = p3 === 'error' ? red.bold : yellow.bold;
91
92      return cyan.bold(relativePath + p2) + ' ' + logColor(p3 + p4);
93    })
94    .replace(/^(In file included from )(\/[^\n]+)(:\d+:[^\n]*)$/gm, (_, p1, p2, p3) => {
95      const relativePath = path.relative(process.cwd(), p2);
96      return gray.italic(p1 + relativePath + p3);
97    })
98    .replace(/\s\^\n(\s[^\n]+)?/g, green.bold('$&\n'))
99    .replace(/\*\* BUILD FAILED \*\*/, red.bold('$&'));
100}
101