UNPKG

17.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.StreamReport = exports.formatNameWithHyperlink = exports.formatName = void 0;
4const tslib_1 = require("tslib");
5const slice_ansi_1 = tslib_1.__importDefault(require("@arcanis/slice-ansi"));
6const MessageName_1 = require("./MessageName");
7const Report_1 = require("./Report");
8const formatUtils = tslib_1.__importStar(require("./formatUtils"));
9const PROGRESS_FRAMES = [`⠋`, `⠙`, `⠹`, `⠸`, `⠼`, `⠴`, `⠦`, `⠧`, `⠇`, `⠏`];
10const PROGRESS_INTERVAL = 80;
11const BASE_FORGETTABLE_NAMES = new Set([MessageName_1.MessageName.FETCH_NOT_CACHED, MessageName_1.MessageName.UNUSED_CACHE_ENTRY]);
12const BASE_FORGETTABLE_BUFFER_SIZE = 5;
13const GROUP = process.env.GITHUB_ACTIONS
14 ? { start: (what) => `::group::${what}\n`, end: (what) => `::endgroup::\n` }
15 : process.env.TRAVIS
16 ? { start: (what) => `travis_fold:start:${what}\n`, end: (what) => `travis_fold:end:${what}\n` }
17 : process.env.GITLAB_CI
18 ? { start: (what) => `section_start:${Math.floor(Date.now() / 1000)}:${what.toLowerCase().replace(/\W+/g, `_`)}\r\x1b[0K${what}\n`, end: (what) => `section_end:${Math.floor(Date.now() / 1000)}:${what.toLowerCase().replace(/\W+/g, `_`)}\r\x1b[0K` }
19 : null;
20const now = new Date();
21// We only want to support environments that will out-of-the-box accept the
22// characters we want to use. Others can enforce the style from the project
23// configuration.
24const supportsEmojis = [`iTerm.app`, `Apple_Terminal`].includes(process.env.TERM_PROGRAM) || !!process.env.WT_SESSION;
25const makeRecord = (obj) => obj;
26const PROGRESS_STYLES = makeRecord({
27 patrick: {
28 date: [17, 3],
29 chars: [`🍀`, `🌱`],
30 size: 40,
31 },
32 simba: {
33 date: [19, 7],
34 chars: [`🦁`, `🌴`],
35 size: 40,
36 },
37 jack: {
38 date: [31, 10],
39 chars: [`🎃`, `🦇`],
40 size: 40,
41 },
42 hogsfather: {
43 date: [31, 12],
44 chars: [`🎉`, `🎄`],
45 size: 40,
46 },
47 default: {
48 chars: [`=`, `-`],
49 size: 80,
50 },
51});
52const defaultStyle = (supportsEmojis && Object.keys(PROGRESS_STYLES).find(name => {
53 const style = PROGRESS_STYLES[name];
54 if (style.date && (style.date[0] !== now.getDate() || style.date[1] !== now.getMonth() + 1))
55 return false;
56 return true;
57})) || `default`;
58function formatName(name, { configuration, json }) {
59 const num = name === null ? 0 : name;
60 const label = `YN${num.toString(10).padStart(4, `0`)}`;
61 if (!json && name === null) {
62 return formatUtils.pretty(configuration, label, `grey`);
63 }
64 else {
65 return label;
66 }
67}
68exports.formatName = formatName;
69function formatNameWithHyperlink(name, { configuration, json }) {
70 const code = formatName(name, { configuration, json });
71 // Only print hyperlinks if allowed per configuration
72 if (!configuration.get(`enableHyperlinks`))
73 return code;
74 // Don't print hyperlinks for the generic messages
75 if (name === null || name === MessageName_1.MessageName.UNNAMED)
76 return code;
77 const desc = MessageName_1.MessageName[name];
78 const href = `https://yarnpkg.com/advanced/error-codes#${code}---${desc}`.toLowerCase();
79 // We use BELL as ST because it seems that iTerm doesn't properly support
80 // the \x1b\\ sequence described in the reference document
81 // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#the-escape-sequence
82 return `\u001b]8;;${href}\u0007${code}\u001b]8;;\u0007`;
83}
84exports.formatNameWithHyperlink = formatNameWithHyperlink;
85class StreamReport extends Report_1.Report {
86 constructor({ configuration, stdout, json = false, includeFooter = true, includeLogs = !json, includeInfos = includeLogs, includeWarnings = includeLogs, forgettableBufferSize = BASE_FORGETTABLE_BUFFER_SIZE, forgettableNames = new Set(), }) {
87 super();
88 this.cacheHitCount = 0;
89 this.cacheMissCount = 0;
90 this.warningCount = 0;
91 this.errorCount = 0;
92 this.startTime = Date.now();
93 this.indent = 0;
94 this.progress = new Map();
95 this.progressTime = 0;
96 this.progressFrame = 0;
97 this.progressTimeout = null;
98 this.forgettableLines = [];
99 this.configuration = configuration;
100 this.forgettableBufferSize = forgettableBufferSize;
101 this.forgettableNames = new Set([
102 ...forgettableNames,
103 ...BASE_FORGETTABLE_NAMES,
104 ]);
105 this.includeFooter = includeFooter;
106 this.includeInfos = includeInfos;
107 this.includeWarnings = includeWarnings;
108 this.json = json;
109 this.stdout = stdout;
110 }
111 static async start(opts, cb) {
112 const report = new this(opts);
113 const emitWarning = process.emitWarning;
114 process.emitWarning = (message, name) => {
115 if (typeof message !== `string`) {
116 const error = message;
117 message = error.message;
118 name = name !== null && name !== void 0 ? name : error.name;
119 }
120 const fullMessage = typeof name !== `undefined`
121 ? `${name}: ${message}`
122 : message;
123 report.reportWarning(MessageName_1.MessageName.UNNAMED, fullMessage);
124 };
125 try {
126 await cb(report);
127 }
128 catch (error) {
129 report.reportExceptionOnce(error);
130 }
131 finally {
132 await report.finalize();
133 process.emitWarning = emitWarning;
134 }
135 return report;
136 }
137 hasErrors() {
138 return this.errorCount > 0;
139 }
140 exitCode() {
141 return this.hasErrors() ? 1 : 0;
142 }
143 reportCacheHit(locator) {
144 this.cacheHitCount += 1;
145 }
146 reportCacheMiss(locator, message) {
147 this.cacheMissCount += 1;
148 if (typeof message !== `undefined` && !this.configuration.get(`preferAggregateCacheInfo`)) {
149 this.reportInfo(MessageName_1.MessageName.FETCH_NOT_CACHED, message);
150 }
151 }
152 startTimerSync(what, cb) {
153 this.reportInfo(null, `┌ ${what}`);
154 const before = Date.now();
155 this.indent += 1;
156 try {
157 return cb();
158 }
159 catch (error) {
160 this.reportExceptionOnce(error);
161 throw error;
162 }
163 finally {
164 const after = Date.now();
165 this.indent -= 1;
166 if (this.configuration.get(`enableTimers`) && after - before > 200) {
167 this.reportInfo(null, `└ Completed in ${formatUtils.pretty(this.configuration, after - before, formatUtils.Type.DURATION)}`);
168 }
169 else {
170 this.reportInfo(null, `└ Completed`);
171 }
172 }
173 }
174 async startTimerPromise(what, cb) {
175 this.reportInfo(null, `┌ ${what}`);
176 if (GROUP !== null)
177 this.stdout.write(GROUP.start(what));
178 const before = Date.now();
179 this.indent += 1;
180 try {
181 return await cb();
182 }
183 catch (error) {
184 this.reportExceptionOnce(error);
185 throw error;
186 }
187 finally {
188 const after = Date.now();
189 this.indent -= 1;
190 if (GROUP !== null)
191 this.stdout.write(GROUP.end(what));
192 if (this.configuration.get(`enableTimers`) && after - before > 200) {
193 this.reportInfo(null, `└ Completed in ${formatUtils.pretty(this.configuration, after - before, formatUtils.Type.DURATION)}`);
194 }
195 else {
196 this.reportInfo(null, `└ Completed`);
197 }
198 }
199 }
200 async startCacheReport(cb) {
201 const cacheInfo = this.configuration.get(`preferAggregateCacheInfo`)
202 ? { cacheHitCount: this.cacheHitCount, cacheMissCount: this.cacheMissCount }
203 : null;
204 try {
205 return await cb();
206 }
207 catch (error) {
208 this.reportExceptionOnce(error);
209 throw error;
210 }
211 finally {
212 if (cacheInfo !== null) {
213 this.reportCacheChanges(cacheInfo);
214 }
215 }
216 }
217 reportSeparator() {
218 if (this.indent === 0) {
219 this.writeLineWithForgettableReset(``);
220 }
221 else {
222 this.reportInfo(null, ``);
223 }
224 }
225 reportInfo(name, text) {
226 if (!this.includeInfos)
227 return;
228 const message = `${formatUtils.pretty(this.configuration, `➤`, `blueBright`)} ${this.formatNameWithHyperlink(name)}: ${this.formatIndent()}${text}`;
229 if (!this.json) {
230 if (this.forgettableNames.has(name)) {
231 this.forgettableLines.push(message);
232 if (this.forgettableLines.length > this.forgettableBufferSize) {
233 while (this.forgettableLines.length > this.forgettableBufferSize)
234 this.forgettableLines.shift();
235 this.writeLines(this.forgettableLines, { truncate: true });
236 }
237 else {
238 this.writeLine(message, { truncate: true });
239 }
240 }
241 else {
242 this.writeLineWithForgettableReset(message);
243 }
244 }
245 else {
246 this.reportJson({ type: `info`, name, displayName: this.formatName(name), indent: this.formatIndent(), data: text });
247 }
248 }
249 reportWarning(name, text) {
250 this.warningCount += 1;
251 if (!this.includeWarnings)
252 return;
253 if (!this.json) {
254 this.writeLineWithForgettableReset(`${formatUtils.pretty(this.configuration, `➤`, `yellowBright`)} ${this.formatNameWithHyperlink(name)}: ${this.formatIndent()}${text}`);
255 }
256 else {
257 this.reportJson({ type: `warning`, name, displayName: this.formatName(name), indent: this.formatIndent(), data: text });
258 }
259 }
260 reportError(name, text) {
261 this.errorCount += 1;
262 if (!this.json) {
263 this.writeLineWithForgettableReset(`${formatUtils.pretty(this.configuration, `➤`, `redBright`)} ${this.formatNameWithHyperlink(name)}: ${this.formatIndent()}${text}`, { truncate: false });
264 }
265 else {
266 this.reportJson({ type: `error`, name, displayName: this.formatName(name), indent: this.formatIndent(), data: text });
267 }
268 }
269 reportProgress(progressIt) {
270 let stopped = false;
271 const promise = Promise.resolve().then(async () => {
272 const progressDefinition = {
273 progress: 0,
274 title: undefined,
275 };
276 this.progress.set(progressIt, progressDefinition);
277 this.refreshProgress(-1);
278 for await (const { progress, title } of progressIt) {
279 if (stopped)
280 continue;
281 if (progressDefinition.progress === progress && progressDefinition.title === title)
282 continue;
283 progressDefinition.progress = progress;
284 progressDefinition.title = title;
285 this.refreshProgress();
286 }
287 stop();
288 });
289 const stop = () => {
290 if (stopped)
291 return;
292 stopped = true;
293 this.progress.delete(progressIt);
294 this.refreshProgress(+1);
295 };
296 return { ...promise, stop };
297 }
298 reportJson(data) {
299 if (this.json) {
300 this.writeLineWithForgettableReset(`${JSON.stringify(data)}`);
301 }
302 }
303 async finalize() {
304 if (!this.includeFooter)
305 return;
306 let installStatus = ``;
307 if (this.errorCount > 0)
308 installStatus = `Failed with errors`;
309 else if (this.warningCount > 0)
310 installStatus = `Done with warnings`;
311 else
312 installStatus = `Done`;
313 const timing = formatUtils.pretty(this.configuration, Date.now() - this.startTime, formatUtils.Type.DURATION);
314 const message = this.configuration.get(`enableTimers`)
315 ? `${installStatus} in ${timing}`
316 : installStatus;
317 if (this.errorCount > 0) {
318 this.reportError(MessageName_1.MessageName.UNNAMED, message);
319 }
320 else if (this.warningCount > 0) {
321 this.reportWarning(MessageName_1.MessageName.UNNAMED, message);
322 }
323 else {
324 this.reportInfo(MessageName_1.MessageName.UNNAMED, message);
325 }
326 }
327 writeLine(str, { truncate } = {}) {
328 this.clearProgress({ clear: true });
329 this.stdout.write(`${this.truncate(str, { truncate })}\n`);
330 this.writeProgress();
331 }
332 writeLineWithForgettableReset(str, { truncate } = {}) {
333 this.forgettableLines = [];
334 this.writeLine(str, { truncate });
335 }
336 writeLines(lines, { truncate } = {}) {
337 this.clearProgress({ delta: lines.length });
338 for (const line of lines)
339 this.stdout.write(`${this.truncate(line, { truncate })}\n`);
340 this.writeProgress();
341 }
342 reportCacheChanges({ cacheHitCount, cacheMissCount }) {
343 const cacheHitDelta = this.cacheHitCount - cacheHitCount;
344 const cacheMissDelta = this.cacheMissCount - cacheMissCount;
345 if (cacheHitDelta === 0 && cacheMissDelta === 0)
346 return;
347 let fetchStatus = ``;
348 if (this.cacheHitCount > 1)
349 fetchStatus += `${this.cacheHitCount} packages were already cached`;
350 else if (this.cacheHitCount === 1)
351 fetchStatus += ` - one package was already cached`;
352 else
353 fetchStatus += `No packages were cached`;
354 if (this.cacheHitCount > 0) {
355 if (this.cacheMissCount > 1) {
356 fetchStatus += `, ${this.cacheMissCount} had to be fetched`;
357 }
358 else if (this.cacheMissCount === 1) {
359 fetchStatus += `, one had to be fetched`;
360 }
361 }
362 else {
363 if (this.cacheMissCount > 1) {
364 fetchStatus += ` - ${this.cacheMissCount} packages had to be fetched`;
365 }
366 else if (this.cacheMissCount === 1) {
367 fetchStatus += ` - one package had to be fetched`;
368 }
369 }
370 this.reportInfo(MessageName_1.MessageName.FETCH_NOT_CACHED, fetchStatus);
371 }
372 clearProgress({ delta = 0, clear = false }) {
373 if (!this.configuration.get(`enableProgressBars`) || this.json)
374 return;
375 if (this.progress.size + delta > 0) {
376 this.stdout.write(`\x1b[${this.progress.size + delta}A`);
377 if (delta > 0 || clear) {
378 this.stdout.write(`\x1b[0J`);
379 }
380 }
381 }
382 writeProgress() {
383 if (!this.configuration.get(`enableProgressBars`) || this.json)
384 return;
385 if (this.progressTimeout !== null)
386 clearTimeout(this.progressTimeout);
387 this.progressTimeout = null;
388 if (this.progress.size === 0)
389 return;
390 const now = Date.now();
391 if (now - this.progressTime > PROGRESS_INTERVAL) {
392 this.progressFrame = (this.progressFrame + 1) % PROGRESS_FRAMES.length;
393 this.progressTime = now;
394 }
395 const spinner = PROGRESS_FRAMES[this.progressFrame];
396 const styleName = this.configuration.get(`progressBarStyle`) || defaultStyle;
397 if (!Object.prototype.hasOwnProperty.call(PROGRESS_STYLES, styleName))
398 throw new Error(`Assertion failed: Invalid progress bar style`);
399 const style = PROGRESS_STYLES[styleName];
400 const PAD_LEFT = `➤ YN0000: ┌ `.length;
401 const maxWidth = Math.max(0, Math.min(process.stdout.columns - PAD_LEFT, 80));
402 const scaledSize = Math.floor(style.size * maxWidth / 80);
403 for (const { progress } of this.progress.values()) {
404 const okSize = scaledSize * progress;
405 const ok = style.chars[0].repeat(okSize);
406 const ko = style.chars[1].repeat(scaledSize - okSize);
407 this.stdout.write(`${formatUtils.pretty(this.configuration, `➤`, `blueBright`)} ${this.formatName(null)}: ${spinner} ${ok}${ko}\n`);
408 }
409 this.progressTimeout = setTimeout(() => {
410 this.refreshProgress();
411 }, 1000 / 60);
412 }
413 refreshProgress(delta = 0) {
414 this.clearProgress({ delta });
415 this.writeProgress();
416 }
417 truncate(str, { truncate } = {}) {
418 if (!this.configuration.get(`enableProgressBars`))
419 truncate = false;
420 if (typeof truncate === `undefined`)
421 truncate = this.configuration.get(`preferTruncatedLines`);
422 // The -1 is to account for terminals that would wrap after
423 // the last column rather before the first overwrite
424 if (truncate)
425 str = slice_ansi_1.default(str, 0, process.stdout.columns - 1);
426 return str;
427 }
428 formatName(name) {
429 return formatName(name, {
430 configuration: this.configuration,
431 json: this.json,
432 });
433 }
434 formatNameWithHyperlink(name) {
435 return formatNameWithHyperlink(name, {
436 configuration: this.configuration,
437 json: this.json,
438 });
439 }
440 formatIndent() {
441 return `│ `.repeat(this.indent);
442 }
443}
444exports.StreamReport = StreamReport;