UNPKG

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