Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: typechecks and type declarations #16

Merged
merged 10 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ quick-test.js
coverage/
tabtab/
test/tabtab.log
/types
6 changes: 3 additions & 3 deletions lib/filename.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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}`);
Expand All @@ -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}`);
Expand Down
50 changes: 24 additions & 26 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<String>}
*/
const getCompletionScript = async ({ name, completer, shell }) => {
if (!name) throw new TypeError('options.name is required');
Expand All @@ -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');
Expand Down Expand Up @@ -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 = '';
Expand All @@ -175,12 +179,6 @@ const completionItem = item => {
};
};

/**
* @typedef {Object} CompletionItem
* @property {String} name
* @property {String} description
*/

/**
* Main logging utility to pass completion items.
*
Expand All @@ -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.<CompletionItem>} args to log, Strings or Objects with name and
* @param {Array.<CompletionItem | String>} args to log, Strings or Objects with name and
* description property.
* @param {String} shell
*/
Expand All @@ -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) {
Expand All @@ -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}`);
}
};

Expand Down
3 changes: 2 additions & 1 deletion lib/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
3 changes: 3 additions & 0 deletions lib/prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const prompt = async () => {

const finalAnswers = {};

// @ts-ignore
const { shell } = await enquirer.prompt(questions);
debug('answers', shell);

Expand All @@ -30,6 +31,7 @@ const prompt = async () => {

Object.assign(finalAnswers, { location, shell });

// @ts-ignore
const { locationOK } = await enquirer.prompt({
type: 'confirm',
name: 'locationOK',
Expand All @@ -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.',
Expand Down
8 changes: 8 additions & 0 deletions lib/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "../types",
}
}
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": [
Expand Down
27 changes: 23 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion test/fixtures/tabtab-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const tabtab = require('../..');
(async () => {
await tabtab.install({
name: 'foo',
completer: 'foo-complete'
completer: 'foo-complete',
shell: 'bash',
});
})();
5 changes: 3 additions & 2 deletions test/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () =>
Expand Down
1 change: 1 addition & 0 deletions test/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -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$/);
});
Expand Down
8 changes: 6 additions & 2 deletions test/tabtab-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -26,18 +27,21 @@ 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/
);
});

it('rejects on missing completer options', async () => {
await assert.rejects(
// @ts-ignore
async () => tabtab.install({ name: 'foo' }),
/options\.completer is required/
);
Expand All @@ -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' });

Expand Down
6 changes: 6 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"noEmit": true,
}
}
Loading
Loading