Skip to content

Commit f09deff

Browse files
committed
refactor: move parser to different file
1 parent db5f36e commit f09deff

File tree

4 files changed

+113
-96
lines changed

4 files changed

+113
-96
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.formatOnSave": true
3+
}

cli-examples/__tests__/prompts.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const path = require('path');
22
const { createCommandInterface } = require('../../lib');
33

4-
test('should pass', async () => {
4+
test('should block 12 year old', async () => {
55
const commandInterface = createCommandInterface('node ./prompts.js', {
66
cwd: path.join(__dirname, '..'),
77
});

lib/cli-ansi-parser.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// types
2+
/**
3+
* @typedef {({
4+
* textAfter: (question: string) => string,
5+
* stringOutput: string,
6+
* tokenizedOutput: string,
7+
* rawOutput: string
8+
* })} ParsedOutput
9+
*
10+
* @typedef {({
11+
* typeDelay: number,
12+
* logData: boolean,
13+
* cwd: string,
14+
* env: any
15+
* })} Options
16+
*/
17+
18+
// Big shoutout to https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 for super cool reference to all ansi codes
19+
const ESC = "(?:\\x1[bB])|(?:\\u001b)\\[";
20+
const ESC_NO_BRACKET = ESC.replace("\\[", "");
21+
const STRING_ESC = "\x1b[";
22+
const ANSI_CHARACTER_MAPS = {
23+
// Cursor and Line erase
24+
ERASE_LINE: `${ESC}2K`,
25+
CURSOR_LEFT: `${ESC}1G`,
26+
CURSOR_UP: `${ESC}1A`,
27+
CURSOR_DOWN: `${ESC}1B`,
28+
CURSOR_VISIBLE: `${ESC}\\?25h`,
29+
CURSOR_INVISIBLE: `${ESC}\\?25l`,
30+
CURSOR_STORE: `${ESC_NO_BRACKET}7`,
31+
CURSOR_RESTORE: `${ESC_NO_BRACKET}8`,
32+
// Colors
33+
RED_START: `${ESC}31m`,
34+
GREEN_START: `${ESC}32m`,
35+
YELLOW_START: `${ESC}33m`,
36+
BLUE_START: `${ESC}34m`,
37+
CYAN_START: `${ESC}36m`,
38+
GREY_START: `${ESC}90m`,
39+
COLOR_END: `${ESC}39m`,
40+
// Graphics
41+
BOLD_START: `${ESC}1m`,
42+
BOLD_END: `${ESC}22m`,
43+
ITALIC_START: `${ESC}3m`,
44+
ITALIC_END: `${ESC}23m`,
45+
UNDERLINE_START: `${ESC}4m`,
46+
UNDERLINE_END: `${ESC}24m`,
47+
};
48+
49+
const parseOutput = (output) => {
50+
const textAfter = (question) => {
51+
const questionIndex = output.indexOf(question) + question.length;
52+
const endlineIndex = output.indexOf("\n", questionIndex + 1);
53+
const cleanEndlineIndex = endlineIndex <= 0 ? undefined : endlineIndex;
54+
return output.slice(questionIndex, cleanEndlineIndex).trim();
55+
};
56+
57+
const tokenizeOutput = () => {
58+
let out = output;
59+
for (const [
60+
ESCAPE_CHARACTER_NAME,
61+
ESCAPE_CHARACTER_REGEX,
62+
] of Object.entries(ANSI_CHARACTER_MAPS)) {
63+
out = out.replace(
64+
new RegExp(`(${ESCAPE_CHARACTER_REGEX})`, "g"),
65+
`[${ESCAPE_CHARACTER_NAME}]`
66+
);
67+
}
68+
69+
return out;
70+
};
71+
72+
const finalString = () => {
73+
let parsedOutput = tokenizeOutput();
74+
const lastEraseLineIndex = parsedOutput.lastIndexOf("[ERASE_LINE]");
75+
const outputAfterLastEraseLine = parsedOutput.slice(
76+
lastEraseLineIndex > 0 ? lastEraseLineIndex : 0
77+
);
78+
return outputAfterLastEraseLine.replace(/\[(\w*)\]/g, "");
79+
};
80+
81+
return {
82+
textAfter,
83+
rawOutput: output,
84+
tokenizedOutput: tokenizeOutput(),
85+
stringOutput: finalString(),
86+
};
87+
};
88+
89+
module.exports = { parseOutput, STRING_ESC, ANSI_CHARACTER_MAPS };

lib/cli-testing-library.js

Lines changed: 20 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,51 @@
1-
const { spawn } = require('child_process');
1+
const { spawn } = require("child_process");
2+
const { parseOutput } = require("./cli-ansi-parser");
23

3-
// types
4-
/**
5-
* @typedef {({
6-
* textAfter: (question: string) => string,
7-
* stringOutput: string,
8-
* tokenizedOutput: string,
9-
* rawOutput: string
10-
* })} ParsedOutput
11-
*
12-
* @typedef {({
13-
* typeDelay: number,
14-
* logData: boolean,
15-
* cwd: string,
16-
* env: any
17-
* })} Options
18-
*/
19-
20-
// Big shoutout to https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 for super cool reference to all ansi codes
21-
const ESC = '(?:\\x1[bB])|(?:\\u001b)\\[';
22-
const ESC_NO_BRACKET = ESC.replace('\\[', '');
23-
const STRING_ESC = '\x1b[';
24-
const ANSI_CHARACTER_MAPS = {
25-
ERASE_LINE: `${ESC}2K`,
26-
CURSOR_LEFT: `${ESC}1G`,
27-
CURSOR_UP: `${ESC}1A`,
28-
CURSOR_DOWN: `${ESC}1B`,
29-
CURSOR_VISIBLE: `${ESC}\\?25h`,
30-
CURSOR_INVISIBLE: `${ESC}\\?25l`,
31-
CURSOR_STORE: `${ESC_NO_BRACKET}7`,
32-
CURSOR_RESTORE: `${ESC_NO_BRACKET}8`,
33-
UNDERLINE: `${ESC}4m`,
34-
// Colors
35-
RED_START: `${ESC}31m`,
36-
GREEN_START: `${ESC}32m`,
37-
YELLOW_START: `${ESC}33m`,
38-
BLUE_START: `${ESC}34m`,
39-
CYAN_START: `${ESC}36m`,
40-
GREY_START: `${ESC}90m`,
41-
COLOR_END: `${ESC}39m`,
42-
BOLD_START: `${ESC}1m`,
43-
BOLD_END: `${ESC}22m`,
44-
BOLD_2_START: `${ESC}3m`,
45-
BOLD_2_END: `${ESC}23m`,
46-
}
47-
48-
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
49-
50-
const parseOutput = (output) => {
51-
const textAfter = (question) => {
52-
const questionIndex = output.indexOf(question) + question.length;
53-
const endlineIndex = output.indexOf('\n', questionIndex + 1);
54-
const cleanEndlineIndex = endlineIndex <= 0 ? undefined : endlineIndex;
55-
return output.slice(questionIndex, cleanEndlineIndex).trim();
56-
};
57-
58-
const tokenizeOutput = () => {
59-
let out = output;
60-
for (const [ESCAPE_CHARACTER_NAME, ESCAPE_CHARACTER_REGEX] of Object.entries(ANSI_CHARACTER_MAPS)) {
61-
out = out.replace(new RegExp(`(${ESCAPE_CHARACTER_REGEX})`, 'g'), `[${ESCAPE_CHARACTER_NAME}]`);
62-
}
63-
64-
return out;
65-
}
66-
67-
const finalString = () => {
68-
let parsedOutput = tokenizeOutput();
69-
const lastEraseLineIndex = parsedOutput.lastIndexOf('[ERASE_LINE]')
70-
const outputAfterLastEraseLine = parsedOutput.slice(lastEraseLineIndex > 0 ? lastEraseLineIndex : 0);
71-
return outputAfterLastEraseLine.replace(/\[(\w*)\]/g, '');
72-
}
73-
74-
return {
75-
textAfter,
76-
rawOutput: output,
77-
tokenizedOutput: tokenizeOutput(),
78-
stringOutput: finalString(),
79-
};
80-
};
4+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
815

826
/**
83-
*
84-
* @param {string} commandString
85-
* @param {Options} options
7+
*
8+
* @param {string} commandString
9+
* @param {Options} options
8610
*/
8711
const createCommandInterface = (commandString, options) => {
8812
// /** @type {import('child_process').ChildProcessWithoutNullStreams} */
8913
// let command;
90-
const commandArgs = commandString.split(' ');
14+
const commandArgs = commandString.split(" ");
9115
const command = spawn(commandArgs[0], commandArgs.slice(1), {
9216
detached: true,
93-
stdio: 'pipe',
17+
stdio: "pipe",
9418
cwd: options.cwd || process.cwd(),
9519
env: options.env || undefined,
9620
});
9721

98-
let outputs = '';
22+
let outputs = "";
9923
let isFinishTypingCalled = false;
10024

101-
command.stdout.on('data', (data) => {
25+
command.stdout.on("data", (data) => {
10226
if (options.logData) {
10327
console.log(data.toString());
10428
}
10529
outputs += data.toString();
10630
});
10731

108-
command.stderr.on('data', (error) => {
32+
command.stderr.on("data", (error) => {
10933
if (options.logData) {
11034
console.error(error.toString());
11135
}
11236
outputs += error.toString();
11337
});
11438

115-
command.on('error', (error) => {
39+
command.on("error", (error) => {
11640
throw error;
11741
});
11842

11943
const type = async (text) => {
12044
if (isFinishTypingCalled) {
121-
throw new Error('[cli-testing-library]: `type` cannot be called after `getOutput` or `finishTyping`');
122-
};
45+
throw new Error(
46+
"[cli-testing-library]: `type` cannot be called after `getOutput` or `finishTyping`"
47+
);
48+
}
12349

12450
await wait(options.typeDelay ? options.typeDelay : 100);
12551

@@ -131,8 +57,8 @@ const createCommandInterface = (commandString, options) => {
13157
};
13258

13359
const keys = {
134-
enter: () => type('\n'),
135-
arrowDown: () => type(`${STRING_ESC}1B`),
60+
enter: () => type("\n"),
61+
arrowDown: () => type(`${STRING_ESC}1B`),
13662
arrowUp: () => type(`${STRING_ESC}1A`),
13763
};
13864

@@ -144,7 +70,7 @@ const createCommandInterface = (commandString, options) => {
14470
finishTyping();
14571
}
14672
return new Promise((resolve) => {
147-
command.stdout.on('end', () => {
73+
command.stdout.on("end", () => {
14874
return resolve(parseOutput(outputs.trim()));
14975
});
15076
});
@@ -160,12 +86,11 @@ const createCommandInterface = (commandString, options) => {
16086
finishTyping,
16187
getOutput,
16288
command,
163-
keys
89+
keys,
16490
};
16591
};
16692

16793
module.exports = {
16894
createCommandInterface,
16995
parseOutput,
17096
};
171-

0 commit comments

Comments
 (0)