@sasjs/cli 6.6.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +2 -1
  2. package/build/commands/flow/flowCommand.js +11 -1
  3. package/build/commands/flow/internal/executeFlow.js +44 -19
  4. package/build/commands/flow/internal/saveLog.d.ts +12 -1
  5. package/build/commands/flow/internal/saveLog.js +2 -2
  6. package/build/commands/flow/internal/spec/executeFlow.spec.js +44 -1
  7. package/build/commands/flow/spec/flowCommand.spec.js +44 -0
  8. package/build/commands/help/help.js +10 -0
  9. package/build/commands/index.d.ts +1 -0
  10. package/build/commands/index.js +3 -1
  11. package/build/commands/job/internal/execute/sas9.js +16 -13
  12. package/build/commands/job/internal/execute/sasjs.js +12 -9
  13. package/build/commands/job/internal/execute/viya.js +41 -36
  14. package/build/commands/job/jobCommand.js +13 -0
  15. package/build/commands/job/spec/executeLogOrder.spec.d.ts +1 -0
  16. package/build/commands/job/spec/executeLogOrder.spec.js +119 -0
  17. package/build/commands/job/spec/jobCommand.spec.js +32 -0
  18. package/build/commands/logcheck/collectLogs.d.ts +60 -0
  19. package/build/commands/logcheck/collectLogs.js +386 -0
  20. package/build/commands/logcheck/logCheckConfig.d.ts +83 -0
  21. package/build/commands/logcheck/logCheckConfig.js +343 -0
  22. package/build/commands/logcheck/logcheckCommand.d.ts +6 -0
  23. package/build/commands/logcheck/logcheckCommand.js +200 -0
  24. package/build/commands/logcheck/processLogCheck.d.ts +44 -0
  25. package/build/commands/logcheck/processLogCheck.js +242 -0
  26. package/build/commands/logcheck/renderLogCheck.d.ts +129 -0
  27. package/build/commands/logcheck/renderLogCheck.js +424 -0
  28. package/build/commands/logcheck/spec/collectLogs.spec.d.ts +1 -0
  29. package/build/commands/logcheck/spec/collectLogs.spec.js +477 -0
  30. package/build/commands/logcheck/spec/logCheckConfig.spec.d.ts +1 -0
  31. package/build/commands/logcheck/spec/logCheckConfig.spec.js +384 -0
  32. package/build/commands/logcheck/spec/logcheckCommand.spec.d.ts +1 -0
  33. package/build/commands/logcheck/spec/logcheckCommand.spec.js +360 -0
  34. package/build/commands/logcheck/spec/processLogCheck.spec.d.ts +1 -0
  35. package/build/commands/logcheck/spec/processLogCheck.spec.js +354 -0
  36. package/build/commands/logcheck/spec/renderLogCheck.spec.d.ts +1 -0
  37. package/build/commands/logcheck/spec/renderLogCheck.spec.js +426 -0
  38. package/build/commands/logcheck/types.d.ts +106 -0
  39. package/build/commands/logcheck/types.js +14 -0
  40. package/build/commands/request/request.js +66 -27
  41. package/build/commands/request/requestCommand.js +11 -1
  42. package/build/commands/request/spec/requestCommand.spec.js +40 -0
  43. package/build/commands/run/run.d.ts +1 -3
  44. package/build/commands/run/run.js +20 -5
  45. package/build/commands/run/runCommand.js +9 -0
  46. package/build/commands/run/spec/runCommand.spec.js +50 -0
  47. package/build/types/command/commandAliases.js +1 -0
  48. package/build/types/command/commandFactory.js +2 -0
  49. package/build/types/command/returnCode.d.ts +2 -1
  50. package/build/types/command/returnCode.js +4 -0
  51. package/build/utils/index.d.ts +1 -0
  52. package/build/utils/index.js +1 -0
  53. package/build/utils/logcheck.d.ts +44 -0
  54. package/build/utils/logcheck.js +224 -0
  55. package/build/utils/spec/logcheck.spec.d.ts +1 -0
  56. package/build/utils/spec/logcheck.spec.js +297 -0
  57. package/package.json +3 -3
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __values = (this && this.__values) || function(o) {
14
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
15
+ if (m) return m.call(o);
16
+ if (o && typeof o.length === "number") return {
17
+ next: function () {
18
+ if (o && i >= o.length) o = void 0;
19
+ return { value: o && o[i++], done: !o };
20
+ }
21
+ };
22
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
23
+ };
24
+ var __read = (this && this.__read) || function (o, n) {
25
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
26
+ if (!m) return o;
27
+ var i = m.call(o), r, ar = [], e;
28
+ try {
29
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
30
+ }
31
+ catch (error) { e = { error: error }; }
32
+ finally {
33
+ try {
34
+ if (r && !r.done && (m = i["return"])) m.call(i);
35
+ }
36
+ finally { if (e) throw e.error; }
37
+ }
38
+ return ar;
39
+ };
40
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
41
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
42
+ if (ar || !(i in from)) {
43
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
44
+ ar[i] = from[i];
45
+ }
46
+ }
47
+ return to.concat(ar || Array.prototype.slice.call(from));
48
+ };
49
+ var __importDefault = (this && this.__importDefault) || function (mod) {
50
+ return (mod && mod.__esModule) ? mod : { "default": mod };
51
+ };
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.toAnnotations = exports.toReportIssue = exports.toJson = exports.summarise = exports.truncate = exports.severityLabel = exports.lineLabel = exports.columns = exports.issueDetails = exports.fileFails = exports.fileSummary = exports.renderHuman = exports.renderLogCheck = exports.MAX_MESSAGE_WIDTH = void 0;
54
+ var chalk_1 = __importDefault(require("chalk"));
55
+ var types_1 = require("./types");
56
+ /** Longest message shown in the human report before it is cut short. */
57
+ exports.MAX_MESSAGE_WIDTH = 110;
58
+ /** Blank line between two blocks of the human report. */
59
+ var GAP = '';
60
+ /**
61
+ * Prints the result in the requested format.
62
+ *
63
+ * The three formats are three views of one summary, never three scans: the
64
+ * terminal report, the JSON document and the CI annotations must all be
65
+ * describing the same parse, or they will disagree about the same log.
66
+ */
67
+ var renderLogCheck = function (summary, config, options) {
68
+ if (options === void 0) { options = {}; }
69
+ var format = options.format || 'human';
70
+ if (format === 'json') {
71
+ process.stdout.write("".concat(JSON.stringify((0, exports.toJson)(summary, config), null, 2), "\n"));
72
+ return;
73
+ }
74
+ if (format === 'annotations') {
75
+ var lines = (0, exports.toAnnotations)(summary);
76
+ if (lines.length)
77
+ process.stdout.write("".concat(lines.join('\n'), "\n"));
78
+ return;
79
+ }
80
+ (0, exports.renderHuman)(summary, config, options.verbose || false);
81
+ };
82
+ exports.renderLogCheck = renderLogCheck;
83
+ /**
84
+ * The human report: what was checked, what was found, and the verdict.
85
+ *
86
+ * Colour carries severity and nothing else, so a reader scanning for trouble
87
+ * has one thing to look at, and the same report pasted into a ticket or
88
+ * piped into a log file without a terminal is still readable.
89
+ */
90
+ var renderHuman = function (summary, config, verbose) {
91
+ var e_1, _a, e_2, _b;
92
+ var _c;
93
+ if (verbose === void 0) { verbose = false; }
94
+ var lines = [];
95
+ lines.push("".concat(chalk_1.default.bold('SASjs logcheck')).concat(chalk_1.default.dim(summary.searched.length ? " ".concat(summary.searched.join(', ')) : '')));
96
+ lines.push(GAP);
97
+ try {
98
+ for (var _d = __values(summary.missing), _e = _d.next(); !_e.done; _e = _d.next()) {
99
+ var missing = _e.value;
100
+ lines.push(chalk_1.default.red(" Path not found: ".concat(missing)) +
101
+ (summary.usedDefaultInput
102
+ ? chalk_1.default.dim(' - nothing has run yet, or pass a path')
103
+ : ''));
104
+ }
105
+ }
106
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
107
+ finally {
108
+ try {
109
+ if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
110
+ }
111
+ finally { if (e_1) throw e_1.error; }
112
+ }
113
+ try {
114
+ for (var _f = __values(summary.unreadable), _g = _f.next(); !_g.done; _g = _f.next()) {
115
+ var folder = _g.value;
116
+ lines.push(chalk_1.default.red(" Could not read: ".concat(folder.displayPath)) +
117
+ chalk_1.default.dim(" (".concat(folder.reason, ") - its logs were not checked")));
118
+ }
119
+ }
120
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
121
+ finally {
122
+ try {
123
+ if (_g && !_g.done && (_b = _f.return)) _b.call(_f);
124
+ }
125
+ finally { if (e_2) throw e_2.error; }
126
+ }
127
+ var scanned = summary.logs.filter(function (entry) { return !entry.skippedReason; });
128
+ if (!scanned.length) {
129
+ if (!summary.missing.length) {
130
+ lines.push(summary.skipped.length
131
+ ? chalk_1.default.yellow(' No SAS logs found - every file was skipped.')
132
+ : chalk_1.default.yellow(' No logs found at the path searched.'));
133
+ }
134
+ }
135
+ else {
136
+ lines.push.apply(lines, __spreadArray([], __read((0, exports.fileSummary)(scanned, config)), false));
137
+ lines.push.apply(lines, __spreadArray([], __read(skippedLines(summary, verbose)), false));
138
+ var details = (0, exports.issueDetails)(scanned);
139
+ if (details.length)
140
+ lines.push.apply(lines, __spreadArray([GAP], __read(details), false));
141
+ lines.push(GAP);
142
+ lines.push(" ".concat((0, exports.summarise)(summary, scanned.length)));
143
+ if (summary.totals.suppressed) {
144
+ lines.push(chalk_1.default.dim(" ".concat(plural(summary.totals.suppressed, 'issue'), " suppressed by message code").concat(config.ignoreCodes.length
145
+ ? " (".concat(config.ignoreCodes.join(', '), ")")
146
+ : '')));
147
+ }
148
+ }
149
+ if (summary.failure) {
150
+ lines.push("".concat(chalk_1.default.red.bold(' Logcheck failed: ')).concat(chalk_1.default.red(summary.failure)) +
151
+ chalk_1.default.dim(" (fail-on: ".concat(config.failOn, ")")));
152
+ }
153
+ else if (scanned.length) {
154
+ lines.push(chalk_1.default.green(" Logcheck passed (fail-on: ".concat(config.failOn, ")")));
155
+ }
156
+ lines.push('');
157
+ (_c = process.logger) === null || _c === void 0 ? void 0 : _c.log(lines.join('\n'));
158
+ };
159
+ exports.renderHuman = renderHuman;
160
+ /** The one-line-per-file table: what each log carried, and whether it failed. */
161
+ var fileSummary = function (scanned, config) {
162
+ if (config === void 0) { config = types_1.defaultLogCheckConfig; }
163
+ var rows = scanned.map(function (entry) { return [
164
+ entry.displayPath,
165
+ countCell(entry.counts.errors, chalk_1.default.red),
166
+ countCell(entry.counts.warnings, chalk_1.default.yellow),
167
+ countCell(entry.counts.notes, chalk_1.default.cyan),
168
+ (0, exports.fileFails)(entry.counts, config)
169
+ ? chalk_1.default.red.bold('FAIL')
170
+ : chalk_1.default.green('pass')
171
+ ]; });
172
+ return (0, exports.columns)(rows, ['FILE', 'ERRORS', 'WARNINGS', 'NOTES', 'STATUS'], ' ');
173
+ };
174
+ exports.fileSummary = fileSummary;
175
+ /**
176
+ * Whether a single log breaks the policy.
177
+ *
178
+ * The warnings budget is deliberately not applied here: it is a budget for the
179
+ * whole run, so a log carrying three warnings is not the one that broke it.
180
+ */
181
+ var fileFails = function (counts, config) {
182
+ if (config.failOn === 'never')
183
+ return false;
184
+ if (counts.errors)
185
+ return true;
186
+ if (config.failOn === 'warning' || config.failOn === 'note') {
187
+ if (counts.warnings)
188
+ return true;
189
+ }
190
+ return config.failOn === 'note' && counts.notes > 0;
191
+ };
192
+ exports.fileFails = fileFails;
193
+ /**
194
+ * The issues themselves, grouped under the log they came from.
195
+ *
196
+ * This is the shape a compiler prints and a reader already knows how to scan:
197
+ * the file once, then `line:column`, the severity, the SAS code where there is
198
+ * one, and the message.
199
+ */
200
+ var issueDetails = function (scanned) {
201
+ var e_3, _a;
202
+ var blocks = [];
203
+ try {
204
+ for (var scanned_1 = __values(scanned), scanned_1_1 = scanned_1.next(); !scanned_1_1.done; scanned_1_1 = scanned_1.next()) {
205
+ var entry = scanned_1_1.value;
206
+ if (!entry.issues.length)
207
+ continue;
208
+ blocks.push(__spreadArray([
209
+ chalk_1.default.bold(" ".concat(entry.displayPath))
210
+ ], __read((0, exports.columns)(entry.issues.map(function (issue) { return [
211
+ (0, exports.lineLabel)(issue),
212
+ (0, exports.severityLabel)(issue.severity),
213
+ issue.code || chalk_1.default.dim('-'),
214
+ (0, exports.truncate)(issue.message)
215
+ ]; }), ['LINE:COL', 'SEVERITY', 'CODE', 'MESSAGE'], ' ', ' ')), false));
216
+ }
217
+ }
218
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
219
+ finally {
220
+ try {
221
+ if (scanned_1_1 && !scanned_1_1.done && (_a = scanned_1.return)) _a.call(scanned_1);
222
+ }
223
+ finally { if (e_3) throw e_3.error; }
224
+ }
225
+ // One blank between two logs' issues, none after the last: the caller adds
226
+ // the gap before the summary line, and two would leave a hole in the report.
227
+ return blocks.reduce(function (lines, block, index) {
228
+ return index === blocks.length - 1
229
+ ? __spreadArray(__spreadArray([], __read(lines), false), __read(block), false) : __spreadArray(__spreadArray(__spreadArray([], __read(lines), false), __read(block), false), [GAP], false);
230
+ }, []);
231
+ };
232
+ exports.issueDetails = issueDetails;
233
+ var skippedLines = function (summary, verbose) {
234
+ if (!summary.skipped.length)
235
+ return [];
236
+ if (!verbose) {
237
+ return [
238
+ GAP,
239
+ chalk_1.default.dim(" ".concat(plural(summary.skipped.length, 'file'), " skipped (not a SAS log, or excluded) - rerun with --verbose to list them."))
240
+ ];
241
+ }
242
+ return __spreadArray([
243
+ GAP
244
+ ], __read((0, exports.columns)(summary.skipped.map(function (entry) { return [
245
+ chalk_1.default.dim(entry.displayPath),
246
+ chalk_1.default.dim(entry.reason)
247
+ ]; }), ['SKIPPED FILE', 'REASON'], ' ', ' ')), false);
248
+ };
249
+ /**
250
+ * Lays rows out in aligned columns, with the header underlined by dashes.
251
+ *
252
+ * Colour codes take no space on screen, so widths are measured on the text
253
+ * with the codes removed - otherwise every coloured cell is padded by the
254
+ * length of its own escape sequences and the table drifts.
255
+ */
256
+ var columns = function (rows, head, gap, indent) {
257
+ if (gap === void 0) { gap = ' '; }
258
+ if (indent === void 0) { indent = ''; }
259
+ var widths = [];
260
+ var measure = function (row) {
261
+ return row.forEach(function (cell, index) {
262
+ widths[index] = Math.max(widths[index] || 0, visibleLength(cell));
263
+ });
264
+ };
265
+ measure(head.map(function (header) { return chalk_1.default.white.bold(header); }));
266
+ rows.forEach(measure);
267
+ var render = function (row) {
268
+ return "".concat(indent).concat(row
269
+ .map(function (cell, index) {
270
+ return cell + ' '.repeat(Math.max(0, widths[index] - visibleLength(cell)));
271
+ })
272
+ .join(gap)
273
+ .trimEnd());
274
+ };
275
+ return __spreadArray([
276
+ render(head.map(function (header) { return chalk_1.default.white.bold(header); })),
277
+ "".concat(indent).concat(widths.map(function (width) { return '-'.repeat(width); }).join(gap))
278
+ ], __read(rows.map(render)), false);
279
+ };
280
+ exports.columns = columns;
281
+ var visibleLength = function (value) {
282
+ return value.replace(/\u001b\[[0-9;]*m/g, '').length;
283
+ };
284
+ /** `13:5` for a one-line message, `13-15:5` for one SAS wrapped. */
285
+ var lineLabel = function (issue) {
286
+ return issue.endLine > issue.line
287
+ ? "".concat(issue.line + 1, "-").concat(issue.endLine + 1, ":").concat(issue.column + 1)
288
+ : "".concat(issue.line + 1, ":").concat(issue.column + 1);
289
+ };
290
+ exports.lineLabel = lineLabel;
291
+ var severityLabel = function (severity) {
292
+ if (severity === 'error')
293
+ return chalk_1.default.red.bold('error');
294
+ if (severity === 'warning')
295
+ return chalk_1.default.yellow.bold('warning');
296
+ return chalk_1.default.cyan.bold('note');
297
+ };
298
+ exports.severityLabel = severityLabel;
299
+ /** Trims a message to the report width, on a word boundary where there is one. */
300
+ var truncate = function (message) {
301
+ var flat = message.replace(/\s+/g, ' ').trim();
302
+ if (flat.length <= exports.MAX_MESSAGE_WIDTH)
303
+ return flat;
304
+ var cut = flat.slice(0, exports.MAX_MESSAGE_WIDTH - 3);
305
+ var boundary = cut.lastIndexOf(' ');
306
+ return "".concat(boundary > 0 ? cut.slice(0, boundary) : cut, "...");
307
+ };
308
+ exports.truncate = truncate;
309
+ var countCell = function (count, colour) {
310
+ return count ? colour(String(count)) : chalk_1.default.dim('0');
311
+ };
312
+ /**
313
+ * The one-line answer: what was found, and over how many logs.
314
+ *
315
+ * It names the logs that were checked rather than the files that were not,
316
+ * because the number a reader compares against is the size of the result they
317
+ * have just looked at.
318
+ */
319
+ var summarise = function (summary, scannedCount) {
320
+ var _a = summary.totals, errors = _a.errors, warnings = _a.warnings, notes = _a.notes, logs = _a.logs, skipped = _a.skipped;
321
+ var parts = [];
322
+ if (errors)
323
+ parts.push(plural(errors, 'error'));
324
+ if (warnings)
325
+ parts.push(plural(warnings, 'warning'));
326
+ if (notes)
327
+ parts.push(plural(notes, 'note'));
328
+ var scope = skipped > 0 ? "".concat(logs, " of ").concat(logs + skipped, " files") : plural(logs, 'log');
329
+ return parts.length
330
+ ? "".concat(parts.join(', '), " in ").concat(scope)
331
+ : "No issues in ".concat(scope);
332
+ };
333
+ exports.summarise = summarise;
334
+ /**
335
+ * The machine report.
336
+ *
337
+ * Line and column are one-based here, as an editor and a CI annotation count
338
+ * them, while the parser works in indexes - the conversion happens once, at
339
+ * this boundary, so no consumer has to remember which convention it holds.
340
+ */
341
+ var toJson = function (summary, config) {
342
+ var _a, _b;
343
+ return ({
344
+ tool: 'sasjs logcheck',
345
+ version: 1,
346
+ config: {
347
+ failOn: config.failOn,
348
+ maxWarnings: (_a = config.maxWarnings) !== null && _a !== void 0 ? _a : null,
349
+ autoCheck: config.autoCheck === true,
350
+ ignoreCodes: config.ignoreCodes,
351
+ ignorePaths: config.ignorePaths
352
+ },
353
+ summary: {
354
+ logs: summary.totals.logs,
355
+ skipped: summary.totals.skipped,
356
+ errors: summary.totals.errors,
357
+ warnings: summary.totals.warnings,
358
+ notes: summary.totals.notes,
359
+ suppressed: summary.totals.suppressed,
360
+ failed: !!summary.failure,
361
+ failure: (_b = summary.failure) !== null && _b !== void 0 ? _b : null
362
+ },
363
+ searched: summary.searched,
364
+ missing: summary.missing,
365
+ unreadable: summary.unreadable.map(function (folder) { return ({
366
+ folder: folder.displayPath,
367
+ reason: folder.reason
368
+ }); }),
369
+ logs: summary.logs.map(function (entry) {
370
+ var _a;
371
+ return ({
372
+ file: entry.displayPath,
373
+ errors: entry.counts.errors,
374
+ warnings: entry.counts.warnings,
375
+ notes: entry.counts.notes,
376
+ suppressed: entry.suppressed.length,
377
+ skipped: (_a = entry.skippedReason) !== null && _a !== void 0 ? _a : null,
378
+ issues: entry.issues.map(exports.toReportIssue)
379
+ });
380
+ }),
381
+ skipped: summary.skipped.map(function (entry) { return ({
382
+ file: entry.displayPath,
383
+ reason: entry.reason
384
+ }); })
385
+ });
386
+ };
387
+ exports.toJson = toJson;
388
+ var toReportIssue = function (issue) { return (__assign(__assign({ line: issue.line + 1, endLine: issue.endLine + 1, column: issue.column + 1, endColumn: issue.endColumn + 1, severity: issue.severity }, (issue.code ? { code: issue.code } : {})), { message: issue.message })); };
389
+ exports.toReportIssue = toReportIssue;
390
+ /**
391
+ * GitHub Actions annotations, one line per issue.
392
+ *
393
+ * The workflow commands escape three characters in the message and five in the
394
+ * properties; doing it here means SAS's own text arrives intact, commas and
395
+ * colons included.
396
+ */
397
+ var toAnnotations = function (summary) {
398
+ return summary.logs.flatMap(function (entry) {
399
+ return entry.issues.map(function (issue) {
400
+ var properties = [
401
+ "file=".concat(escapeProperty(entry.displayPath)),
402
+ "line=".concat(issue.line + 1),
403
+ "col=".concat(issue.column + 1),
404
+ "endLine=".concat(issue.endLine + 1),
405
+ "endColumn=".concat(issue.endColumn + 1),
406
+ "title=".concat(escapeProperty("SAS ".concat(issue.severity.toUpperCase()).concat(issue.code ? " ".concat(issue.code) : '')))
407
+ ].join(',');
408
+ return "::".concat(annotationLevel(issue.severity), " ").concat(properties, "::").concat(escapeData(issue.message));
409
+ });
410
+ });
411
+ };
412
+ exports.toAnnotations = toAnnotations;
413
+ var annotationLevel = function (severity) {
414
+ return severity === 'error' ? 'error' : severity === 'warning' ? 'warning' : 'notice';
415
+ };
416
+ var escapeData = function (value) {
417
+ return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
418
+ };
419
+ var escapeProperty = function (value) {
420
+ return escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C');
421
+ };
422
+ var plural = function (count, noun) {
423
+ return "".concat(count, " ").concat(noun).concat(count === 1 ? '' : 's');
424
+ };
@@ -0,0 +1 @@
1
+ export {};