Comparing version 0.1.0 to 0.2.0
264
index.js
'use strict'; | ||
var punycode = require('punycode'); | ||
var inherits = require('util').inherits; | ||
var Emitter = require('events').EventEmitter; | ||
var chalk = require('chalk'); | ||
var keypress = require('keypress'); | ||
var stdin = process.stdin; | ||
var stdout = process.stdout; | ||
function countSymbols(str) { | ||
return punycode.ucs2.decode(str).length; | ||
} | ||
const isObject = value => value !== null && typeof value === 'object'; | ||
function showCursor(x, y) { | ||
stdout.write('\x1b[?25h'); | ||
}; | ||
const deepMerge = (...sources) => { | ||
let returnValue = {}; | ||
function hideCursor(x, y) { | ||
stdout.write('\x1b[?25l'); | ||
}; | ||
for (const source of sources) { | ||
if (Array.isArray(source)) { | ||
if (!(Array.isArray(returnValue))) { | ||
returnValue = []; | ||
} | ||
function move(x, y) { | ||
stdout.write('\x1b[' + y + ';' + x + 'H'); | ||
returnValue = [...returnValue, ...source]; | ||
} else if (isObject(source)) { | ||
for (let [key, value] of Object.entries(source)) { | ||
if (isObject(value) && Reflect.has(returnValue, key)) { | ||
value = deepMerge(returnValue[key], value); | ||
} | ||
returnValue = {...returnValue, [key]: value}; | ||
} | ||
} | ||
} | ||
return returnValue; | ||
}; | ||
function reset() { | ||
clear(); | ||
move(0, 0); | ||
showCursor(); | ||
} | ||
const requestMethods = [ | ||
'get', | ||
'post', | ||
'put', | ||
'patch', | ||
'head', | ||
'delete' | ||
]; | ||
function clearLine() { | ||
stdout.write('\x1b[2K'); | ||
} | ||
const responseTypes = [ | ||
'json', | ||
'text', | ||
'formData', | ||
'arrayBuffer', | ||
'blob' | ||
]; | ||
function clearDown() { | ||
stdout.write('\x1b[J'); | ||
} | ||
const retryMethods = new Set([ | ||
'get', | ||
'put', | ||
'head', | ||
'delete', | ||
'options', | ||
'trace' | ||
]); | ||
function clear() { | ||
stdout.write('\x1b[0m\x1b[1J'); | ||
clearDown(); | ||
} | ||
const retryStatusCodes = new Set([ | ||
408, | ||
413, | ||
429, | ||
500, | ||
502, | ||
503, | ||
504 | ||
]); | ||
function pluck(arr, prop) { | ||
return arr.map(function (el) { | ||
return el[prop]; | ||
}); | ||
class HTTPError extends Error { | ||
constructor(response) { | ||
super(response.statusText); | ||
this.name = 'HTTPError'; | ||
this.response = response; | ||
} | ||
} | ||
function List(items, opts) { | ||
if (items.length === 0) { | ||
throw new Error('At least one list item required'); | ||
class TimeoutError extends Error { | ||
constructor() { | ||
super('Request timed out'); | ||
this.name = 'TimeoutError'; | ||
} | ||
opts = opts || {}; | ||
this.items = items || []; | ||
this.pointer = opts.pointer || chalk.yellow('❯ '); | ||
this.y = typeof opts.y === 'number' ? opts.x : 3; | ||
this.x = typeof opts.x === 'number' ? opts.x : 5; | ||
this.header = opts.header; | ||
this.footer = opts.footer; | ||
this.current = items[0].id; | ||
} | ||
inherits(List, Emitter); | ||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); | ||
List.prototype.render = function () { | ||
var pointerLength = countSymbols(chalk.stripColor(this.pointer)); | ||
var padding = Array(pointerLength + 1).join(' '); | ||
var y = this.y + 1; | ||
const timeout = (promise, ms) => new Promise((resolve, reject) => { | ||
promise.then(resolve, reject); // eslint-disable-line promise/prefer-await-to-then | ||
clear(); | ||
(async () => { | ||
await delay(ms); | ||
reject(new TimeoutError()); | ||
})(); | ||
}); | ||
if (this.header) { | ||
this.header.split('\n').forEach(function (el) { | ||
move(this.x + pointerLength, y++); | ||
stdout.write(el); | ||
}, this); | ||
move(this.x, y++); | ||
} | ||
class Ky { | ||
constructor(input, options) { | ||
this._input = input; | ||
this._retryCount = 0; | ||
this.items.forEach(function (el) { | ||
var prefix = this.current === el.id ? this.pointer : padding; | ||
var title = el.disabled ? chalk.gray(el.title) : el.title; | ||
move(this.x, y++); | ||
stdout.write(prefix + title); | ||
}, this); | ||
this._options = { | ||
method: 'get', | ||
credentials: 'same-origin', // TODO: This can be removed when the spec change is implemented in all browsers. Context: https://www.chromestatus.com/feature/4539473312350208 | ||
retry: 3, | ||
timeout: 10000, | ||
...options | ||
}; | ||
if (this.footer) { | ||
move(this.x, ++y); | ||
this.footer.split('\n').forEach(function (el) { | ||
move(this.x + pointerLength, y++); | ||
stdout.write(el); | ||
}, this); | ||
} | ||
}; | ||
this._timeout = this._options.timeout; | ||
delete this._options.timeout; | ||
List.prototype.init = function () { | ||
hideCursor(); | ||
this.render(); | ||
keypress(stdin); | ||
stdin.setRawMode(true); | ||
stdin.resume(); | ||
stdin.on('keypress', this.onKeypress.bind(this)); | ||
}; | ||
const headers = new window.Headers(this._options.headers || {}); | ||
List.prototype.destroy = function () { | ||
reset(); | ||
showCursor(); | ||
stdin.removeListener('keypress', this.onKeypress.bind(this)); | ||
stdin.pause(); | ||
}; | ||
if (this._options.json) { | ||
headers.set('content-type', 'application/json'); | ||
this._options.body = JSON.stringify(this._options.json); | ||
delete this._options.json; | ||
} | ||
List.prototype.change = function (id) { | ||
this.emit('change2', id); | ||
this.current = id; | ||
this.render(); | ||
}; | ||
this._options.headers = headers; | ||
List.prototype.prev = function () { | ||
var i = pluck(this.items, 'id').indexOf(this.current) - 1; | ||
var item = this.items[i < 0 ? this.items.length - 1 : i]; | ||
this._response = this._fetch(); | ||
this.change(item.id); | ||
for (const type of responseTypes) { | ||
this._response[type] = this._retry(async () => { | ||
if (this._retryCount > 0) { | ||
this._response = this._fetch(); | ||
} | ||
if (item.disabled) { | ||
this.prev(); | ||
} | ||
}; | ||
const response = await this._response; | ||
List.prototype.next = function () { | ||
var i = pluck(this.items, 'id').indexOf(this.current) + 1; | ||
var item = this.items[i % this.items.length]; | ||
if (!response.ok) { | ||
throw new HTTPError(response); | ||
} | ||
this.change(item.id); | ||
return response.clone()[type](); | ||
}); | ||
} | ||
if (item.disabled) { | ||
this.next(); | ||
return this._response; | ||
} | ||
}; | ||
List.prototype.onKeypress = function (ch, key) { | ||
if (!key) { | ||
return; | ||
} | ||
_retry(fn) { | ||
if (!retryMethods.has(this._options.method.toLowerCase())) { | ||
return fn; | ||
} | ||
this.emit('keypress', key, this.current); | ||
const retry = async () => { | ||
try { | ||
return await fn(); | ||
} catch (error) { | ||
const shouldRetryStatusCode = error instanceof HTTPError ? retryStatusCodes.has(error.response.status) : true; | ||
if (!(error instanceof TimeoutError) && shouldRetryStatusCode && this._retryCount < this._options.retry) { | ||
this._retryCount++; | ||
const BACKOFF_FACTOR = 0.3; | ||
const delaySeconds = BACKOFF_FACTOR * (2 ** (this._retryCount - 1)); | ||
await delay(delaySeconds * 1000); | ||
return retry(); | ||
} | ||
if (key.name === 'escape' || key.ctrl && key.name === 'c') { | ||
this.destroy(); | ||
throw error; | ||
} | ||
}; | ||
return retry; | ||
} | ||
if (key.name === 'up') { | ||
this.prev(); | ||
_fetch() { | ||
return timeout(window.fetch(this._input, this._options), this._timeout); | ||
} | ||
} | ||
if (key.name === 'down') { | ||
this.next(); | ||
const createInstance = (defaults = {}) => { | ||
const ky = (input, options) => new Ky(input, deepMerge({}, defaults, options)); | ||
for (const method of requestMethods) { | ||
ky[method] = (input, options) => new Ky(input, deepMerge({}, defaults, options, {method})); | ||
} | ||
if (key.name === 'return') { | ||
this.emit('select', this.current); | ||
} | ||
return ky; | ||
}; | ||
module.exports = List; | ||
module.exports = createInstance(); | ||
module.exports.extend = defaults => createInstance(defaults); | ||
module.exports.HTTPError = HTTPError; | ||
module.exports.TimeoutError = TimeoutError; |
{ | ||
"name": "ky", | ||
"version": "0.1.0", | ||
"description": "Easy list UI for your CLI app", | ||
"license": "MIT", | ||
"repository": "sindresorhus/ky", | ||
"bin": "cli.js", | ||
"author": { | ||
"name": "Sindre Sorhus", | ||
"email": "sindresorhus@gmail.com", | ||
"url": "http://sindresorhus.com" | ||
}, | ||
"engines": { | ||
"node": ">=0.10.0" | ||
}, | ||
"files": [ | ||
"index.js" | ||
], | ||
"devDependencies": { | ||
"mocha": "*" | ||
}, | ||
"dependencies": { | ||
"chalk": "^0.4.0", | ||
"keypress": "^0.2.1" | ||
} | ||
"name": "ky", | ||
"version": "0.2.0", | ||
"description": "Tiny and elegant HTTP client based on the browser Fetch API", | ||
"license": "MIT", | ||
"repository": "sindresorhus/ky", | ||
"author": { | ||
"name": "Sindre Sorhus", | ||
"email": "sindresorhus@gmail.com", | ||
"url": "sindresorhus.com" | ||
}, | ||
"engines": { | ||
"node": ">=8" | ||
}, | ||
"scripts": { | ||
"test": "xo && nyc ava" | ||
}, | ||
"files": [ | ||
"index.js" | ||
], | ||
"keywords": [ | ||
"fetch", | ||
"request", | ||
"requests", | ||
"http", | ||
"https", | ||
"fetching", | ||
"get", | ||
"url", | ||
"curl", | ||
"wget", | ||
"net", | ||
"network", | ||
"ajax", | ||
"api", | ||
"rest", | ||
"xhr", | ||
"browser", | ||
"got", | ||
"axios", | ||
"node-fetch" | ||
], | ||
"devDependencies": { | ||
"ava": "1.0.0-beta.8", | ||
"body": "^5.1.0", | ||
"create-test-server": "2.1.1", | ||
"delay": "^4.0.0", | ||
"node-fetch": "^2.2.0", | ||
"nyc": "^13.0.1", | ||
"xo": "^0.23.0" | ||
}, | ||
"xo": { | ||
"envs": [ | ||
"browser" | ||
] | ||
} | ||
} |
155
readme.md
@@ -1,6 +0,31 @@ | ||
# ky [![Build Status](https://travis-ci.org/sindresorhus/ky.svg?branch=master)](https://travis-ci.org/sindresorhus/ky) | ||
<div align="center"> | ||
<br> | ||
<div> | ||
<img width="600" src="media/logo.svg" alt="ky"> | ||
</div> | ||
<br> | ||
<br> | ||
<br> | ||
</div> | ||
> Easy list UI for your CLI app | ||
> Ky is a tiny and elegant HTTP client based on the browser [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) | ||
[![Build Status](https://travis-ci.com/sindresorhus/ky.svg?branch=master)](https://travis-ci.com/sindresorhus/ky) [![codecov](https://codecov.io/gh/sindresorhus/ky/branch/master/graph/badge.svg)](https://codecov.io/gh/sindresorhus/ky) | ||
Ky targets [modern browsers](#browser-support). For older browsers, you will need to transpile and use a [`fetch` polyfill](https://github.com/github/fetch). For Node.js, check out [Got](https://github.com/sindresorhus/got). | ||
1 KB *(minified & gzipped)*, one file, and no dependencies. | ||
## Benefits over plain `fetch` | ||
- Simpler API | ||
- Method shortcuts (`ky.post()`) | ||
- Treats non-200 status codes as errors | ||
- Retries failed requests | ||
- JSON option | ||
- Timeout support | ||
- Instances with custom defaults | ||
## Install | ||
@@ -12,38 +37,122 @@ | ||
<a href="https://www.patreon.com/sindresorhus"> | ||
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"> | ||
</a> | ||
## Usage | ||
```js | ||
var chalk = require('chalk'); | ||
var List = require('ky'); | ||
import ky from 'ky'; | ||
var items = [ | ||
{id: 'foo', title: 'Ponies'}, | ||
{id: 'bar', title: 'OMG NO', disabled: true}, | ||
{id: 'baz', title: 'javaSCript'}, | ||
{id: 'bal', title: 'Some Highlander Whiz'} | ||
]; | ||
(async () => { | ||
const json = await ky.post('https://some-api.com', {json: {foo: true}}).json(); | ||
var list = new List(items, { | ||
header: chalk.bgYellow.black('Unicorns & rainbows'), | ||
footer: 'IAMA FOOTER' | ||
}); | ||
console.log(json); | ||
//=> `{data: '🦄'}` | ||
})(); | ||
``` | ||
list.init(); | ||
With plain `fetch`, it would be: | ||
list.on('keypress', function (key, selected) { | ||
if (key.name === 'return') { | ||
list.header = require('chalk').red('Candy??'); | ||
list.render(); | ||
```js | ||
(async () => { | ||
class HTTPError extends Error {} | ||
const response = await fetch('https://sindresorhus.com', { | ||
method: 'POST', | ||
body: JSON.stringify({foo: true}), | ||
headers: { | ||
'content-type': 'application/json' | ||
} | ||
}); | ||
if (!response.ok) { | ||
throw new HTTPError(`Fetch error:`, response.statusText); | ||
} | ||
}); | ||
list.on('change', function () { | ||
list.header = 'change'; | ||
}); | ||
const json = await response.json(); | ||
console.log(json); | ||
//=> `{data: '🦄'}` | ||
})(); | ||
``` | ||
## API | ||
### ky(input, [options]) | ||
The `input` and `options` are the same as [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), with some exceptions: | ||
- The `credentials` option is `same-origin` by default, which is the default in the spec too, but not all browsers have caught up yet. | ||
- Adds some more options. See below. | ||
Returns a [`Response` object](https://developer.mozilla.org/en-US/docs/Web/API/Response) with [`Body` methods](https://developer.mozilla.org/en-US/docs/Web/API/Body#Methods) added for convenience. So you can, for example, call `ky.json()` directly on the `Response` without having to await it first. Unlike the `Body` methods of `window.Fetch`; these will throw an `HTTPError` if the response status is not in the range `200...299`. | ||
#### options | ||
Type: `Object` | ||
##### json | ||
Type: `Object` | ||
Shortcut for sending JSON. Use this instead of the `body` option. Accepts a plain object which will be `JSON.stringify()`'d and the correct header will be set for you. | ||
### ky.get(input, [options]) | ||
### ky.post(input, [options]) | ||
### ky.put(input, [options]) | ||
### ky.patch(input, [options]) | ||
### ky.head(input, [options]) | ||
### ky.delete(input, [options]) | ||
Sets `options.method` to the method name and makes a request. | ||
#### retry | ||
Type: `number`<br> | ||
Default: `2` | ||
Retry failed requests made with one of the below methods that result in a network error or one of the below status codes. | ||
Methods: `GET` `PUT` `HEAD` `DELETE` `OPTIONS` `TRACE`<br> | ||
Status codes: [`408`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) [`413`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) [`429`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) [`500`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) [`502`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) [`503`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) [`504`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) | ||
#### timeout | ||
Type: `number`<br> | ||
Default: `10000` | ||
Timeout in milliseconds for getting a response. | ||
### ky.extend(defaultOptions) | ||
Create a new `ky` instance with some defaults overridden with your own. | ||
#### defaultOptions | ||
Type: `Object` | ||
### ky.HTTPError | ||
Exposed for `instanceof` checks. The error has a `response` property with the [`Response` object](https://developer.mozilla.org/en-US/docs/Web/API/Response). | ||
### ky.TimeoutError | ||
The error thrown when the request times out. | ||
## Browser support | ||
The latest version of Chrome, Firefox, and Safari. | ||
## Related | ||
- [got](https://github.com/sindresorhus/got) - Simplified HTTP requests for Node.js | ||
## License | ||
MIT © [Sindre Sorhus](https://sindresorhus.com) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
10338
0
145
158
7
- Removedchalk@^0.4.0
- Removedkeypress@^0.2.1
- Removedansi-styles@1.0.0(transitive)
- Removedchalk@0.4.0(transitive)
- Removedhas-color@0.1.7(transitive)
- Removedkeypress@0.2.1(transitive)
- Removedstrip-ansi@0.1.1(transitive)