UNPKG

4.5 kBJavaScriptView Raw
1import { relative } from "path";
2import { fileURLToPath } from "url";
3import { inspect } from "util";
4
5/**
6 * @param {NodeJS.WritableStream} stream
7 * @param {string} level
8 * @param {Date} timestamp
9 * @param {string} context
10 * @param {*} message
11 * @returns {void}
12 */
13export function writePretty(stream, level, timestamp, context, message) {
14 stream.write(`${formatPretty(level, timestamp, context, message)}\n`);
15}
16
17/**
18 * @param {NodeJS.WritableStream} stream
19 * @param {string} level
20 * @param {Date} timestamp
21 * @param {string} context
22 * @param {*} message
23 * @returns {void}
24 */
25export function writeGithubActions(stream, level, timestamp, context, message) {
26 if (level === "error") {
27 // file=app.js,line=10,col=15
28 const { relativePath, column, line } = getErrorLogCaller();
29
30 // See https://github.com/actions/toolkit/issues/193#issuecomment-605394935 for the
31 // replace hack
32 stream.write(
33 `::error file=${relativePath},line=${line},col=${column}::${formatPretty(
34 undefined, // Always an error
35 timestamp,
36 context,
37 message,
38 )
39 .replace(/\n/g, "%0A")
40
41 // Removes ansi color codes from logs
42 // eslint-disable-next-line no-control-regex
43 .replace(/\u001b\[.*?m/g, "")}\n`,
44 );
45 } else {
46 writePretty(stream, level, timestamp, context, message);
47 }
48}
49
50/**
51 * @param {string|undefined} level
52 * @param {Date} timestamp
53 * @param {string|any} context
54 * @param {*} message
55 * @returns {string}
56 */
57export function formatPretty(level, timestamp, context, message) {
58 let prefix = level
59 ? `${formatDate(timestamp)} ${formatLevelAndType(level, context?.type)} `
60 : "";
61
62 if (message) {
63 if (Array.isArray(message)) {
64 return `${
65 prefix + message.map((it) => formatMessagePretty(it)).join(", ")
66 }`;
67 }
68 let keyCount = 0;
69 if (context?.type) {
70 // Dynamic conditional for context writing
71 keyCount = 1;
72 }
73
74 if (Object.keys(context).length > keyCount) {
75 prefix += `${formatMessagePretty(context)} `;
76 }
77
78 return `${prefix + formatMessagePretty(message)}`;
79 }
80
81 return prefix;
82}
83
84/**
85 * @param {*} value
86 * @returns {string}
87 */
88function formatMessagePretty(value) {
89 if (
90 typeof value === "boolean" ||
91 typeof value === "string" ||
92 typeof value === "number"
93 ) {
94 return String(value);
95 }
96 return inspect(value, {
97 colors: true,
98 depth: null,
99 });
100}
101
102/**
103 * @param {Date} date
104 * @returns {string}
105 */
106function formatDate(date) {
107 const h = date.getHours().toString(10).padStart(2, "0");
108 const m = date.getMinutes().toString(10).padStart(2, "0");
109 const s = date.getSeconds().toString(10).padStart(2, "0");
110 const ms = date.getMilliseconds().toString(10).padStart(3, "0");
111
112 return `${h}:${m}:${s}.${ms}`;
113}
114
115/**
116 * @param {string} level
117 * @param {string} type
118 * @returns {string}
119 */
120function formatLevelAndType(level, type) {
121 const str =
122 typeof type === "string" && type.length > 0 ? `${level}[${type}]` : level;
123
124 return level === "error"
125 ? `\x1b[31m${str}\x1b[39m`
126 : `\x1b[34m${str}\x1b[39m`;
127}
128
129/**
130 * Get the caller of the error function, by parsing the stack. May fail
131 *
132 * @returns {{
133 * relativePath: string,
134 * line: number,
135 * column: number,
136 * }}
137 */
138function getErrorLogCaller() {
139 const err = {};
140 Error.captureStackTrace(err);
141
142 // Input:
143 // [0] Error title
144 // [1] writeXxx
145 // [2] error fn
146 // [3] wrapWriter
147 // [4] caller
148 // at main (file:///home/dirk/projects/compas/scripts/brr.js:11:7)
149 const stackLines = err.stack.split("\n").slice(1);
150
151 let callerStackLine = stackLines[0].trim();
152 for (const line of stackLines) {
153 if (
154 line.includes("getErrorLogCaller") ||
155 line.includes("writeGithubActions") ||
156 line.includes("wrapWriter") ||
157 line.includes("Object.error")
158 ) {
159 continue;
160 }
161
162 callerStackLine = line.trim();
163 break;
164 }
165
166 const rawLocation = callerStackLine.split(" ")[2];
167
168 if (callerStackLine.length === 0 || (rawLocation?.length ?? 0) < 5) {
169 return {
170 relativePath: rawLocation,
171 line: 1,
172 column: 1,
173 };
174 }
175
176 const rawLocationParts = rawLocation
177 .substring(1, rawLocation.length - 1)
178 .split(":");
179
180 const rawFile = rawLocationParts
181 .splice(0, rawLocationParts.length - 2)
182 .join(":");
183
184 return {
185 relativePath: relative(process.cwd(), fileURLToPath(rawFile)),
186 line: rawLocationParts[0],
187 column: rawLocationParts[1],
188 };
189}