diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b6824b..73d4f07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,3 +32,23 @@ jobs: run: pnpm install - name: run tests run: SHELL=/bin/bash pnpm test + + typecheck: + name: Type Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Commit + uses: actions/checkout@v1 + - name: Setup Node + uses: actions/setup-node@v1 + with: + node-version: '20' + - name: install pnpm + run: | + npm install pnpm -g + pnpm --version + - name: pnpm install + run: pnpm install + - name: type check + run: pnpm run typecheck diff --git a/.gitignore b/.gitignore index 71876ed..c8394bb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ quick-test.js coverage/ tabtab/ test/tabtab.log +/types diff --git a/lib/filename.js b/lib/filename.js index 91d18cc..7bfeaca 100644 --- a/lib/filename.js +++ b/lib/filename.js @@ -5,7 +5,7 @@ const { COMPLETION_FILE_EXT } = require('./constants'); * @param {String} shell * @returns {String} */ -const templateFileName = (shell = systemShell()) => { +const templateFileName = shell => { const ext = COMPLETION_FILE_EXT[shell]; if (!ext) { throw new Error(`Unsupported shell: ${shell}`); @@ -19,7 +19,7 @@ const templateFileName = (shell = systemShell()) => { * @param {String} shell * @returns {String} */ -const completionFileName = (name, shell = systemShell()) => { +const completionFileName = (name, shell) => { const ext = COMPLETION_FILE_EXT[shell]; if (!ext) { throw new Error(`Unsupported shell: ${shell}`); @@ -32,7 +32,7 @@ const completionFileName = (name, shell = systemShell()) => { * @param {String} shell * @returns {String} */ -const tabtabFileName = (shell = systemShell()) => { +const tabtabFileName = shell => { const ext = COMPLETION_FILE_EXT[shell]; if (!ext) { throw new Error(`Unsupported shell: ${shell}`); diff --git a/lib/index.js b/lib/index.js index d351ce3..03b8f84 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,7 +13,7 @@ const debug = tabtabDebug('tabtab'); * @param {String} options.name - The package configured for completion * @param {String} options.completer - The program the will act as the completer for the `name` program * @param {String} options.shell - * @returns {String} + * @returns {Promise.} */ const getCompletionScript = async ({ name, completer, shell }) => { if (!name) throw new TypeError('options.name is required'); @@ -29,10 +29,12 @@ const getCompletionScript = async ({ name, completer, shell }) => { * - SHELL (bash, zsh or fish) * - Path to shell script (with sensible defaults) * - * @param {Object} Options to use with namely `name` and `completer` - * + * @param {Object} options to use with namely `name` and `completer` + * @param {String} options.name + * @param {String} options.completer + * @param {String} options.shell */ -const install = async (options = { name: '', completer: '' }) => { +const install = async (options) => { const { name, completer } = options; if (!name) throw new TypeError('options.name is required'); if (!completer) throw new TypeError('options.completer is required'); @@ -142,21 +144,23 @@ const parseEnv = env => { }; }; +/** + * @typedef {Object} CompletionItem + * @property {String} name + * @property {String} [description] + */ + /** * Helper to normalize String and Objects with { name, description } when logging out. * - * @param {String|Object} item - Item to normalize + * @param {String|CompletionItem} item - Item to normalize + * @param {String} shell + * @returns {CompletionItem} normalized items */ -const completionItem = item => { +const completionItem = (item, shell) => { debug('completion item', item); - if (item.name || item.description) { - return { - name: item.name, - description: item.description || '' - }; - } - const shell = systemShell(); + if (typeof item === 'object') return item let name = item; let description = ''; @@ -175,12 +179,6 @@ const completionItem = item => { }; }; -/** - * @typedef {Object} CompletionItem - * @property {String} name - * @property {String} description - */ - /** * Main logging utility to pass completion items. * @@ -190,7 +188,7 @@ const completionItem = item => { * Bash needs in addition to filter out the args for the completion to work * (zsh, fish don't need this). * - * @param {Array.} args to log, Strings or Objects with name and + * @param {Array.} args to log, Strings or Objects with name and * description property. * @param {String} shell */ @@ -200,12 +198,12 @@ const log = (args, shell = systemShell()) => { } // Normalize arguments if there are some Objects { name, description } in them. - args = args.map(completionItem).map(item => { + let lines = args.map(item => completionItem(item, shell)).map(item => { const { name: rawName, description: rawDescription } = item; - const name = shell === 'zsh' ? rawName.replace(/:/g, '\\:') : rawName; + const name = shell === 'zsh' ? rawName?.replace(/:/g, '\\:') : rawName; const description = - shell === 'zsh' ? rawDescription.replace(/:/g, '\\:') : rawDescription; + shell === 'zsh' ? rawDescription?.replace(/:/g, '\\:') : rawDescription; let str = name; if (shell === 'zsh' && description) { @@ -219,11 +217,11 @@ const log = (args, shell = systemShell()) => { if (shell === 'bash') { const env = parseEnv(process.env); - args = args.filter(arg => arg.indexOf(env.last) === 0); + lines = lines.filter(arg => arg.indexOf(env.last) === 0); } - for (const arg of args) { - console.log(`${arg}`); + for (const line of lines) { + console.log(`${line}`); } }; diff --git a/lib/installer.js b/lib/installer.js index bc3cc38..8131109 100644 --- a/lib/installer.js +++ b/lib/installer.js @@ -109,11 +109,12 @@ const checkFilenameForLine = async (filename, line) => { filecontent = await readFile(untildify(filename), 'utf8'); } catch (err) { if (err.code !== 'ENOENT') { - return console.error( + console.error( 'Got an error while trying to read from %s file', filename, err ); + return false; } } diff --git a/lib/prompt.js b/lib/prompt.js index 1a729ff..06a9063 100644 --- a/lib/prompt.js +++ b/lib/prompt.js @@ -22,6 +22,7 @@ const prompt = async () => { const finalAnswers = {}; + // @ts-ignore const { shell } = await enquirer.prompt(questions); debug('answers', shell); @@ -30,6 +31,7 @@ const prompt = async () => { Object.assign(finalAnswers, { location, shell }); + // @ts-ignore const { locationOK } = await enquirer.prompt({ type: 'confirm', name: 'locationOK', @@ -42,6 +44,7 @@ const prompt = async () => { } // otherwise, ask for specific **absolute** path + // @ts-ignore const { userLocation } = await enquirer.prompt({ name: 'userLocation', message: 'Which path then ? Must be absolute.', diff --git a/lib/tsconfig.json b/lib/tsconfig.json new file mode 100644 index 0000000..a361ac7 --- /dev/null +++ b/lib/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.common.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "../types", + } +} diff --git a/package.json b/package.json index af17ed8..ebcc749 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,15 @@ "name": "@pnpm/tabtab", "description": "tab completion helpers, for node cli programs. Inspired by npm completion.", "main": "lib/index.js", + "types": "types/index.d.ts", "engines": { "node": ">=18" }, "scripts": { "test": "mkdir -p ~/.config/tabtab && DEBUG='tabtab*' c8 mocha --timeout 5000", + "typecheck": "pnpm run build && tsc -p test --noEmit", + "build": "tsc -p lib", + "prepublishOnly": "pnpm run build", "posttest": "npm run eslint", "mocha": "DEBUG='tabtab*' mocha --timeout 5000", "coverage": "c8 report --reporter=text-lcov | coveralls", @@ -34,7 +38,10 @@ "npm-watch": "^0.4.0", "remark-cli": "^5.0.0", "remark-toc": "^5.1.1", - "serve": "^10.1.2" + "serve": "^10.1.2", + "typescript": "^5.3.3", + "@types/mocha": "^7.0.0", + "@types/node": "^20.11.13" }, "license": "MIT", "keywords": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5451919..ecce81e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,12 @@ dependencies: version: 4.0.0(patch_hash=wtr47rwd2khewhm6li7h5z3egq) devDependencies: + '@types/mocha': + specifier: ^7.0.0 + version: 7.0.2 + '@types/node': + specifier: ^20.11.13 + version: 20.11.13 auto-changelog: specifier: ^1.16.4 version: 1.16.4 @@ -60,6 +66,9 @@ devDependencies: serve: specifier: ^10.1.2 version: 10.1.2 + typescript: + specifier: ^5.3.3 + version: 5.3.3 packages: @@ -107,11 +116,15 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.11.10 + '@types/node': 20.11.13 dev: true - /@types/node@20.11.10: - resolution: {integrity: sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==} + /@types/mocha@7.0.2: + resolution: {integrity: sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==} + dev: true + + /@types/node@20.11.13: + resolution: {integrity: sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==} dependencies: undici-types: 5.26.5 dev: true @@ -119,7 +132,7 @@ packages: /@types/responselike@1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 20.11.10 + '@types/node': 20.11.13 dev: true /@types/unist@2.0.10: @@ -5007,6 +5020,12 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: true + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /typical@2.6.1: resolution: {integrity: sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==} dev: true diff --git a/test/fixtures/tabtab-install.js b/test/fixtures/tabtab-install.js index 3cf4f33..c898336 100644 --- a/test/fixtures/tabtab-install.js +++ b/test/fixtures/tabtab-install.js @@ -3,6 +3,7 @@ const tabtab = require('../..'); (async () => { await tabtab.install({ name: 'foo', - completer: 'foo-complete' + completer: 'foo-complete', + shell: 'bash', }); })(); diff --git a/test/installer.js b/test/installer.js index 333d0d9..1766e71 100644 --- a/test/installer.js +++ b/test/installer.js @@ -9,7 +9,8 @@ const { writeToShellConfig, writeToCompletionScript } = require('../lib/installer'); -const { COMPLETION_DIR, TABTAB_SCRIPT_NAME } = require('../lib/constants'); +const { COMPLETION_DIR } = require('../lib/constants'); +const { tabtabFileName } = require('../lib/filename'); const { rejects, setupSuiteForInstall } = require('./utils'); // For node 7 / 8 @@ -63,7 +64,7 @@ describe('installer', () => { const bashDir = untildify(path.join(COMPLETION_DIR, 'bash')); await mkdir(bashDir, { recursive: true }); // Make sure __tabtab.bash starts with empty content, it'll be restored by setupSuiteForInstall - await writeFile(path.join(bashDir, `${TABTAB_SCRIPT_NAME}.bash`), ''); + await writeFile(path.join(bashDir, tabtabFileName('bash')), ''); }); it('installs the necessary line into ~/.bashrc', () => diff --git a/test/log.js b/test/log.js index 4fbeec0..532698a 100644 --- a/test/log.js +++ b/test/log.js @@ -4,6 +4,7 @@ const tabtab = require('..'); describe('tabtab.log', () => { it('tabtab.log throws an Error in case args is not an Array', () => { assert.throws(() => { + // @ts-ignore tabtab.log('foo', 'bar'); }, /^Error: log: Invalid arguments, must be an array$/); }); diff --git a/test/tabtab-install.js b/test/tabtab-install.js index 5c93935..2a9aa41 100644 --- a/test/tabtab-install.js +++ b/test/tabtab-install.js @@ -6,7 +6,8 @@ const path = require('path'); const fs = require('fs'); const { promisify } = require('util'); const tabtab = require('..'); -const { COMPLETION_DIR, TABTAB_SCRIPT_NAME } = require('../lib/constants'); +const { COMPLETION_DIR } = require('../lib/constants'); +const { tabtabFileName } = require('../lib/filename'); const { rejects, setupSuiteForInstall } = require('./utils'); const readFile = promisify(fs.readFile); @@ -26,11 +27,13 @@ describe('tabtab.install()', () => { }); it('rejects on missing options', async () => { + // @ts-ignore await assert.rejects(async () => tabtab.install(), TypeError); }); it('rejects on missing name options', async () => { await assert.rejects( + // @ts-ignore async () => tabtab.install({}), /options\.name is required/ ); @@ -38,6 +41,7 @@ describe('tabtab.install()', () => { it('rejects on missing completer options', async () => { await assert.rejects( + // @ts-ignore async () => tabtab.install({ name: 'foo' }), /options\.completer is required/ ); @@ -55,7 +59,7 @@ describe('tabtab.install()', () => { const bashDir = untildify(path.join(COMPLETION_DIR, 'bash')); await mkdir(bashDir, { recursive: true }); // Make sure __tabtab.bash starts with empty content, it'll be restored by setupSuiteForInstall - await writeFile(path.join(bashDir, `${TABTAB_SCRIPT_NAME}.bash`), ''); + await writeFile(path.join(bashDir, tabtabFileName('bash')), ''); await tabtab.install({ name: 'foo', completer: 'foo', shell: 'bash' }); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..22b7188 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.common.json", + "compilerOptions": { + "noEmit": true, + } +} diff --git a/test/utils/index.js b/test/utils/index.js index fe9149c..f9f5603 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -2,7 +2,8 @@ const fs = require('fs'); const path = require('path'); const untildify = require('untildify'); const { promisify } = require('util'); -const { COMPLETION_DIR, TABTAB_SCRIPT_NAME } = require('../../lib/constants'); +const { COMPLETION_DIR } = require('../../lib/constants'); +const { tabtabFileName } = require('../../lib/filename'); const { exists } = require('../../lib/utils'); @@ -12,7 +13,7 @@ const readFile = promisify(fs.readFile); /** * Returns both { exists, content } * - * @param {filename} filename - The file to check and read + * @param {String} filename - The file to check and read */ const readIfExists = async filename => { /* eslint-disable no-return-await */ @@ -29,7 +30,7 @@ const readIfExists = async filename => { const afterWrites = (prevBashrc, prevScript) => async () => { const bashrc = untildify('~/.bashrc'); const tabtabScript = untildify( - path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.bash`) + path.join(COMPLETION_DIR, tabtabFileName('bash')) ); await writeFile(bashrc, prevBashrc); @@ -46,7 +47,7 @@ const afterWrites = (prevBashrc, prevScript) => async () => { const setupSuiteForInstall = async (shouldUseAfter = false) => { const files = {}; const hook = shouldUseAfter ? after : afterEach; - const tabtabScript = path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.bash`); + const tabtabScript = path.join(COMPLETION_DIR, tabtabFileName('bash')); before(async () => { const { exists: bashrcExists, content: bashrcContent } = await readIfExists( diff --git a/tsconfig.common.json b/tsconfig.common.json new file mode 100644 index 0000000..0505ee4 --- /dev/null +++ b/tsconfig.common.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "module": "Node16", + "strictNullChecks": true, + "target": "ES2022", + } +}