From 5b909f8f4c48a943449e2505838d3628d14ee9bc Mon Sep 17 00:00:00 2001 From: Gyubong Date: Thu, 5 Aug 2021 14:03:56 -0700 Subject: [PATCH] Add TypeScript definitions (#138) --- index.d.ts | 522 ++++++++++++++++++++++++++++++++++++++++++++ index.js | 2 +- index.test-d.ts | 34 +++ package.json | 7 +- test/cache.js | 2 +- test/fetch.js | 2 +- test/test.js | 2 +- test/user-config.js | 2 +- 8 files changed, 566 insertions(+), 7 deletions(-) create mode 100644 index.d.ts create mode 100644 index.test-d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..752f415 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,522 @@ +import Conf from 'conf'; +import {Options} from 'got'; + +export interface FetchOptions extends Options { + /** + Equal to 'searchParams' + */ + readonly query?: string | Record | URLSearchParams | undefined; + + /** + Number of milliseconds this request should be cached. + */ + readonly maxAge?: number; + + /** + Transform the response before it gets cached. + */ + readonly transform?: (body: any) => unknown; +} + +export interface OutputOptions { + /** + A script can be set to re-run automatically after some interval. + The script will only be re-run if the script filter is still active and the user hasn't changed the state of the filter by typing and triggering a re-run. + For example, it could be used to update the progress of a particular task: + */ + readonly rerunInterval?: number; +} + +export interface CacheConfGetOptions { + /** + Get the item for the key provided without taking the maxAge of the item into account. + */ + readonly ignoreMaxAge?: boolean; +} + +export interface CacheConfSetOptions { + /** + Number of milliseconds the cached value is valid. + */ + readonly maxAge?: number; +} + +export interface CacheConf extends Conf { + isExpired: (key: T) => boolean; + + get(key: Key, options?: CacheConfGetOptions): T[Key]; + get(key: Key, defaultValue: Required[Key], options?: CacheConfGetOptions): Required[Key]; + get(key: Exclude, defaultValue?: Value, options?: CacheConfGetOptions): Value; + get(key: string, defaultValue?: unknown, options?: CacheConfGetOptions): unknown; + + set(key: Key, value?: T[Key], options?: CacheConfSetOptions): void; + set(key: string, value: unknown, options: CacheConfSetOptions): void; + set(object: Partial, options: CacheConfSetOptions): void; + set(key: Partial | Key | string, value?: T[Key] | unknown, options?: CacheConfSetOptions): void; +} + +/** +The icon displayed in the result row. Workflows are run from their workflow folder, so you can reference icons stored in your workflow relatively. +By omitting the 'type', Alfred will load the file path itself, for example a png. +By using 'type': 'fileicon', Alfred will get the icon for the specified path. +Finally, by using 'type': 'filetype', you can get the icon of a specific file, for example 'path': 'public.png' +*/ +export interface IconElement { + readonly path?: string; + readonly type?: 'fileicon' | 'filetype'; +} + +/** +The text element defines the text the user will get when copying the selected result row with `⌘C` or displaying large type with `⌘L`. +If these are not defined, you will inherit Alfred's standard behaviour where the arg is copied to the Clipboard or used for Large Type. +*/ +export interface TextElement { + /** + User will get when copying the selected result row with `⌘C` + */ + readonly copy?: string; + + /** + User will get displaying large type with `⌘L`. + */ + readonly largetype?: string; +} + +/** +Defines what to change when the modifier key is pressed. +When you release the modifier key, it returns to the original item. +*/ +export interface ModifierKeyItem { + readonly valid?: boolean; + readonly title?: string; + readonly subtitle?: string; + readonly arg?: string; + readonly icon?: string; + readonly variables?: Record; +} + +/** +This element defines the Universal Action items used when actioning the result, and overrides arg being used for actioning. +The action key can take a string or array for simple types, and the content type will automatically be derived by Alfred to file, url or text. +*/ +export interface ActionElement { + /** + Forward text to Alfred. + */ + readonly text?: string | string[]; + + /** + Forward url to Alfred. + */ + readonly url?: string | string[]; + + /** + Forward file path to Alfred. + */ + readonly file?: string | string[]; + + /** + Forward some value and let the value type be infered from Alfred. + */ + readonly auto?: string | string[]; +} + +type PossibleModifiers = 'fn' | 'ctrl' | 'opt' | 'cmd' | 'shift'; + +/** +Each item describes a result row displayed in Alfred. +*/ +export interface ScriptFilterItem { + /** + This is a unique identifier for the item which allows help Alfred to learn about this item for subsequent sorting and ordering of the user's actioned results. + It is important that you use the same UID throughout subsequent executions of your script to take advantage of Alfred's knowledge and sorting. + If you would like Alfred to always show the results in the order you return them from your script, exclude the UID field. + */ + readonly uid?: string; + + /** + The title displayed in the result row. There are no options for this element and it is essential that this element is populated. + + @example + ``` + 'title': 'Desktop' + ``` + */ + readonly title: string; + + /** + The subtitle displayed in the result row. This element is optional. + + @example + ``` + 'subtitle': '~/Desktop' + ``` + */ + readonly subtitle?: string; + + /** + The argument which is passed through the workflow to the connected output action. + While the arg attribute is optional, it's highly recommended that you populate this as it's the string which is passed to your connected output actions. + If excluded, you won't know which result item the user has selected. + + @example + ``` + 'arg': '~/Desktop' + ``` + */ + readonly arg?: string; + + /** + The icon displayed in the result row. Workflows are run from their workflow folder, so you can reference icons stored in your workflow relatively. + By omitting the 'type', Alfred will load the file path itself, for example a png. + By using 'type': 'fileicon', Alfred will get the icon for the specified path. Finally, by using 'type': 'filetype', you can get the icon of a specific file, for example 'path': 'public.png' + + @example + ``` + 'icon': { + 'type': 'fileicon', + 'path': '~/Desktop' + } + ``` + */ + readonly icon?: IconElement | string; + + /** + If this item is valid or not. If an item is valid then Alfred will action this item when the user presses return. + + @default true + */ + readonly valid?: boolean; + + /** + From Alfred 3.5, the match field enables you to define what Alfred matches against when the workflow is set to 'Alfred Filters Results'. + If match is present, it fully replaces matching on the title property. + */ + readonly match?: string; + + /** + An optional but recommended string you can provide which is populated into Alfred's search field if the user auto-complete's the selected result (`⇥` by default). + */ + readonly autocomplete?: string; + + /** + By specifying 'type': 'file', this makes Alfred treat your result as a file on your system. + This allows the user to perform actions on the file like they can with Alfred's standard file filters. + When returning files, Alfred will check if the file exists before presenting that result to the user. + This has a very small performance implication but makes the results as predictable as possible. + If you would like Alfred to skip this check as you are certain that the files you are returning exist, you can use 'type': 'file:skipcheck'. + + @default 'default' + */ + readonly type?: 'default' | 'file' | 'file:skipcheck'; + + /** + The mod element gives you control over how the modifier keys react. + You can now define the valid attribute to mark if the result is valid based on the modifier selection and set a different arg to be passed out if actioned with the modifier. + */ + readonly mods?: Record; + + /** + This element defines the Universal Action items used when actioning the result, and overrides arg being used for actioning. + The action key can take a string or array for simple types, and the content type will automatically be derived by Alfred to file, url or text. + + @example + ``` + // For Single Item, + 'action': 'Alfred is Great' + + // For Multiple Items, + 'action': ['Alfred is Great', 'I use him all day long'] + + // For control over the content type of the action, you can use an object with typed keys as follows: + 'action': { + 'text': ['one', 'two', 'three'], + 'url': 'https://www.alfredapp.com', + 'file': '~/Desktop', + 'auto': '~/Pictures' + } + ``` + */ + // To do (jopemachine): Activate attribute below after 'action' is implemented in Alfred. + // readonly action?: string | string[] | ActionElement; + + /** + The text element defines the text the user will get when copying the selected result row with `⌘C` or displaying large type with `⌘L`. + + @example + ``` + 'text': { + 'copy': 'https://www.alfredapp.com/ (text here to copy)', + 'largetype': 'https://www.alfredapp.com/ (text here for large type)' + } + ``` + */ + readonly text?: TextElement; + + /** + A Quick Look URL which will be visible if the user uses the Quick Look feature within Alfred (tapping shift, or `⌘Y`). + Note that quicklookurl will also accept a file path, both absolute and relative to home using `~/`. + + @example + ``` + 'quicklookurl': 'https://www.alfredapp.com/' + ``` + */ + readonly quicklookurl?: string; + + /** + Variables can be passed out of the script filter within a variables object. + */ + readonly variables?: Record; +} + +/** +Create Alfred workflows with ease + +@example +``` +const alfy = require('alfy'); + +const data = await alfy.fetch('https://jsonplaceholder.typicode.com/posts'); + +const items = alfy + .inputMatches(data, 'title') + .map(element => ({ + title: element.title, + subtitle: element.body, + arg: element.id + })); + +alfy.output(items); +``` +*/ +export interface Alfy { + /** + Return output to Alfred. + + @param items + @param options + + @example + ``` + alfy.output([ + { + title: 'Unicorn' + }, + { + title: 'Rainbow' + } + ]); + ``` + */ + output: (items: ScriptFilterItem[], options?: OutputOptions) => void; + + /** + Returns items in list that case-insensitively contains input. + + @param input + @param list + @param target + @returns items in list that case-insensitively contains input. + + @example + ``` + alfy.matches('Corn', ['foo', 'unicorn']); + //=> ['unicorn'] + ``` + */ + matches: (input: string, list: T, target?: string | ((item: string | ScriptFilterItem, input: string) => boolean)) => T; + + /** + Same as matches(), but with `alfy.input` as input. + + @param list + @param target + @returns items in list that case-insensitively contains `alfy.input`. + + @example + ``` + // Assume user types 'Corn'. + alfy.inputMatches(['foo', 'unicorn']); + //=> ['unicorn'] + */ + inputMatches: (list: T, target?: string | ((item: string | ScriptFilterItem, input: string) => boolean)) => T; + + /** + Log value to the Alfred workflow debugger. + + @param text + */ + log: (text: string) => void; + + /** + Display an error or error message in Alfred. + + **Note:** You don't need to `.catch()` top-level promises. Alfy handles that for you. + + @param error + */ + error: (error: Error | string) => void; + + /** + Returns a `Promise` that returns the body of the response. + + @param url + @param options + @returns Body of the response. + + @example + ``` + await alfy.fetch('https://api.foo.com', { + transform: body => { + body.foo = 'bar'; + return body; + } + }) + ``` + */ + fetch: (url: string, options?: FetchOptions) => Promise; + + /** + @example + ``` + { + name: 'Emoj', + version: '0.2.5', + uid: 'user.workflow.B0AC54EC-601C-479A-9428-01F9FD732959', + bundleId: 'com.sindresorhus.emoj' + } + ``` + */ + meta: { + name: string; + version: string; + bundleId: string; + type: string; + }; + + /** + Input from Alfred. What the user wrote in the input box. + */ + input: string; + + /** + Persist config data. + + Exports a [`conf` instance](https://github.com/sindresorhus/conf#instance) with the correct config path set. + + @example + ``` + alfy.config.set('unicorn', '🦄'); + + alfy.config.get('unicorn'); + //=> '🦄' + ``` + */ + config: Conf; + + /** + Persist cache data. + + Exports a modified [`conf` instance](https://github.com/sindresorhus/conf#instance) with the correct cache path set. + + @example + ``` + alfy.cache.set('unicorn', '🦄'); + + alfy.cache.get('unicorn'); + //=> '🦄' + ``` + */ + cache: CacheConf; + + /** + Get various default system icons. + + The most useful ones are included as keys. The rest you can get with `icon.get()`. Go to `/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources` in Finder to see them all. + + @example + ``` + console.log(alfy.icon.error); + //=> '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns' + + console.log(alfy.icon.get('Clock')); + //=> '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Clock.icns' + ``` + */ + icon: { + /** + Get various default system icons. + You can get icons in '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources' + */ + get: (icon: string) => string; + + /** + Get 'info' icon which is '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ToolbarInfo' + */ + info: string; + + /** + Get 'warning' icon which is '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertCautionIcon' + */ + warning: string; + + /** + Get 'error' icon which is '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon' + */ + error: string; + + /** + Get 'alert' icon which is '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Actions' + */ + alert: string; + + /** + Get 'like' icon which is '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ToolbarFavoritesIcon' + */ + like: string; + + /** + Get 'delete' icon which is '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ToolbarDeleteIcon' + */ + delete: string; + }; + + /** + Alfred metadata. + */ + alfred: { + version: string; + theme: string; + themeBackground: string; + themeSelectionBackground: string; + themeSubtext: string; + data: string; + cache: string; + preferences: string; + preferencesLocalHash: string; + }; + + /** + Whether the user currently has the workflow debugger open. + */ + debug: boolean; + + /** + Exports a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with the user workflow configuration. A workflow configuration allows your users to provide + configuration information for the workflow. For instance, if you are developing a GitHub workflow, you could let your users provide their own API tokens. + + See [`alfred-config`](https://github.com/SamVerschueren/alfred-config#workflow-configuration) for more details. + + @example + ``` + alfy.userConfig.get('apiKey'); + //=> '16811cad1b8547478b3e53eae2e0f083' + ``` + */ + userConfig: Map; +} + +export const alfy: Alfy; + +export default alfy; diff --git a/index.js b/index.js index eb5c794..1b2b1cd 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,7 @@ const cleanStack = require('clean-stack'); const dotProp = require('dot-prop'); const CacheConf = require('cache-conf'); const AlfredConfig = require('alfred-config'); -const updateNotification = require('./lib/update-notification'); +const updateNotification = require('./lib/update-notification.js'); const alfy = module.exports; diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..5509406 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-confusing-void-expression */ + +import {expectType} from 'tsd'; +import alfy, {ScriptFilterItem} from './index.js'; + +const mockItems: ScriptFilterItem[] = [ + { + title: 'Unicorn' + }, + { + title: 'Rainbow' + } +]; + +expectType(alfy.output(mockItems)); + +expectType(alfy.matches('Corn', ['foo', 'unicorn'])); + +expectType(alfy.matches('Unicorn', mockItems, 'title')); + +expectType(alfy.inputMatches(['foo', 'unicorn'])); + +expectType(alfy.inputMatches(mockItems, 'title')); + +expectType(alfy.error(new Error('some error'))); + +expectType(alfy.log('some message')); + +expectType>(alfy.fetch('https://foo.bar', { + transform: body => { + body.foo = 'bar'; + return body; + } +})); diff --git a/package.json b/package.json index 50d7c88..c882584 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,11 @@ "node": ">=10" }, "scripts": { - "test": "xo && ava" + "test": "xo && ava && tsd" }, "files": [ "index.js", + "index.d.ts", "init.js", "cleanup.js", "run-node.sh", @@ -53,6 +54,8 @@ "delay": "^4.3.0", "nock": "^13.1.0", "tempfile": "^3.0.0", - "xo": "^0.24.0" + "tsd": "^0.17.0", + "typescript": "^4.3.5", + "xo": "^0.39.1" } } diff --git a/test/cache.js b/test/cache.js index 56a6c8d..c0343fd 100644 --- a/test/cache.js +++ b/test/cache.js @@ -1,7 +1,7 @@ import test from 'ava'; import delay from 'delay'; import tempfile from 'tempfile'; -import {alfy as createAlfy} from './_utils'; +import {alfy as createAlfy} from './_utils.js'; test('no cache', t => { const alfy = createAlfy(); diff --git a/test/fetch.js b/test/fetch.js index e13f980..5d5c3d5 100644 --- a/test/fetch.js +++ b/test/fetch.js @@ -2,7 +2,7 @@ import test from 'ava'; import nock from 'nock'; import delay from 'delay'; import tempfile from 'tempfile'; -import {alfy as createAlfy} from './_utils'; +import {alfy as createAlfy} from './_utils.js'; const URL = 'https://foo.bar'; diff --git a/test/test.js b/test/test.js index ea6ec92..a22b868 100644 --- a/test/test.js +++ b/test/test.js @@ -1,6 +1,6 @@ import test from 'ava'; import hookStd from 'hook-std'; -import {alfy} from './_utils'; +import {alfy} from './_utils.js'; const alfyInstance = alfy(); diff --git a/test/user-config.js b/test/user-config.js index 62ea284..69a7bfe 100644 --- a/test/user-config.js +++ b/test/user-config.js @@ -1,6 +1,6 @@ import path from 'path'; import test from 'ava'; -import {alfy as createAlfy} from './_utils'; +import {alfy as createAlfy} from './_utils.js'; test('read user config', t => { const alfy = createAlfy({