@git.zone/tstest 1.0.79

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 (34) hide show
  1. package/cli.js +4 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +1 -0
  5. package/dist_ts/index.js +10 -0
  6. package/dist_ts/tstest.classes.tap.combinator.d.ts +6 -0
  7. package/dist_ts/tstest.classes.tap.combinator.js +62 -0
  8. package/dist_ts/tstest.classes.tap.parser.d.ts +37 -0
  9. package/dist_ts/tstest.classes.tap.parser.js +158 -0
  10. package/dist_ts/tstest.classes.tap.testresult.d.ts +14 -0
  11. package/dist_ts/tstest.classes.tap.testresult.js +26 -0
  12. package/dist_ts/tstest.classes.testdirectory.d.ts +27 -0
  13. package/dist_ts/tstest.classes.testdirectory.js +35 -0
  14. package/dist_ts/tstest.classes.tstest.d.ts +14 -0
  15. package/dist_ts/tstest.classes.tstest.js +192 -0
  16. package/dist_ts/tstest.logprefixes.d.ts +3 -0
  17. package/dist_ts/tstest.logprefixes.js +6 -0
  18. package/dist_ts/tstest.paths.d.ts +3 -0
  19. package/dist_ts/tstest.paths.js +5 -0
  20. package/dist_ts/tstest.plugins.d.ts +17 -0
  21. package/dist_ts/tstest.plugins.js +23 -0
  22. package/npmextra.json +17 -0
  23. package/package.json +55 -0
  24. package/readme.md +61 -0
  25. package/ts/00_commitinfo_data.ts +8 -0
  26. package/ts/index.ts +10 -0
  27. package/ts/tstest.classes.tap.combinator.ts +65 -0
  28. package/ts/tstest.classes.tap.parser.ts +202 -0
  29. package/ts/tstest.classes.tap.testresult.ts +26 -0
  30. package/ts/tstest.classes.testdirectory.ts +57 -0
  31. package/ts/tstest.classes.tstest.ts +227 -0
  32. package/ts/tstest.logprefixes.ts +7 -0
  33. package/ts/tstest.paths.ts +5 -0
  34. package/ts/tstest.plugins.ts +42 -0
@@ -0,0 +1,202 @@
1
+ import { ChildProcess } from 'child_process';
2
+ import { coloredString as cs } from '@push.rocks/consolecolor';
3
+
4
+ // ============
5
+ // combines different tap test files to an overall result
6
+ // ============
7
+ import * as plugins from './tstest.plugins.js';
8
+ import { TapTestResult } from './tstest.classes.tap.testresult.js';
9
+ import * as logPrefixes from './tstest.logprefixes.js';
10
+
11
+ export class TapParser {
12
+ testStore: TapTestResult[] = [];
13
+
14
+ expectedTestsRegex = /([0-9]*)\.\.([0-9]*)$/;
15
+ expectedTests: number;
16
+ receivedTests: number;
17
+
18
+ testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*)\s#\stime=(.*)ms$/;
19
+ activeTapTestResult: TapTestResult;
20
+
21
+ /**
22
+ * the constructor for TapParser
23
+ */
24
+ constructor(public fileName: string) {}
25
+
26
+ private _getNewTapTestResult() {
27
+ this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
28
+ }
29
+
30
+ private _processLog(logChunk: Buffer | string) {
31
+ if (Buffer.isBuffer(logChunk)) {
32
+ logChunk = logChunk.toString();
33
+ }
34
+ const logLineArray = logChunk.split('\n');
35
+ if (logLineArray[logLineArray.length - 1] === '') {
36
+ logLineArray.pop();
37
+ }
38
+
39
+ // lets parse the log information
40
+ for (const logLine of logLineArray) {
41
+ let logLineIsTapProtocol = false;
42
+ if (!this.expectedTests && this.expectedTestsRegex.test(logLine)) {
43
+ logLineIsTapProtocol = true;
44
+ const regexResult = this.expectedTestsRegex.exec(logLine);
45
+ this.expectedTests = parseInt(regexResult[2]);
46
+ console.log(
47
+ `${logPrefixes.TapPrefix} ${cs(`Expecting ${this.expectedTests} tests!`, 'blue')}`
48
+ );
49
+
50
+ // initiating first TapResult
51
+ this._getNewTapTestResult();
52
+ } else if (this.testStatusRegex.test(logLine)) {
53
+ logLineIsTapProtocol = true;
54
+ const regexResult = this.testStatusRegex.exec(logLine);
55
+ const testId = parseInt(regexResult[2]);
56
+ const testOk = (() => {
57
+ if (regexResult[1] === 'ok') {
58
+ return true;
59
+ }
60
+ return false;
61
+ })();
62
+
63
+ const testSubject = regexResult[3];
64
+ const testDuration = parseInt(regexResult[4]);
65
+
66
+ // test for protocol error
67
+ if (testId !== this.activeTapTestResult.id) {
68
+ console.log(
69
+ `${logPrefixes.TapErrorPrefix} Something is strange! Test Ids are not equal!`
70
+ );
71
+ }
72
+ this.activeTapTestResult.setTestResult(testOk);
73
+
74
+ if (testOk) {
75
+ console.log(
76
+ logPrefixes.TapPrefix,
77
+ `${cs(`T${testId} ${plugins.figures.tick}`, 'green')} ${plugins.figures.arrowRight} ` +
78
+ cs(testSubject, 'blue') +
79
+ ` | ${cs(`${testDuration} ms`, 'orange')}`
80
+ );
81
+ } else {
82
+ console.log(
83
+ logPrefixes.TapPrefix,
84
+ `${cs(`T${testId} ${plugins.figures.cross}`, 'red')} ${plugins.figures.arrowRight} ` +
85
+ cs(testSubject, 'blue') +
86
+ ` | ${cs(`${testDuration} ms`, 'orange')}`
87
+ );
88
+ }
89
+ }
90
+
91
+ if (!logLineIsTapProtocol) {
92
+ if (this.activeTapTestResult) {
93
+ this.activeTapTestResult.addLogLine(logLine);
94
+ }
95
+ console.log(logLine);
96
+ }
97
+
98
+ if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
99
+ this.testStore.push(this.activeTapTestResult);
100
+ this._getNewTapTestResult();
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * returns all tests that are not completed
107
+ */
108
+ public getUncompletedTests() {
109
+ // TODO:
110
+ }
111
+
112
+ /**
113
+ * returns all tests that threw an error
114
+ */
115
+ public getErrorTests() {
116
+ return this.testStore.filter((tapTestArg) => {
117
+ return !tapTestArg.testOk;
118
+ });
119
+ }
120
+
121
+ /**
122
+ * returns a test overview as string
123
+ */
124
+ getTestOverviewAsString() {
125
+ let overviewString = '';
126
+ for (const test of this.testStore) {
127
+ if (overviewString !== '') {
128
+ overviewString += ' | ';
129
+ }
130
+ if (test.testOk) {
131
+ overviewString += cs(`T${test.id} ${plugins.figures.tick}`, 'green');
132
+ } else {
133
+ overviewString += cs(`T${test.id} ${plugins.figures.cross}`, 'red');
134
+ }
135
+ }
136
+ return overviewString;
137
+ }
138
+
139
+ /**
140
+ * handles a tap process
141
+ * @param childProcessArg
142
+ */
143
+ public async handleTapProcess(childProcessArg: ChildProcess) {
144
+ const done = plugins.smartpromise.defer();
145
+ childProcessArg.stdout.on('data', (data) => {
146
+ this._processLog(data);
147
+ });
148
+ childProcessArg.stderr.on('data', (data) => {
149
+ this._processLog(data);
150
+ });
151
+ childProcessArg.on('exit', async () => {
152
+ await this._evaluateResult();
153
+ done.resolve();
154
+ });
155
+ await done.promise;
156
+ }
157
+
158
+ public async handleTapLog(tapLog: string) {
159
+ this._processLog(tapLog);
160
+ await this._evaluateResult();
161
+ }
162
+
163
+ private async _evaluateResult() {
164
+ this.receivedTests = this.testStore.length;
165
+
166
+ // check wether all tests ran
167
+ if (this.expectedTests === this.receivedTests) {
168
+ console.log(
169
+ `${logPrefixes.TapPrefix} ${cs(
170
+ `${this.receivedTests} out of ${this.expectedTests} Tests completed!`,
171
+ 'green'
172
+ )}`
173
+ );
174
+ } else {
175
+ console.log(
176
+ `${logPrefixes.TapErrorPrefix} ${cs(
177
+ `Only ${this.receivedTests} out of ${this.expectedTests} completed!`,
178
+ 'red'
179
+ )}`
180
+ );
181
+ }
182
+ if (!this.expectedTests) {
183
+ console.log(cs('Error: No tests were defined. Therefore the testfile failed!', 'red'));
184
+ } else if (this.expectedTests !== this.receivedTests) {
185
+ console.log(
186
+ cs(
187
+ 'Error: The amount of received tests and expectedTests is unequal! Therefore the testfile failed',
188
+ 'red'
189
+ )
190
+ );
191
+ } else if (this.getErrorTests().length === 0) {
192
+ console.log(`${logPrefixes.TapPrefix} ${cs(`All tests are successfull!!!`, 'green')}`);
193
+ } else {
194
+ console.log(
195
+ `${logPrefixes.TapPrefix} ${cs(
196
+ `${this.getErrorTests().length} tests threw an error!!!`,
197
+ 'red'
198
+ )}`
199
+ );
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,26 @@
1
+ // ============
2
+ // combines different tap test files to an overall result
3
+ // ============
4
+ import * as plugins from './tstest.plugins.js';
5
+
6
+ export class TapTestResult {
7
+ testLogBuffer = Buffer.from('');
8
+ testOk: boolean = false;
9
+ testSettled: boolean = false;
10
+ constructor(public id: number) {}
11
+
12
+ /**
13
+ * adds a logLine to the log buffer of the test
14
+ * @param logLine
15
+ */
16
+ addLogLine(logLine: string) {
17
+ logLine = logLine + '\n';
18
+ const logLineBuffer = Buffer.from(logLine);
19
+ this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
20
+ }
21
+
22
+ setTestResult(testOkArg: boolean) {
23
+ this.testOk = testOkArg;
24
+ this.testSettled = true;
25
+ }
26
+ }
@@ -0,0 +1,57 @@
1
+ import * as plugins from './tstest.plugins.js';
2
+ import * as paths from './tstest.paths.js';
3
+ import { Smartfile } from '@push.rocks/smartfile';
4
+
5
+ // tap related stuff
6
+ import { TapCombinator } from './tstest.classes.tap.combinator.js';
7
+ import { TapParser } from './tstest.classes.tap.parser.js';
8
+ import { TapTestResult } from './tstest.classes.tap.testresult.js';
9
+
10
+ export class TestDirectory {
11
+ /**
12
+ * the current working directory
13
+ */
14
+ cwd: string;
15
+
16
+ /**
17
+ * the relative location of the test dir
18
+ */
19
+ relativePath: string;
20
+
21
+ /**
22
+ * the absolute path of the test dir
23
+ */
24
+ absolutePath: string;
25
+
26
+ /**
27
+ * an array of Smartfiles
28
+ */
29
+ testfileArray: Smartfile[] = [];
30
+
31
+ /**
32
+ * the constructor for TestDirectory
33
+ * tell it the path
34
+ * @param pathToTestDirectory
35
+ */
36
+ constructor(cwdArg: string, relativePathToTestDirectory: string) {
37
+ this.cwd = cwdArg;
38
+ this.relativePath = relativePathToTestDirectory;
39
+ }
40
+
41
+ private async _init() {
42
+ this.testfileArray = await plugins.smartfile.fs.fileTreeToObject(
43
+ plugins.path.join(this.cwd, this.relativePath),
44
+ 'test*.ts'
45
+ );
46
+ }
47
+
48
+ async getTestFilePathArray() {
49
+ await this._init();
50
+ const testFilePaths: string[] = [];
51
+ for (const testFile of this.testfileArray) {
52
+ const filePath = plugins.path.join(this.relativePath, testFile.path);
53
+ testFilePaths.push(filePath);
54
+ }
55
+ return testFilePaths;
56
+ }
57
+ }
@@ -0,0 +1,227 @@
1
+ import * as plugins from './tstest.plugins.js';
2
+ import * as paths from './tstest.paths.js';
3
+ import * as logPrefixes from './tstest.logprefixes.js';
4
+
5
+ import { coloredString as cs } from '@push.rocks/consolecolor';
6
+
7
+ import { TestDirectory } from './tstest.classes.testdirectory.js';
8
+ import { TapCombinator } from './tstest.classes.tap.combinator.js';
9
+ import { TapParser } from './tstest.classes.tap.parser.js';
10
+
11
+ export class TsTest {
12
+ public testDir: TestDirectory;
13
+
14
+ public smartshellInstance = new plugins.smartshell.Smartshell({
15
+ executor: 'bash',
16
+ pathDirectories: [paths.binDirectory],
17
+ sourceFilePaths: [],
18
+ });
19
+ public smartbrowserInstance = new plugins.smartbrowser.SmartBrowser();
20
+
21
+ public tsbundleInstance = new plugins.tsbundle.TsBundle();
22
+
23
+ constructor(cwdArg: string, relativePathToTestDirectory: string) {
24
+ this.testDir = new TestDirectory(cwdArg, relativePathToTestDirectory);
25
+ }
26
+
27
+ async run() {
28
+ const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
29
+ console.log(cs(plugins.figures.hamburger.repeat(80), 'cyan'));
30
+ console.log('');
31
+ console.log(`${logPrefixes.TsTestPrefix} FOUND ${fileNamesToRun.length} TESTFILE(S):`);
32
+ for (const fileName of fileNamesToRun) {
33
+ console.log(`${logPrefixes.TsTestPrefix} ${cs(fileName, 'orange')}`);
34
+ }
35
+ console.log('-'.repeat(48));
36
+ console.log(''); // force new line
37
+
38
+ const tapCombinator = new TapCombinator(); // lets create the TapCombinator
39
+ for (const fileNameArg of fileNamesToRun) {
40
+ switch (true) {
41
+ case process.env.CI && fileNameArg.includes('.nonci.'):
42
+ console.log('!!!!!!!!!!!');
43
+ console.log(
44
+ `not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag`
45
+ );
46
+ console.log('!!!!!!!!!!!');
47
+ break;
48
+ case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
49
+ const tapParserBrowser = await this.runInChrome(fileNameArg);
50
+ tapCombinator.addTapParser(tapParserBrowser);
51
+ break;
52
+ case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
53
+ console.log('>>>>>>> TEST PART 1: chrome');
54
+ const tapParserBothBrowser = await this.runInChrome(fileNameArg);
55
+ tapCombinator.addTapParser(tapParserBothBrowser);
56
+ console.log(cs(`|`.repeat(16), 'cyan'));
57
+ console.log(''); // force new line
58
+ console.log('>>>>>>> TEST PART 2: node');
59
+ const tapParserBothNode = await this.runInNode(fileNameArg);
60
+ tapCombinator.addTapParser(tapParserBothNode);
61
+ break;
62
+ default:
63
+ const tapParserNode = await this.runInNode(fileNameArg);
64
+ tapCombinator.addTapParser(tapParserNode);
65
+ break;
66
+ }
67
+
68
+ console.log(cs(`^`.repeat(16), 'cyan'));
69
+ console.log(''); // force new line
70
+ }
71
+ tapCombinator.evaluate();
72
+ }
73
+
74
+ public async runInNode(fileNameArg: string): Promise<TapParser> {
75
+ console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in node.js runtime.`);
76
+ console.log(`${cs(`= `.repeat(32), 'cyan')}`);
77
+ const tapParser = new TapParser(fileNameArg + ':node');
78
+
79
+ // tsrun options
80
+ let tsrunOptions = '';
81
+ if (process.argv.includes('--web')) {
82
+ tsrunOptions += ' --web';
83
+ }
84
+
85
+ const execResultStreaming = await this.smartshellInstance.execStreamingSilent(
86
+ `tsrun ${fileNameArg}${tsrunOptions}`
87
+ );
88
+ await tapParser.handleTapProcess(execResultStreaming.childProcess);
89
+ return tapParser;
90
+ }
91
+
92
+ public async runInChrome(fileNameArg: string): Promise<TapParser> {
93
+ console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in chromium runtime.`);
94
+ console.log(`${cs(`= `.repeat(32), 'cyan')}`);
95
+
96
+ // lets get all our paths sorted
97
+ const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
98
+ const bundleFileName = fileNameArg.replace('/', '__') + '.js';
99
+ const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
100
+
101
+ // lets bundle the test
102
+ await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
103
+ await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, {
104
+ bundler: 'esbuild',
105
+ });
106
+
107
+ // lets create a server
108
+ const server = new plugins.typedserver.servertools.Server({
109
+ cors: true,
110
+ port: 3007,
111
+ });
112
+ server.addRoute(
113
+ '/test',
114
+ new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
115
+ res.type('.html');
116
+ res.write(`
117
+ <html>
118
+ <head>
119
+ <script>
120
+ globalThis.testdom = true;
121
+ </script>
122
+ </head>
123
+ <body></body>
124
+ </html>
125
+ `);
126
+ res.end();
127
+ })
128
+ );
129
+ server.addRoute('*', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
130
+ await server.start();
131
+
132
+ // lets do the browser bit
133
+ await this.smartbrowserInstance.start();
134
+ const evaluation = await this.smartbrowserInstance.evaluateOnPage(
135
+ `http://localhost:3007/test?bundleName=${bundleFileName}`,
136
+ async () => {
137
+ const convertToText = (obj: any): string => {
138
+ // create an array that will later be joined into a string.
139
+ const stringArray: string[] = [];
140
+
141
+ if (typeof obj === 'object' && typeof obj.toString === 'function') {
142
+ stringArray.push(obj.toString());
143
+ } else if (typeof obj === 'object' && obj.join === undefined) {
144
+ stringArray.push('{');
145
+ for (const prop of Object.keys(obj)) {
146
+ stringArray.push(prop, ': ', convertToText(obj[prop]), ',');
147
+ }
148
+ stringArray.push('}');
149
+
150
+ // is array
151
+ } else if (typeof obj === 'object' && !(obj.join === undefined)) {
152
+ stringArray.push('[');
153
+ for (const prop of Object.keys(obj)) {
154
+ stringArray.push(convertToText(obj[prop]), ',');
155
+ }
156
+ stringArray.push(']');
157
+
158
+ // is function
159
+ } else if (typeof obj === 'function') {
160
+ stringArray.push(obj.toString());
161
+
162
+ // all other values can be done with JSON.stringify
163
+ } else {
164
+ stringArray.push(JSON.stringify(obj));
165
+ }
166
+
167
+ return stringArray.join('');
168
+ };
169
+
170
+ let logStore = '';
171
+ // tslint:disable-next-line: max-classes-per-file
172
+ const log = console.log.bind(console);
173
+ console.log = (...args) => {
174
+ args = args.map((argument) => {
175
+ return typeof argument !== 'string' ? convertToText(argument) : argument;
176
+ });
177
+ logStore += `${args}\n`;
178
+ log(...args);
179
+ };
180
+ const error = console.error;
181
+ console.error = (...args) => {
182
+ args = args.map((argument) => {
183
+ return typeof argument !== 'string' ? convertToText(argument) : argument;
184
+ });
185
+ logStore += `${args}\n`;
186
+ error(...args);
187
+ };
188
+ const bundleName = new URLSearchParams(window.location.search).get('bundleName');
189
+ console.log(`::TSTEST IN CHROMIUM:: Relevant Script name is: ${bundleName}`);
190
+ const bundleResponse = await fetch(`/${bundleName}`);
191
+ console.log(
192
+ `::TSTEST IN CHROMIUM:: Got ${bundleName} with STATUS ${bundleResponse.status}`
193
+ );
194
+ const bundle = await bundleResponse.text();
195
+ console.log(`::TSTEST IN CHROMIUM:: Executing ${bundleName}`);
196
+ try {
197
+ // tslint:disable-next-line: no-eval
198
+ eval(bundle);
199
+ } catch (err) {
200
+ console.error(err);
201
+ }
202
+
203
+ if (
204
+ (globalThis as any).tapbundleDeferred &&
205
+ (globalThis as any).tapbundleDeferred.promise
206
+ ) {
207
+ await (globalThis as any).tapbundleDeferred.promise;
208
+ } else {
209
+ console.log('Error: Could not find tapbundle Deferred');
210
+ }
211
+ return logStore;
212
+ }
213
+ );
214
+ await this.smartbrowserInstance.stop();
215
+ await server.stop();
216
+ console.log(
217
+ `${cs('=> ', 'blue')} Stopped ${cs(fileNameArg, 'orange')} chromium instance and server.`
218
+ );
219
+ console.log(`${cs('=> ', 'blue')} See the result captured from the chromium execution:`);
220
+ // lets create the tap parser
221
+ const tapParser = new TapParser(fileNameArg + ':chrome');
222
+ tapParser.handleTapLog(evaluation);
223
+ return tapParser;
224
+ }
225
+
226
+ public async runInDeno() {}
227
+ }
@@ -0,0 +1,7 @@
1
+ import * as plugins from './tstest.plugins.js';
2
+ import { coloredString as cs } from '@push.rocks/consolecolor';
3
+
4
+ export const TapPrefix = cs(`::TAP::`, 'pink', 'black');
5
+ export const TapErrorPrefix = cs(` !!!TAP PROTOCOL ERROR!!! `, 'red', 'black');
6
+
7
+ export const TsTestPrefix = cs(`**TSTEST**`, 'pink', 'black');
@@ -0,0 +1,5 @@
1
+ import * as plugins from './tstest.plugins.js';
2
+
3
+ export const cwd = process.cwd();
4
+ export const testDir = plugins.path.join(cwd, './test/');
5
+ export const binDirectory = plugins.path.join(cwd, './node_modules/.bin');
@@ -0,0 +1,42 @@
1
+ // node native
2
+ import * as path from 'path';
3
+
4
+ export { path };
5
+
6
+ // @apiglobal scope
7
+ import * as typedserver from '@apiglobal/typedserver';
8
+
9
+ export {
10
+ typedserver
11
+ }
12
+
13
+ // @push.rocks scope
14
+ import * as consolecolor from '@push.rocks/consolecolor';
15
+ import * as smartbrowser from '@push.rocks/smartbrowser';
16
+ import * as smartdelay from '@push.rocks/smartdelay';
17
+ import * as smartfile from '@push.rocks/smartfile';
18
+ import * as smartlog from '@push.rocks/smartlog';
19
+ import * as smartpromise from '@push.rocks/smartpromise';
20
+ import * as smartshell from '@push.rocks/smartshell';
21
+ import * as tapbundle from '@push.rocks/tapbundle';
22
+
23
+ export {
24
+ consolecolor,
25
+ smartbrowser,
26
+ smartdelay,
27
+ smartfile,
28
+ smartlog,
29
+ smartpromise,
30
+ smartshell,
31
+ tapbundle,
32
+ };
33
+
34
+ // @gitzone scope
35
+ import * as tsbundle from '@gitzone/tsbundle';
36
+
37
+ export { tsbundle };
38
+
39
+ // sindresorhus
40
+ import figures from 'figures';
41
+
42
+ export { figures };