diff --git a/modules/setup.js b/modules/setup.js index 485a7dd..06a294b 100644 --- a/modules/setup.js +++ b/modules/setup.js @@ -5,7 +5,7 @@ /* api */ import path from 'node:path'; import process from 'node:process'; -import readline from 'readline-sync'; +import { confirm, input } from '@inquirer/prompts'; import { CmdArgs, Setup, createFile, getStat, isDir, isExecutable, isFile } from 'web-ext-native-msg'; @@ -19,6 +19,12 @@ const CHAR = 'utf8'; const INDENT = 2; const PERM_FILE = 0o644; +/* wrap inquirer (for test) */ +export const inquirer = { + confirm, + input +}; + /* setup command options */ export const setupOpts = new Map(); @@ -42,11 +48,19 @@ export const handleCmdArgsInput = async editorArgs => { if (Array.isArray(editorArgs)) { cmdArgs = editorArgs; } else { - const useCmdArgs = - readline.keyInYNStrict('Execute editor with command line options?'); + const useCmdArgs = await inquirer.confirm({ + message: 'Execute editor with command line options?', + default: false + }); if (useCmdArgs) { - const ans = readline.question('Input command line options: '); - cmdArgs = new CmdArgs(ans.trim()).toArray(); + const ans = await inquirer.input({ + message: 'Input command line options:' + }); + if (ans) { + cmdArgs = new CmdArgs(ans.trim()).toArray(); + } else { + cmdArgs = []; + } } else { cmdArgs = []; } @@ -64,7 +78,10 @@ export const handleEditorPathInput = async editorFilePath => { if (isFile(editorFilePath) && isExecutable(editorFilePath)) { editorPath = editorFilePath; } else { - const ans = readline.question('Input editor path: '); + const ans = await inquirer.input({ + message: 'Input editor path:', + required: true + }); const stat = getStat(ans); if (stat) { if (stat.isFile()) { @@ -109,39 +126,53 @@ export const createEditorConfig = async () => { return filePath; }; +/** + * confirm overwrite editorconfig file + * @param {string} file - file path + * @returns {Promise.} - handleEditorPathInput() / abortSetup() + */ +export const confirmOverwriteEditorConfig = async file => { + let func; + const ans = await inquirer.confirm({ + message: `${file} already exists. Overwrite?`, + default: false + }); + if (ans) { + func = createEditorConfig(); + } else { + func = abortSetup(`${file} already exists.`); + } + return func; +}; + /** * handle setup callback * @param {object} info - info - * @returns {Function} - handleEditorPathInput() / abortSetup() + * @returns {Promise.} - promise chain */ export const handleSetupCallback = (info = {}) => { const { configDirPath: configPath } = info; if (!isDir(configPath)) { throw new Error(`No such directory: ${configPath}.`); } - const editorArgs = setupOpts.get('editorArgs'); - const editorPath = setupOpts.get('editorPath'); - const overwriteEditorConfig = setupOpts.get('overwriteEditorConfig'); - const file = path.join(configPath, EDITOR_CONFIG_FILE); - let func; setupOpts.set('configPath', configPath); + const editorPath = setupOpts.get('editorPath'); if (isString(editorPath)) { setupOpts.set('editorFilePath', editorPath.trim()); } + const editorArgs = setupOpts.get('editorArgs'); if (isString(editorArgs)) { setupOpts.set('editorCmdArgs', new CmdArgs(editorArgs.trim()).toArray()); } + let func; + const file = path.join(configPath, EDITOR_CONFIG_FILE); + const overwriteEditorConfig = setupOpts.get('overwriteEditorConfig'); if (isFile(file) && !overwriteEditorConfig) { - const ans = readline.keyInYNStrict(`${file} already exists.\nOverwrite?`); - if (ans) { - func = createEditorConfig().catch(throwErr); - } else { - func = abortSetup(`${file} already exists.`); - } + func = confirmOverwriteEditorConfig(file).catch(throwErr); } else { func = createEditorConfig().catch(throwErr); } - return func || null; + return func; }; /** diff --git a/package.json b/package.json index 7eadc1d..7e5bd44 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ "type": "module", "main": "./index.js", "dependencies": { + "@inquirer/prompts": "^7.0.0", "commander": "^12.1.0", - "readline-sync": "^1.4.10", "semver-parser": "^4.1.6", "undici": "^6.20.1", - "web-ext-native-msg": "^8.0.2" + "web-ext-native-msg": "^8.0.3" }, "devDependencies": { "boxednode": "^2.4.4", diff --git a/test/setup.test.js b/test/setup.test.js index 90cfa6d..366b3a2 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -4,7 +4,6 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; -import readline from 'readline-sync'; import { assert } from 'chai'; import { afterEach, beforeEach, describe, it } from 'mocha'; import sinon from 'sinon'; @@ -14,8 +13,9 @@ import { /* test */ import { - abortSetup, createEditorConfig, handleCmdArgsInput, handleEditorPathInput, - handleSetupCallback, runSetup, setupOpts + abortSetup, confirmOverwriteEditorConfig, createEditorConfig, inquirer, + handleCmdArgsInput, handleEditorPathInput, handleSetupCallback, runSetup, + setupOpts } from '../modules/setup.js'; /* constants */ @@ -47,75 +47,73 @@ describe('abortSetup', () => { describe('handleCmdArgsInput', () => { it('should get array', async () => { - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(true); - const stubRlQues = - sinon.stub(readline, 'question').returns('foo "bar baz" qux'); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(true); + const stubInput = + sinon.stub(inquirer, 'input').resolves('foo "bar baz" qux'); const cmdArgs = ['foo', 'bar', 'baz']; const res = await handleCmdArgsInput(cmdArgs); - assert.isFalse(stubRlKey.called); - assert.isFalse(stubRlQues.called); + assert.isFalse(stubConfirm.called); + assert.isFalse(stubInput.called); assert.deepEqual(res, cmdArgs); - stubRlKey.restore(); - stubRlQues.restore(); + stubConfirm.restore(); + stubInput.restore(); }); it('should call function and get array', async () => { - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(true); - const stubRlQues = - sinon.stub(readline, 'question').returns('foo bar baz'); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(true); + const stubInput = sinon.stub(inquirer, 'input').resolves('foo bar baz'); const res = await handleCmdArgsInput(); - assert.isTrue(stubRlKey.calledOnce); - assert.isTrue(stubRlQues.calledOnce); + assert.isTrue(stubConfirm.calledOnce); + assert.isTrue(stubInput.calledOnce); assert.deepEqual(res, [ 'foo', 'bar', 'baz' ]); - stubRlKey.restore(); - stubRlQues.restore(); + stubConfirm.restore(); + stubInput.restore(); }); it('should call function and get array', async () => { - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(true); - const stubRlQues = - sinon.stub(readline, 'question').returns('foo "bar baz" qux'); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(true); + const stubInput = + sinon.stub(inquirer, 'input').resolves('foo "bar baz" qux'); const res = await handleCmdArgsInput(); - assert.isTrue(stubRlKey.calledOnce); - assert.isTrue(stubRlQues.calledOnce); + assert.isTrue(stubConfirm.calledOnce); + assert.isTrue(stubInput.calledOnce); assert.deepEqual(res, [ 'foo', 'bar baz', 'qux' ]); - stubRlKey.restore(); - stubRlQues.restore(); + stubConfirm.restore(); + stubInput.restore(); }); it('should call function and get array', async () => { - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(true); - const stubRlQues = - sinon.stub(readline, 'question').returns('foo bar="baz qux"'); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(true); + const stubInput = + sinon.stub(inquirer, 'input').resolves('foo bar="baz qux"'); const res = await handleCmdArgsInput(); - assert.isTrue(stubRlKey.calledOnce); - assert.isTrue(stubRlQues.calledOnce); + assert.isTrue(stubConfirm.calledOnce); + assert.isTrue(stubInput.calledOnce); assert.deepEqual(res, [ 'foo', 'bar=baz qux' ]); - stubRlKey.restore(); - stubRlQues.restore(); + stubConfirm.restore(); + stubInput.restore(); }); it('should call function and get array', async () => { - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(false); - const stubRlQues = - sinon.stub(readline, 'question').returns('foo bar baz'); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(false); + const stubInput = sinon.stub(inquirer, 'input').resolves('foo bar baz'); const res = await handleCmdArgsInput(); - assert.isTrue(stubRlKey.calledOnce); - assert.isFalse(stubRlQues.calledOnce); + assert.isTrue(stubConfirm.calledOnce); + assert.isFalse(stubInput.calledOnce); assert.deepEqual(res, []); - stubRlKey.restore(); - stubRlQues.restore(); + stubConfirm.restore(); + stubInput.restore(); }); }); @@ -126,11 +124,11 @@ describe('handleEditorPathInput', () => { if (!IS_WIN) { fs.chmodSync(editorPath, PERM_APP); } - const stubRlPath = sinon.stub(readline, 'question').returns(editorPath); + const stubInput = sinon.stub(inquirer, 'input').resolves(editorPath); const res = await handleEditorPathInput(editorPath); - assert.isFalse(stubRlPath.called); + assert.isFalse(stubInput.called); assert.strictEqual(res, editorPath); - stubRlPath.restore(); + stubInput.restore(); }); it('should call function and get string', async () => { @@ -139,11 +137,11 @@ describe('handleEditorPathInput', () => { if (!IS_WIN) { fs.chmodSync(editorPath, PERM_APP); } - const stubRlPath = sinon.stub(readline, 'question').returns(editorPath); + const stubInput = sinon.stub(inquirer, 'input').resolves(editorPath); const res = await handleEditorPathInput(); - assert.isTrue(stubRlPath.calledOnce); + assert.isTrue(stubInput.calledOnce); assert.strictEqual(res, editorPath); - stubRlPath.restore(); + stubInput.restore(); }); it('should call function and get string', async () => { @@ -157,18 +155,18 @@ describe('handleEditorPathInput', () => { fs.chmodSync(editorPath, PERM_APP); } const inputPath = path.resolve('test', 'file', 'test.txt'); - const stubRlPath = sinon.stub(readline, 'question'); - const i = stubRlPath.callCount; - stubRlPath.onFirstCall().returns(inputPath); - stubRlPath.onSecondCall().returns(editorPath); + const stubInput = sinon.stub(inquirer, 'input'); + const i = stubInput.callCount; + stubInput.onFirstCall().resolves(inputPath); + stubInput.onSecondCall().resolves(editorPath); const res = await handleEditorPathInput(); const { calledOnce: warnCalled } = stubWarn; stubWarn.restore(); assert.isTrue(warnCalled); assert.strictEqual(wrn, `${inputPath} is not executable.`); - assert.strictEqual(stubRlPath.callCount, i + 2); + assert.strictEqual(stubInput.callCount, i + 2); assert.strictEqual(res, editorPath); - stubRlPath.restore(); + stubInput.restore(); }); it('should call function and get string', async () => { @@ -182,18 +180,18 @@ describe('handleEditorPathInput', () => { fs.chmodSync(editorPath, PERM_APP); } const inputPath = path.resolve('test', 'file'); - const stubRlPath = sinon.stub(readline, 'question'); - const i = stubRlPath.callCount; - stubRlPath.onFirstCall().returns(inputPath); - stubRlPath.onSecondCall().returns(editorPath); + const stubInput = sinon.stub(inquirer, 'input'); + const i = stubInput.callCount; + stubInput.onFirstCall().resolves(inputPath); + stubInput.onSecondCall().resolves(editorPath); const res = await handleEditorPathInput(); const { calledOnce: warnCalled } = stubWarn; stubWarn.restore(); assert.isTrue(warnCalled); assert.strictEqual(wrn, `${inputPath} is not a file.`); - assert.strictEqual(stubRlPath.callCount, i + 2); + assert.strictEqual(stubInput.callCount, i + 2); assert.strictEqual(res, editorPath); - stubRlPath.restore(); + stubInput.restore(); }); it('should call function and get string', async () => { @@ -207,18 +205,18 @@ describe('handleEditorPathInput', () => { fs.chmodSync(editorPath, PERM_APP); } const inputPath = path.resolve('test', 'file', 'foo'); - const stubRlPath = sinon.stub(readline, 'question'); - const i = stubRlPath.callCount; - stubRlPath.onFirstCall().returns(inputPath); - stubRlPath.onSecondCall().returns(editorPath); + const stubInput = sinon.stub(inquirer, 'input'); + const i = stubInput.callCount; + stubInput.onFirstCall().resolves(inputPath); + stubInput.onSecondCall().resolves(editorPath); const res = await handleEditorPathInput(); const { calledOnce: warnCalled } = stubWarn; stubWarn.restore(); assert.isTrue(warnCalled); assert.strictEqual(wrn, `${inputPath} not found.`); - assert.strictEqual(stubRlPath.callCount, i + 2); + assert.strictEqual(stubInput.callCount, i + 2); assert.strictEqual(res, editorPath); - stubRlPath.restore(); + stubInput.restore(); }); }); @@ -278,6 +276,85 @@ describe('createEditorConfig', () => { }); }); +describe('confirmOverwriteEditorConfig', () => { + beforeEach(() => { + const configDirPath = path.join(DIR_TMP, 'withexeditorhost-test'); + removeDirSync(configDirPath, DIR_TMP); + setupOpts.clear(); + }); + afterEach(() => { + const configDirPath = path.join(DIR_TMP, 'withexeditorhost-test'); + removeDirSync(configDirPath, DIR_TMP); + setupOpts.clear(); + }); + + it('should abort', async () => { + let info; + const stubInfo = sinon.stub(console, 'info').callsFake(msg => { + info = msg; + }); + const stubExit = sinon.stub(process, 'exit'); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(false); + const res = await confirmOverwriteEditorConfig('/foo/bar'); + const { calledOnce: infoCalled } = stubInfo; + const { calledOnce: exitCalled } = stubExit; + stubInfo.restore(); + stubExit.restore(); + assert.isTrue(stubConfirm.calledOnce); + assert.isTrue(infoCalled); + assert.isTrue(exitCalled); + assert.strictEqual(info, 'Setup aborted: /foo/bar already exists.'); + assert.isUndefined(res); + stubConfirm.restore(); + }); + + it('should call function', async () => { + let info; + const stubInfo = sinon.stub(console, 'info').callsFake(msg => { + info = msg; + }); + const stubExit = sinon.stub(process, 'exit'); + const app = IS_WIN ? 'test.cmd' : 'test.sh'; + const editorPath = path.resolve('test', 'file', app); + if (!IS_WIN) { + fs.chmodSync(editorPath, PERM_APP); + } + const stubRl = sinon.stub(inquirer, 'input'); + const i = stubRl.callCount; + stubRl.onFirstCall().resolves(editorPath); + stubRl.onSecondCall().resolves(''); + const stubConfirm = sinon.stub(inquirer, 'confirm'); + const j = stubConfirm.callCount; + stubConfirm.onFirstCall().resolves(true); + stubConfirm.onSecondCall().resolves(true); + const configDirPath = await createDirectory( + path.join(DIR_TMP, 'withexeditorhost-test') + ); + const filePath = path.join(configDirPath, EDITOR_CONFIG_FILE); + const content = `${JSON.stringify({}, null, INDENT)}\n`; + await createFile(filePath, content, { + encoding: CHAR, + flag: 'w' + }); + setupOpts.set('configPath', configDirPath); + const res = await confirmOverwriteEditorConfig(filePath); + const { calledOnce: infoCalled } = stubInfo; + const { calledOnce: exitCalled } = stubExit; + stubInfo.restore(); + stubExit.restore(); + assert.strictEqual(setupOpts.get('configPath'), configDirPath); + assert.strictEqual(stubRl.callCount, i + 2); + assert.strictEqual(stubConfirm.callCount, j + 2); + assert.isTrue(infoCalled); + assert.isFalse(exitCalled); + assert.strictEqual(info, `Created: ${filePath}`); + assert.isTrue(isFile(filePath)); + assert.strictEqual(res, filePath); + stubRl.restore(); + stubConfirm.restore(); + }); +}); + describe('handleSetupCallback', () => { beforeEach(() => { const configDirPath = path.join(DIR_TMP, 'withexeditorhost-test'); @@ -312,11 +389,11 @@ describe('handleSetupCallback', () => { if (!IS_WIN) { fs.chmodSync(editorPath, PERM_APP); } - const stubRl = sinon.stub(readline, 'question'); + const stubRl = sinon.stub(inquirer, 'input'); const i = stubRl.callCount; - stubRl.onFirstCall().returns(editorPath); - stubRl.onSecondCall().returns(''); - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(true); + stubRl.onFirstCall().resolves(editorPath); + stubRl.onSecondCall().resolves(''); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(true); const configDirPath = await createDirectory( path.join(DIR_TMP, 'withexeditorhost-test') ); @@ -328,14 +405,14 @@ describe('handleSetupCallback', () => { stubExit.restore(); assert.strictEqual(setupOpts.get('configPath'), configDirPath); assert.strictEqual(stubRl.callCount, i + 2); - assert.isTrue(stubRlKey.calledOnce); + assert.isTrue(stubConfirm.calledOnce); assert.isTrue(infoCalled); assert.isFalse(exitCalled); assert.strictEqual(info, `Created: ${filePath}`); assert.isTrue(isFile(filePath)); assert.strictEqual(res, filePath); stubRl.restore(); - stubRlKey.restore(); + stubConfirm.restore(); }); it('should abort', async () => { @@ -349,11 +426,11 @@ describe('handleSetupCallback', () => { if (!IS_WIN) { fs.chmodSync(editorPath, PERM_APP); } - const stubRl = sinon.stub(readline, 'question'); + const stubRl = sinon.stub(inquirer, 'input'); const i = stubRl.callCount; - stubRl.onFirstCall().returns(editorPath); - stubRl.onSecondCall().returns(''); - const stubRlKey = sinon.stub(readline, 'keyInYNStrict').returns(false); + stubRl.onFirstCall().resolves(editorPath); + stubRl.onSecondCall().resolves(''); + const stubConfirm = sinon.stub(inquirer, 'confirm').resolves(false); const configDirPath = await createDirectory( path.join(DIR_TMP, 'withexeditorhost-test') ); @@ -370,14 +447,14 @@ describe('handleSetupCallback', () => { stubExit.restore(); assert.strictEqual(setupOpts.get('configPath'), configDirPath); assert.strictEqual(stubRl.callCount, i); - assert.isTrue(stubRlKey.calledOnce); + assert.isTrue(stubConfirm.calledOnce); assert.isTrue(infoCalled); assert.isTrue(exitCalled); assert.strictEqual(info, `Setup aborted: ${filePath} already exists.`); assert.isTrue(isFile(filePath)); - assert.isNull(res); + assert.isUndefined(res); stubRl.restore(); - stubRlKey.restore(); + stubConfirm.restore(); }); it('should call function', async () => { @@ -391,14 +468,14 @@ describe('handleSetupCallback', () => { if (!IS_WIN) { fs.chmodSync(editorPath, PERM_APP); } - const stubRl = sinon.stub(readline, 'question'); + const stubRl = sinon.stub(inquirer, 'input'); const i = stubRl.callCount; - stubRl.onFirstCall().returns(editorPath); - stubRl.onSecondCall().returns(''); - const stubRlKey = sinon.stub(readline, 'keyInYNStrict'); - const j = stubRlKey.callCount; - stubRlKey.onFirstCall().returns(true); - stubRlKey.onSecondCall().returns(true); + stubRl.onFirstCall().resolves(editorPath); + stubRl.onSecondCall().resolves(''); + const stubConfirm = sinon.stub(inquirer, 'confirm'); + const j = stubConfirm.callCount; + stubConfirm.onFirstCall().resolves(true); + stubConfirm.onSecondCall().resolves(true); const configDirPath = await createDirectory( path.join(DIR_TMP, 'withexeditorhost-test') ); @@ -415,14 +492,14 @@ describe('handleSetupCallback', () => { stubExit.restore(); assert.strictEqual(setupOpts.get('configPath'), configDirPath); assert.strictEqual(stubRl.callCount, i + 2); - assert.strictEqual(stubRlKey.callCount, j + 2); + assert.strictEqual(stubConfirm.callCount, j + 2); assert.isTrue(infoCalled); assert.isFalse(exitCalled); assert.strictEqual(info, `Created: ${filePath}`); assert.isTrue(isFile(filePath)); assert.strictEqual(res, filePath); stubRl.restore(); - stubRlKey.restore(); + stubConfirm.restore(); }); it('should call function', async () => { @@ -436,14 +513,14 @@ describe('handleSetupCallback', () => { if (!IS_WIN) { fs.chmodSync(editorPath, PERM_APP); } - const stubRl = sinon.stub(readline, 'question'); + const stubRl = sinon.stub(inquirer, 'input'); const i = stubRl.callCount; - stubRl.onFirstCall().returns(editorPath); - stubRl.onSecondCall().returns(''); - const stubRlKey = sinon.stub(readline, 'keyInYNStrict'); - const j = stubRlKey.callCount; - stubRlKey.onFirstCall().returns(true); - stubRlKey.onSecondCall().returns(true); + stubRl.onFirstCall().resolves(editorPath); + stubRl.onSecondCall().resolves(''); + const stubConfirm = sinon.stub(inquirer, 'confirm'); + const j = stubConfirm.callCount; + stubConfirm.onFirstCall().resolves(true); + stubConfirm.onSecondCall().resolves(true); const configDirPath = await createDirectory( path.join(DIR_TMP, 'withexeditorhost-test') ); @@ -465,14 +542,14 @@ describe('handleSetupCallback', () => { assert.strictEqual(setupOpts.get('editorFilePath'), editorPath); assert.deepEqual(setupOpts.get('editorCmdArgs'), ['foo', 'bar', 'baz']); assert.strictEqual(stubRl.callCount, i); - assert.strictEqual(stubRlKey.callCount, j); + assert.strictEqual(stubConfirm.callCount, j); assert.isTrue(infoCalled); assert.isFalse(exitCalled); assert.strictEqual(info, `Created: ${filePath}`); assert.isTrue(isFile(filePath)); assert.strictEqual(res, filePath); stubRl.restore(); - stubRlKey.restore(); + stubConfirm.restore(); }); });