Skip to content

Commit 3026d11

Browse files
Merge pull request #229 from beakerandjake/feature/inquirer-update
Feature/inquirer update
2 parents 1d46a4f + 8a0123e commit 3026d11

14 files changed

+6008
-4919
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Added support for 2023 to the config file ([#226](https://github.com/beakerandjake/advent-of-code-runner/issues/226))
1111
### Fixed
1212
- Fix number of solvable problems to be 49 not 50 (day 25 only has one level) ([#224](https://github.com/beakerandjake/advent-of-code-runner/issues/224))
13-
13+
### Changed
14+
- Replaced inquirer package with @inquirer/prompts, will reduce package size. ([#230](https://github.com/beakerandjake/advent-of-code-runner/issues/230))
1415

1516
## [1.3.6] - 2023-08-23
1617
### Fixed

package-lock.json

Lines changed: 5772 additions & 4764 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"homepage": "https://github.com/beakerandjake/advent-of-code-runner",
3535
"dependencies": {
36+
"@inquirer/prompts": "^3.3.0",
3637
"chalk": "^5.2.0",
3738
"commander": "^9.4.1",
3839
"date-fns": "^2.29.3",
@@ -41,7 +42,6 @@
4142
"dotenv": "^16.0.3",
4243
"fs-extra": "^11.1.0",
4344
"htmlparser2": "^8.0.1",
44-
"inquirer": "^9.1.4",
4545
"ora": "^6.1.2",
4646
"table": "^6.8.1",
4747
"terminal-link": "^3.0.0",
Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
import inquirer from 'inquirer';
1+
import { confirm } from '@inquirer/prompts';
22

33
/**
44
* Creates a link which when invoked will ask the user the specified question.
55
* The link will return a boolean value based on if the user answered yes to the question.
6-
* @param {Object} question - The question passed to inquirer
6+
* @param {Object} confirmQuestion - The question passed to the confirm prompt
7+
* @param {string} confirmQuestion.message - The question to ask
8+
* @param {boolean} confirmQuestion.default - The default answer (true or false)
79
*/
8-
export const assertUserConfirmation = (question) => {
9-
if (question == null) {
10-
throw new Error('null or undefined question');
10+
export const assertUserConfirmation = async ({ confirmQuestion } = {}) => {
11+
if (!confirmQuestion || !confirmQuestion.message) {
12+
throw new Error('must specify a message');
1113
}
12-
13-
// create a variable for this fn instead of just returning the fn
14-
// this gives the fn a .name property and makes debugging easier.
15-
const _ = {
16-
assertUserConfirmation: async () => {
17-
const { confirmed } = await inquirer.prompt({ ...question, type: 'confirm' });
18-
return confirmed;
19-
},
20-
};
21-
return _.assertUserConfirmation;
14+
return confirm(confirmQuestion);
2215
};

src/actions/getAnswersFromUser.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
1-
import inquirer from 'inquirer';
1+
import { confirm, select, password } from '@inquirer/prompts';
2+
3+
/**
4+
* Prompt the user with an inquirer question.
5+
*/
6+
const prompt = (type, args) => {
7+
switch (type) {
8+
case 'confirm':
9+
return confirm(args);
10+
case 'select':
11+
return select(args);
12+
case 'password':
13+
return password(args);
14+
default:
15+
throw new Error(`unsupported question type: ${type}`);
16+
}
17+
};
218

319
/**
420
* Creates a link which, when invoked will ask the user the specified questions.
521
* The link will add the user answers to the args.
6-
* @param {Object[]} questions - The inquirer.js questions to ask the user
22+
* @param {Object} questions - The inquirer.js questions to ask the user
723
*/
8-
export const getAnswersFromUser = (questions = []) => {
9-
if (questions == null) {
24+
export const getAnswersFromUser = async ({ questions } = {}) => {
25+
if (!questions) {
1026
throw new Error('null or undefined question');
1127
}
12-
13-
// create a variable for this fn instead of just returning the fn
14-
// this gives the fn a .name property and makes debugging easier.
15-
const _ = {
16-
getAnswersFromUser: async () => ({ answers: await inquirer.prompt(questions) }),
17-
};
18-
19-
return _.getAnswersFromUser;
28+
const answers = {};
29+
for (const [key, { type, ...args }] of Object.entries(questions)) {
30+
// eslint-disable-next-line no-await-in-loop
31+
answers[key] = await prompt(type, args);
32+
}
33+
return answers;
2034
};

src/cli/auth.js

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,52 @@
11
import { Command } from 'commander';
2-
import { assertInitialized } from '../actions/assertInitialized.js';
3-
import { assertUserConfirmation } from '../actions/assertUserConfirmation.js';
4-
import { getAnswersFromUser } from '../actions/getAnswersFromUser.js';
5-
import { festiveEmoji, festiveStyle, printFestiveTitle } from '../festive.js';
2+
import { createChain } from '../actions/actionChain.js';
3+
import * as actions from '../actions/index.js';
4+
import { festiveStyle, printFestiveTitle } from '../festive.js';
65
import { createDotEnv } from '../initialize/index.js';
7-
import { dotEnvExists } from '../validation/userFilesExist.js';
86
import { logger } from '../logger.js';
7+
import { dotEnvExists } from '../validation/userFilesExist.js';
98

109
/**
11-
* inquirer question which prompts the user for their auth token.
10+
* inquirer questions which can be used to prompt the user to input their auth token.
1211
*/
13-
/* istanbul ignore next */
1412
export const authTokenQuestion = {
1513
type: 'password',
16-
name: 'authToken',
1714
message: festiveStyle(
1815
'Enter your advent of code authentication token (see README for help)'
1916
),
20-
prefix: festiveEmoji(),
17+
mask: true,
2118
validate: (input) => (input ? true : 'Token cannot be empty!'),
22-
filter: (input) => input.trim(),
2319
};
2420

2521
/**
26-
* inquirer.js question which makes the user confirm the overwriting the .env file
22+
* inquirer question which makes the user confirm the overwriting the .env file
2723
*/
28-
const confirmOverwriteQuestion = {
29-
type: 'confirm',
30-
name: 'confirmed',
24+
const confirmQuestion = {
3125
message: festiveStyle(
3226
'It appears a .env file is already present, do you want to overwrite this file?'
3327
),
3428
default: false,
35-
prefix: festiveEmoji(),
3629
};
3730

3831
/**
3932
* Updates or creates the .env file and writes the users auth token to it.
4033
* @private
4134
*/
42-
export const auth = async () => {
43-
// don't let the user run this command if they haven't ran the "init" command.
44-
if (!(await assertInitialized())) {
45-
return;
46-
}
35+
export const authAction = async () => {
36+
const actionChain = createChain([
37+
actions.assertInitialized,
38+
actions.and(dotEnvExists, actions.assertUserConfirmation),
39+
actions.getAnswersFromUser,
40+
createDotEnv,
41+
]);
4742

48-
// if there is already a .env file then ask users confirmation.
49-
if (
50-
(await dotEnvExists()) &&
51-
!(await assertUserConfirmation(confirmOverwriteQuestion)())
52-
) {
53-
return;
54-
}
43+
await actionChain({
44+
confirmQuestion,
45+
questions: {
46+
authToken: authTokenQuestion,
47+
},
48+
});
5549

56-
const { answers } = await getAnswersFromUser([authTokenQuestion])();
57-
await createDotEnv(answers);
5850
logger.festive(
5951
'Added auth token to the .env file, do not commit this file to source control'
6052
);
@@ -67,4 +59,4 @@ export const authCommand = new Command()
6759
.name('auth')
6860
.hook('preAction', printFestiveTitle)
6961
.description('Add or update the .env file with your advent of code auth token.')
70-
.action(auth);
62+
.action(authAction);

src/cli/initialize.js

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Command } from 'commander';
22
import { createChainWithProgress } from '../actions/actionChainWithProgress.js';
33
import { assertUserConfirmation, getAnswersFromUser } from '../actions/index.js';
44
import { getConfigValue } from '../config.js';
5-
import { festiveEmoji, festiveStyle, printFestiveTitle } from '../festive.js';
5+
import { festiveStyle, printFestiveTitle } from '../festive.js';
66
import {
77
createDataFile,
88
createDotEnv,
@@ -19,32 +19,26 @@ import { authTokenQuestion } from './auth.js';
1919
/**
2020
* inquirer.js question which makes the user confirm the initialize operation.
2121
*/
22-
const confirmInitializeQuestion = {
22+
const confirmQuestion = {
2323
type: 'confirm',
24-
name: 'confirmed',
2524
message: festiveStyle(
2625
'This directory is not empty! This operation will overwrite files, do you want to continue?'
2726
),
2827
default: false,
29-
prefix: festiveEmoji(),
3028
};
3129

32-
/**
33-
* Array of inquirer questions which will be asked in order.
34-
* The answers will provide us all of the information we need to initialize.
35-
*/
36-
const initializeQuestions = [
37-
{
30+
const initializeQuestions = {
31+
year: {
3832
// in future if list of years becomes too large the change to raw input.
39-
type: 'list',
40-
name: 'year',
33+
type: 'select',
4134
message: festiveStyle('What year of advent of code are you doing?'),
42-
prefix: festiveEmoji(),
43-
choices: [...getConfigValue('aoc.validation.years')].reverse(),
35+
choices: [...getConfigValue('aoc.validation.years')]
36+
.reverse()
37+
.map((year) => ({ value: year })),
4438
loop: false,
4539
},
46-
authTokenQuestion,
47-
];
40+
authToken: authTokenQuestion,
41+
};
4842

4943
/**
5044
* A Link which creates all required files in the cwd.
@@ -67,15 +61,12 @@ const createFiles = async ({ answers }) => {
6761
*/
6862
export const initializeAction = async () => {
6963
// if there are files in the cwd, get confirmation with the user that they want to proceed.
70-
if (
71-
!(await cwdIsEmpty()) &&
72-
!(await assertUserConfirmation(confirmInitializeQuestion)())
73-
) {
64+
if (!(await cwdIsEmpty()) && !(await assertUserConfirmation({ confirmQuestion }))) {
7465
return;
7566
}
7667

7768
// get all the info we need in order to initialize.
78-
const { answers } = await getAnswersFromUser(initializeQuestions)();
69+
const answers = await getAnswersFromUser({ questions: initializeQuestions });
7970

8071
// run initialize steps in an action chain that reports its progress to the user.
8172
const actionChain = createChainWithProgress(

src/initialize/createDotEnv.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export const createDotEnv = async ({ authToken } = {}) => {
2323

2424
const { source, dest } = getConfigValue('paths.templates.dotenv');
2525
const templateEnvFileContents = await readFile(source, { encoding: 'utf-8' });
26-
const envFile = replaceTokens(envFileTokens, { authToken }, templateEnvFileContents);
26+
const envFile = replaceTokens(
27+
envFileTokens,
28+
{ authToken: authToken.trim() },
29+
templateEnvFileContents
30+
);
2731
logger.debug('saving .env file to: %s', dest);
2832
await outputFile(dest, envFile);
2933
};
Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { describe, jest, test, afterEach } from '@jest/globals';
22

33
// setup mocks
4-
const mockPrompt = jest.fn();
5-
jest.unstable_mockModule('inquirer', () => ({
6-
default: {
7-
prompt: mockPrompt,
8-
},
4+
jest.unstable_mockModule('@inquirer/prompts', () => ({
5+
confirm: jest.fn(),
96
}));
107

118
// import after setting up mocks
9+
const { confirm } = await import('@inquirer/prompts');
1210
const { assertUserConfirmation } = await import(
1311
'../../src/actions/assertUserConfirmation.js'
1412
);
@@ -18,26 +16,40 @@ describe('assertUserConfirmation()', () => {
1816
jest.resetAllMocks();
1917
});
2018

21-
test.each([null, undefined])('throws if question is %s', (question) => {
22-
expect(() => assertUserConfirmation(question)).toThrow();
19+
test.each([
20+
null,
21+
undefined,
22+
{},
23+
{ confirmQuestion: null },
24+
{ confirmQuestion: {} },
25+
{ confirmQuestion: { message: '' } },
26+
{ confirmQuestion: { message: null } },
27+
])('throws if arg is %s', async (args) => {
28+
await expect(async () => assertUserConfirmation(args)).rejects.toThrow();
2329
});
2430

25-
test('builds and returns function', () => {
26-
const result = assertUserConfirmation({});
27-
expect(result).toBeInstanceOf(Function);
31+
test('invokes confirm only once', async () => {
32+
await assertUserConfirmation({ confirmQuestion: { message: 'hi' } });
33+
expect(confirm).toHaveBeenCalledTimes(1);
2834
});
2935

30-
test('returns true if user confirms', async () => {
31-
mockPrompt.mockResolvedValue({ confirmed: true });
32-
const fn = assertUserConfirmation({});
33-
const result = await fn();
36+
test('passes args to confirm', async () => {
37+
const input = { confirmQuestion: { message: 'hi' } };
38+
await assertUserConfirmation(input);
39+
expect(confirm).toBeCalledWith(input.confirmQuestion);
40+
});
41+
42+
test('returns true if confirm returns true', async () => {
43+
confirm.mockResolvedValue(true);
44+
const input = { confirmQuestion: { message: 'hi' } };
45+
const result = await assertUserConfirmation(input);
3446
expect(result).toBe(true);
3547
});
3648

37-
test('returns false if user does not confirm', async () => {
38-
mockPrompt.mockResolvedValue({ confirmed: false });
39-
const fn = assertUserConfirmation({});
40-
const result = await fn();
49+
test('returns false if confirm returns false', async () => {
50+
confirm.mockResolvedValue(false);
51+
const input = { confirmQuestion: { message: 'hi' } };
52+
const result = await assertUserConfirmation(input);
4153
expect(result).toBe(false);
4254
});
4355
});

tests/actions/executeUserSolution.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ describe('executeUserSolution()', () => {
1616
jest.resetAllMocks();
1717
});
1818

19-
test.each([null, undefined])('throws if question is %s', async (question) => {
20-
await expect(async () => executeUserSolution(question)).rejects.toThrow();
19+
test.each([null, undefined])('throws if provided %s', async (args) => {
20+
await expect(async () => executeUserSolution(args)).rejects.toThrow();
2121
});
2222

2323
test('returns results', async () => {

0 commit comments

Comments
 (0)