Comparing version 0.1.2 to 1.0.0
@@ -1,1 +0,20 @@ | ||
declare module 'javascript-lp-solver/src/solver'; | ||
declare module 'javascript-lp-solver/src/solver' { | ||
export interface Model { | ||
opType: 'max'; | ||
optimize: string; | ||
constraints: Record<string, { max: number }>; | ||
variables: {[variable: string]: Record<string, number>}; | ||
ints?: string[]; | ||
} | ||
export interface ResultMeta { | ||
feasible: boolean; | ||
result: number; | ||
bounded: boolean; | ||
isIntegral: boolean; | ||
} | ||
export type Result = ResultMeta & {[variable: string]: number}; | ||
export function Solve(model: Model): Result; | ||
} |
@@ -84,2 +84,4 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var chalk_1 = __importDefault(require("chalk")); | ||
var commander_1 = require("commander"); | ||
var he_1 = require("he"); | ||
@@ -90,2 +92,15 @@ var solver_1 = __importDefault(require("javascript-lp-solver/src/solver")); | ||
var node_fetch_1 = __importDefault(require("node-fetch")); | ||
var version = require('../package.json').version; | ||
commander_1.program | ||
.version(version) | ||
.arguments('<query>') | ||
.option('--npm', 'Output npm install commands') | ||
.option('-y, --yarn', 'Output yarn add commands') | ||
.option('-n, --num <number>', 'Maximum number of results to show', Number, 10) | ||
.option('--repo', 'Show repo URL, even if package specifies a homepage') | ||
.option('--debug', 'Enable debug logging') | ||
.option('--bundled', 'Only show packages with bundled types') | ||
.option('--dt', 'Only show packages with types on DefinitelyTyped (@types)') | ||
.option('-u, --untyped', 'Search all packages, even those without type declarations.') | ||
.parse(process.argv); | ||
var SEARCH_ENDPOINT = 'https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search'; | ||
@@ -103,5 +118,10 @@ var ATTRIBUTES = [ | ||
]; | ||
var filters = { | ||
default: 'types.ts:"definitely-typed" OR types.ts:"included"', | ||
dt: 'types.ts:"definitely-typed"', | ||
bundled: 'types.ts:"included"', | ||
}; | ||
var PARAMS = { | ||
hitsPerPage: 20, | ||
filters: 'types.ts:"definitely-typed" OR types.ts:"included"', | ||
filters: filters.default, | ||
attributes: ATTRIBUTES.join(','), | ||
@@ -127,2 +147,3 @@ 'x-algolia-agent': 'Algolia for vanilla JavaScript (lite) 3.27.1', | ||
format: function (h) { return h.objectID; }, | ||
highlight: function (v, h) { return highlightValue(v, h._highlightResult.name); }, | ||
importance: 100, | ||
@@ -149,4 +170,15 @@ }, | ||
{ | ||
header: 'npm', | ||
format: function (h) { return makeInstallCommand('npm install', h); }, | ||
importance: -1, | ||
}, | ||
{ | ||
header: 'yarn', | ||
format: function (h) { return makeInstallCommand('yarn add', h); }, | ||
importance: -1, | ||
}, | ||
{ | ||
header: 'description', | ||
format: function (h) { return he_1.decode(h.description || ''); }, | ||
highlight: function (v, h) { return highlightValue(v, h._highlightResult.description); }, | ||
maxWidth: 40, | ||
@@ -159,2 +191,3 @@ importance: 25, | ||
format: function (h) { return he_1.decode(h.description || ''); }, | ||
highlight: function (v, h) { return highlightValue(v, h._highlightResult.description); }, | ||
maxWidth: 60, | ||
@@ -167,2 +200,3 @@ importance: 30, | ||
format: function (h) { return he_1.decode(h.description || ''); }, | ||
highlight: function (v, h) { return highlightValue(v, h._highlightResult.description); }, | ||
importance: 35, | ||
@@ -172,13 +206,12 @@ mutexGroup: 'desc', | ||
{ | ||
header: 'date', | ||
format: function (h) { return moment_1.default(h.modified).format('YYYY-MM-DD'); }, | ||
importance: 1, | ||
}, | ||
{ | ||
header: 'updated', | ||
format: function (h) { return moment_1.default(h.modified).fromNow(); }, | ||
importance: 5, | ||
align: 'right', | ||
}, | ||
{ | ||
header: 'date', | ||
format: function (h) { return moment_1.default(h.modified).format('YYYY-MM-DD'); }, | ||
importance: 1, | ||
}, | ||
{ | ||
header: 'homepage', | ||
@@ -188,3 +221,19 @@ format: function (h) { return h.homepage || (h.repository ? h.repository.url : ''); }, | ||
}, | ||
{ | ||
header: 'repo', | ||
format: function (h) { return h.repository ? h.repository.url : ''; }, | ||
importance: -1, | ||
} | ||
]; | ||
function makeInstallCommand(cmd, _a) { | ||
var types = _a.types, objectID = _a.objectID; | ||
var install = cmd + " " + objectID; | ||
if (types.ts === 'included') { | ||
return install; | ||
} | ||
else if (types.ts === 'definitely-typed') { | ||
return install + " && " + cmd + " -D " + types.definitelyTyped; | ||
} | ||
return ''; | ||
} | ||
function pickColumns(widths) { | ||
@@ -209,3 +258,4 @@ var e_1, _a; | ||
columns.forEach(function (c, i) { | ||
constraints[i] = { max: 1 }; | ||
// name is included here just for debugging. | ||
constraints[i] = { max: 1, name: c.header + (c.maxWidth ? '/' + c.maxWidth : '') }; | ||
}); | ||
@@ -222,3 +272,9 @@ var model = { | ||
}; | ||
if (commander_1.program.debug) { | ||
console.log('Column LP model:', model); | ||
} | ||
var result = solver_1.default.Solve(model); | ||
if (commander_1.program.debug) { | ||
console.log('LP result:', result); | ||
} | ||
if (result.feasible) { | ||
@@ -241,2 +297,31 @@ return columns.map(function (c, i) { return result[i] ? i : null; }).filter(isNonNullish); | ||
} | ||
function highlightValue(val, highlightResult) { | ||
var e_2, _a; | ||
if (!highlightResult || highlightResult.matchLevel === 'none') { | ||
return val; | ||
} | ||
else if (highlightResult.fullyHighlighted) { | ||
return chalk_1.default.bold(val); | ||
} | ||
try { | ||
for (var _b = __values(highlightResult.matchedWords), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var word = _c.value; | ||
val = val.replace(new RegExp(word, 'ig'), chalk_1.default.bold(word)); | ||
} | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b); | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
} | ||
return val; | ||
// Alternatively, this could use highlightResult.value. | ||
// The problem there is that string padding has already happened. | ||
// 'Foo <em>bar</em> baz <em>quux</em>' --> | ||
// [ 'Foo ', '<em>bar', ' baz ', '<em>quux', '' ] | ||
// const parts = highlightResult.value.split(/(<em>.*?)<\/em>/); | ||
// return parts.map(p => p.startsWith('<em>') ? chalk.bold(p.slice(4)) : p).join(''); | ||
} | ||
function isNonNullish(x) { | ||
@@ -252,3 +337,3 @@ return x !== null && x !== undefined; | ||
} | ||
function printTable(rows) { | ||
function printTable(rows, hits) { | ||
var cols = columns.map(function (c, j) { return __spread([ | ||
@@ -260,3 +345,12 @@ c.header.toUpperCase() | ||
var colIndices = pickColumns(widths); | ||
var pickedCols = colIndices.map(function (i) { return formattedCols[i]; }); | ||
var pickedCols = colIndices.map(function (i) { | ||
var spec = columns[i]; | ||
var highlight = spec.highlight; | ||
if (!highlight) { | ||
return formattedCols[i]; | ||
} | ||
else { | ||
return formattedCols[i].map(function (v, j) { return j > 0 ? highlight(v, hits[j - 1]) : v; }); | ||
} | ||
}); | ||
var _loop_1 = function (i) { | ||
@@ -270,37 +364,115 @@ var cols_1 = pickedCols.map(function (c) { return c[i]; }); | ||
} | ||
function adjustImportance(header, newImportance) { | ||
var e_3, _a; | ||
var adjusted = false; | ||
try { | ||
for (var columns_1 = __values(columns), columns_1_1 = columns_1.next(); !columns_1_1.done; columns_1_1 = columns_1.next()) { | ||
var col = columns_1_1.value; | ||
if (col.header === header) { | ||
col.importance = newImportance; | ||
adjusted = true; | ||
} | ||
} | ||
} | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
finally { | ||
try { | ||
if (columns_1_1 && !columns_1_1.done && (_a = columns_1.return)) _a.call(columns_1); | ||
} | ||
finally { if (e_3) throw e_3.error; } | ||
} | ||
if (!adjusted) { | ||
throw new Error("Unable to find column with header " + header); | ||
} | ||
} | ||
function applyFlags() { | ||
// Add special coluns if the user asks for them. | ||
if (commander_1.program.yarn) { | ||
adjustImportance('yarn', 1000); | ||
} | ||
if (commander_1.program.npm) { | ||
adjustImportance('npm', 1000); | ||
} | ||
if (commander_1.program.yarn || commander_1.program.npm) { | ||
adjustImportance('types', 25); | ||
} | ||
if (commander_1.program.repo) { | ||
adjustImportance('repo', 1000); | ||
adjustImportance('homepage', -1); | ||
} | ||
var flags = ['untyped', 'dt', 'bundled']; | ||
if (lodash_1.default.sum(flags.map(function (flag) { return commander_1.program[flag] ? 1 : 0; })) > 1) { | ||
throw new Error("May only specify one of " + flags); | ||
} | ||
if (commander_1.program.untyped) { | ||
delete PARAMS.filters; | ||
} | ||
else if (commander_1.program.dt) { | ||
PARAMS.filters = filters.dt; | ||
} | ||
else if (commander_1.program.bundled) { | ||
PARAMS.filters = filters.bundled; | ||
} | ||
} | ||
(function () { return __awaiter(void 0, void 0, void 0, function () { | ||
var _a, query, params, _b, _c, _d, k, v, qs, response, result, hits, table; | ||
var e_2, _e; | ||
return __generator(this, function (_f) { | ||
switch (_f.label) { | ||
var query, num, params, _a, _b, _c, k, v, qs, url, startMs, response, elapsedMs, result, hits, table; | ||
var e_4, _d; | ||
return __generator(this, function (_e) { | ||
switch (_e.label) { | ||
case 0: | ||
_a = __read(process.argv, 3), query = _a[2]; | ||
query = commander_1.program.args.join(' '); | ||
applyFlags(); | ||
num = commander_1.program.num; | ||
PARAMS.hitsPerPage = Math.floor(num * 1.5); | ||
params = new URLSearchParams(); | ||
try { | ||
for (_b = __values(Object.entries(PARAMS)), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
_d = __read(_c.value, 2), k = _d[0], v = _d[1]; | ||
for (_a = __values(Object.entries(PARAMS)), _b = _a.next(); !_b.done; _b = _a.next()) { | ||
_c = __read(_b.value, 2), k = _c[0], v = _c[1]; | ||
params.set(k, '' + v); | ||
} | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_e = _b.return)) _e.call(_b); | ||
if (_b && !_b.done && (_d = _a.return)) _d.call(_a); | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
finally { if (e_4) throw e_4.error; } | ||
} | ||
params.set('query', query); | ||
qs = params.toString(); | ||
return [4 /*yield*/, node_fetch_1.default(SEARCH_ENDPOINT + "?" + qs)]; | ||
url = SEARCH_ENDPOINT + "?" + qs; | ||
if (commander_1.program.debug) { | ||
console.log('Algolia query params:', params); | ||
console.log('Fetching', url); | ||
} | ||
startMs = Date.now(); | ||
return [4 /*yield*/, node_fetch_1.default(url)]; | ||
case 1: | ||
response = _f.sent(); | ||
response = _e.sent(); | ||
if (!response.ok) { | ||
throw new Error(response.status + " " + response.statusText); | ||
} | ||
elapsedMs = Date.now() - startMs; | ||
if (commander_1.program.debug) { | ||
console.log('Algolia responded in', elapsedMs, 'ms'); | ||
} | ||
return [4 /*yield*/, response.json()]; | ||
case 2: | ||
result = _f.sent(); | ||
result = _e.sent(); | ||
hits = result.hits.filter(function (hit) { return !hit.objectID.startsWith('@types/'); }); | ||
table = hits.slice(0, 10).map(formatResult); | ||
printTable(table); | ||
if (hits.length === 0) { | ||
console.log('No results. Try dtsearch -u to include packages without types.'); | ||
return [2 /*return*/]; | ||
} | ||
if (commander_1.program.debug) { | ||
console.log("Got " + result.hits.length + " results, pared down to " + hits.length); | ||
console.log(result); | ||
} | ||
hits = hits.slice(0, num); | ||
table = hits.map(formatResult); | ||
printTable(table, hits); | ||
if (hits.length < num) { | ||
console.log("\nOnly " + hits.length + " result" + (hits.length > 1 ? 's' : '') + ". " + | ||
"Try dtsearch -u to include packages without types."); | ||
} | ||
return [2 /*return*/]; | ||
@@ -307,0 +479,0 @@ } |
{ | ||
"name": "dtsearch", | ||
"version": "0.1.2", | ||
"description": "Search for npm packages with types, either on Definitely Typed or bundled.", | ||
"version": "1.0.0", | ||
"description": "Find packages with TypeScript types, either bundled or on Definitely Typed", | ||
"keywords": [ | ||
"definitelytyped", | ||
"typescript", | ||
"dts", | ||
"types", | ||
"search" | ||
], | ||
"main": "dist/index.js", | ||
@@ -12,2 +19,3 @@ "author": "Dan Vanderkam (danvdk@gmail.com)", | ||
"devDependencies": { | ||
"@types/chalk": "^2.2.0", | ||
"@types/he": "^1.1.1", | ||
@@ -19,2 +27,4 @@ "@types/lodash": "^4.14.149", | ||
"dependencies": { | ||
"chalk": "^3.0.0", | ||
"commander": "^5.0.0", | ||
"he": "^1.2.0", | ||
@@ -21,0 +31,0 @@ "javascript-lp-solver": "^0.4.24", |
@@ -5,4 +5,82 @@ # dtsearch | ||
Usage with `npx`: | ||
``` | ||
$ npx dtsearch sprintf | ||
DLS NAME TYPES DESCRIPTION | ||
533.3k sprintf @types/sprintf sprintf() for node.js | ||
47.4m sprintf-js @types/sprintf-js JavaScript sprintf implementation | ||
82.9m extsprintf @types/extsprintf extended POSIX-style sprintf | ||
2.1m ssf <bundled> Format data using ECMA-376 spreadsheet Format Codes | ||
1.6m printj <bundled> Pure-JS printf | ||
123k voca @types/voca The ultimate JavaScript string library | ||
746.4k printf <bundled> Full implementation of the `printf` family in pure JS. | ||
1.5k sprintfjs <bundled> POSIX sprintf(3)-style String Formatting for JavaScript | ||
169 @jitesoft/sprintf <bundled> sprintf function for javascript. | ||
94 stringd <bundled> A string variable parser for JavaScript | ||
``` | ||
Alternatively, you can install `dtsearch` globally using either: | ||
npm install --global dtsearch | ||
yarn global add dtsearch | ||
You can use `--yarn` or `--npm` to produce copy/pastable commands to depend on packages _and_ their types: | ||
![Demonstration of search for a library and installing it using yarn](demo.gif) | ||
## Background | ||
There are two ways to distribute TypeScript types for a package on npm: | ||
1. With the package itself ("bundled" or "included"). This is common if the package is written in TypeScript, or if the owner is committed to maintaining its type declarations. The tell-tale sign of bundled types is a `typings` entry in `package.json`. | ||
2. As a separate `@types` package on [DefinitelyTyped]. This is more common for packages which are written in plain JavaScript or another language. The type declarations are often written by someone other than the package author. | ||
Both approaches are common and there are many tradeoffs between them. | ||
As a TypeScript user, you'll often find yourself wanting to search for a package that does X and has type declarations (of either form). The usual approach is to search for packages and then check if they have type declarations ([yarnpkg] has recently added TypeScript badges which help with this). | ||
Once you've found a package, you need to run different commands depending on whether it bundles its types or gets them from DefinitelyTyped. For example, using `yarn` and [`moment`][moment]: | ||
yarn add moment # bundled types | ||
# Types on DefinitelyTyped | ||
yarn add moment-timezone | ||
yarn add -D @types/moment-timzeone | ||
`dtsearch` aims to solve these problems with a fast, simple CLI. It lets you search only packages with types and shows you the exact commands you need to run to add them to your project. | ||
## How this works | ||
This uses Algolia's [npm search][2], the same search that you find on [yarnpkg]. | ||
## Options | ||
- `-n`, `--num <number>` Maximum number of results to show (default: 10) | ||
- `--npm` Output `npm install` commands | ||
- `-y`, `--yarn` Output yarn add commands | ||
- `--bundled` Only show packages with bundled types | ||
- `--dt` Only show packages with types on DefinitelyTyped (@types) | ||
- `-u`, `--untyped` Search all packages, even those without type declarations. | ||
- `--repo` Show repo URLs, even if package specifies a homepage | ||
- `--debug` Enable debug logging | ||
## Related Work | ||
- The old [`typings search`](https://yarnpkg.com/package/typings) command from c. 2016 (before `@types`). | ||
- Microsoft's [TypeSearch](https://microsoft.github.io/TypeSearch/). Unfortunately this only searches DefinitelyTyped and only searches package names. It does not search bundled types or package descriptions. | ||
- [yarnpkg]'s search. This shows small "TS" icons next to packages with type declarations, either bundled or on DT. It does not surface a filter to search only packages with type declarations, however. | ||
- [pikapkg] lets you search packages with a [`has:types`][pikasearch] filter. This only searches bundled typings; it does not consider types on DT. | ||
## Support | ||
If you like this tool, consider buying my book, [_Effective TypeScript_][ets]. [Chapter 6] and particularly Item 46 ("Understand the Three Versions Involved in Type Declarations") are all about the trials and tribulations of getting TypeScript types for your dependencies. | ||
[DefinitelyTyped]: https://github.com/DefinitelyTyped/DefinitelyTyped | ||
[2]: https://discourse.algolia.com/t/2016-algolia-community-gift-yarn-package-search/319 | ||
[moment]: https://momentjs.com/ | ||
[yarnpkg]: https://yarnpkg.com/ | ||
[pikapkg]: https://www.pika.dev/ | ||
[pikasearch]: https://www.pika.dev/search?q=has%3Atypes%20moment | ||
[ets]: https://effectivetypescript.com/ | ||
[Chapter 6]: https://effectivetypescript.com/#Chapter-6-Types-Declarations-and-types |
202
src/index.ts
@@ -0,1 +1,3 @@ | ||
import chalk from 'chalk'; | ||
import {program} from 'commander'; | ||
import {decode} from 'he'; | ||
@@ -6,4 +8,19 @@ import solver from 'javascript-lp-solver/src/solver'; | ||
import fetch from 'node-fetch'; | ||
import { AlgoliaResponse, Hit } from './response'; | ||
import { AlgoliaResponse, Hit, Description } from './response'; | ||
const version = require('../package.json').version; | ||
program | ||
.version(version) | ||
.arguments('<query>') | ||
.option('--npm', 'Output npm install commands') | ||
.option('-y, --yarn', 'Output yarn add commands') | ||
.option('-n, --num <number>', 'Maximum number of results to show', Number, 10) | ||
.option('--repo', 'Show repo URL, even if package specifies a homepage') | ||
.option('--debug', 'Enable debug logging') | ||
.option('--bundled', 'Only show packages with bundled types') | ||
.option('--dt', 'Only show packages with types on DefinitelyTyped (@types)') | ||
.option('-u, --untyped', 'Search all packages, even those without type declarations.') | ||
.parse(process.argv); | ||
const SEARCH_ENDPOINT = 'https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search' | ||
@@ -21,5 +38,10 @@ const ATTRIBUTES = [ | ||
]; | ||
const filters = { | ||
default: 'types.ts:"definitely-typed" OR types.ts:"included"', | ||
dt: 'types.ts:"definitely-typed"', | ||
bundled: 'types.ts:"included"', | ||
}; | ||
const PARAMS = { | ||
hitsPerPage: 20, | ||
filters: 'types.ts:"definitely-typed" OR types.ts:"included"', | ||
filters: filters.default, | ||
attributes: ATTRIBUTES.join(','), | ||
@@ -36,2 +58,3 @@ 'x-algolia-agent': 'Algolia for vanilla JavaScript (lite) 3.27.1', | ||
format: (hit: Hit) => string; | ||
highlight?: (value: string, hit: Hit) => string; | ||
importance: number; | ||
@@ -56,2 +79,3 @@ mutexGroup?: string; | ||
format: h => h.objectID, | ||
highlight: (v, h) => highlightValue(v, h._highlightResult.name), | ||
importance: 100, | ||
@@ -72,4 +96,15 @@ }, | ||
{ | ||
header: 'npm', | ||
format: h => makeInstallCommand('npm install', h), | ||
importance: -1, | ||
}, | ||
{ | ||
header: 'yarn', | ||
format: h => makeInstallCommand('yarn add', h), | ||
importance: -1, | ||
}, | ||
{ | ||
header: 'description', | ||
format: h => decode(h.description || ''), | ||
highlight: (v, h) => highlightValue(v, h._highlightResult.description), | ||
maxWidth: 40, | ||
@@ -82,2 +117,3 @@ importance: 25, | ||
format: h => decode(h.description || ''), | ||
highlight: (v, h) => highlightValue(v, h._highlightResult.description), | ||
maxWidth: 60, | ||
@@ -90,2 +126,3 @@ importance: 30, | ||
format: h => decode(h.description || ''), | ||
highlight: (v, h) => highlightValue(v, h._highlightResult.description), | ||
importance: 35, | ||
@@ -95,13 +132,12 @@ mutexGroup: 'desc', | ||
{ | ||
header: 'date', | ||
format: h => moment(h.modified).format('YYYY-MM-DD'), | ||
importance: 1, | ||
}, | ||
{ | ||
header: 'updated', | ||
format: h => moment(h.modified).fromNow(), | ||
importance: 5, | ||
align: 'right', | ||
}, | ||
{ | ||
header: 'date', | ||
format: h => moment(h.modified).format('YYYY-MM-DD'), | ||
importance: 1, | ||
}, | ||
{ | ||
header: 'homepage', | ||
@@ -111,16 +147,32 @@ format: h => h.homepage || (h.repository ? h.repository.url : ''), | ||
}, | ||
{ | ||
header: 'repo', | ||
format: h => h.repository ? h.repository.url : '', | ||
importance: -1, | ||
} | ||
]; | ||
function makeInstallCommand(cmd: string, {types, objectID}: Hit): string { | ||
const install = `${cmd} ${objectID}`; | ||
if (types.ts === 'included') { | ||
return install; | ||
} else if (types.ts === 'definitely-typed') { | ||
return `${install} && ${cmd} -D ${types.definitelyTyped}`; | ||
} | ||
return ''; | ||
} | ||
function pickColumns(widths: number[]): number[] { | ||
const mutexGroups = new Set(columns.map(c => c.mutexGroup).filter(isNonNullish)); | ||
const constraints = {width: {max: 1 + (process.stdout.columns || 80)}}; | ||
const constraints: solver.Model['constraints'] = {width: {max: 1 + (process.stdout.columns || 80)}}; | ||
const mutexes = [...mutexGroups.keys()]; | ||
for (const mutex of mutexes) { | ||
(constraints as any)[mutex] = {max: 1}; | ||
constraints[mutex] = {max: 1}; | ||
} | ||
columns.forEach((c, i) => { | ||
(constraints as any)[i] = {max: 1}; | ||
// name is included here just for debugging. | ||
(constraints as any)[i] = {max: 1, name: c.header + (c.maxWidth ? '/' + c.maxWidth : '')}; | ||
}); | ||
const model = { | ||
const model: solver.Model = { | ||
opType: 'max', | ||
@@ -141,3 +193,10 @@ optimize: 'importance', | ||
if (program.debug) { | ||
console.log('Column LP model:', model); | ||
} | ||
const result = solver.Solve(model); | ||
if (program.debug) { | ||
console.log('LP result:', result); | ||
} | ||
if (result.feasible) { | ||
@@ -164,2 +223,21 @@ return columns.map((c, i) => result[i] ? i : null).filter(isNonNullish); | ||
function highlightValue(val: string, highlightResult: Description | null) { | ||
if (!highlightResult || highlightResult.matchLevel === 'none') { | ||
return val; | ||
} else if (highlightResult.fullyHighlighted) { | ||
return chalk.bold(val); | ||
} | ||
for (const word of highlightResult.matchedWords) { | ||
val = val.replace(new RegExp(word, 'ig'), chalk.bold(word)); | ||
} | ||
return val; | ||
// Alternatively, this could use highlightResult.value. | ||
// The problem there is that string padding has already happened. | ||
// 'Foo <em>bar</em> baz <em>quux</em>' --> | ||
// [ 'Foo ', '<em>bar', ' baz ', '<em>quux', '' ] | ||
// const parts = highlightResult.value.split(/(<em>.*?)<\/em>/); | ||
// return parts.map(p => p.startsWith('<em>') ? chalk.bold(p.slice(4)) : p).join(''); | ||
} | ||
function isNonNullish<T>(x: T | null | undefined): x is T { | ||
@@ -173,3 +251,3 @@ return x !== null && x !== undefined; | ||
function printTable(rows: string[][]) { | ||
function printTable(rows: string[][], hits: Hit[]) { | ||
const cols = columns.map((c, j) => [ | ||
@@ -181,3 +259,11 @@ c.header.toUpperCase(), ...rows.map(r => r[j]) | ||
const colIndices = pickColumns(widths); | ||
const pickedCols = colIndices.map(i => formattedCols[i]); | ||
const pickedCols = colIndices.map(i => { | ||
const spec = columns[i]; | ||
const {highlight} = spec; | ||
if (!highlight) { | ||
return formattedCols[i]; | ||
} else { | ||
return formattedCols[i].map((v, j) => j > 0 ? highlight(v, hits[j - 1]) : v) | ||
} | ||
}); | ||
@@ -190,5 +276,54 @@ for (let i = 0; i <= rows.length; i++) { | ||
function adjustImportance(header: string, newImportance: number) { | ||
let adjusted = false; | ||
for (const col of columns) { | ||
if (col.header === header) { | ||
col.importance = newImportance; | ||
adjusted = true; | ||
} | ||
} | ||
if (!adjusted) { | ||
throw new Error(`Unable to find column with header ${header}`); | ||
} | ||
} | ||
function applyFlags() { | ||
// Add special coluns if the user asks for them. | ||
if (program.yarn) { | ||
adjustImportance('yarn', 1000); | ||
} | ||
if (program.npm) { | ||
adjustImportance('npm', 1000); | ||
} | ||
if (program.yarn || program.npm) { | ||
adjustImportance('types', 25); | ||
} | ||
if (program.repo) { | ||
adjustImportance('repo', 1000); | ||
adjustImportance('homepage', -1); | ||
} | ||
const flags = ['untyped', 'dt', 'bundled']; | ||
if (_.sum(flags.map(flag => program[flag] ? 1 : 0)) > 1) { | ||
throw new Error(`May only specify one of ${flags}`); | ||
} | ||
if (program.untyped) { | ||
delete PARAMS.filters; | ||
} else if (program.dt) { | ||
PARAMS.filters = filters.dt; | ||
} else if (program.bundled) { | ||
PARAMS.filters = filters.bundled; | ||
} | ||
} | ||
(async () => { | ||
const [, , query] = process.argv; | ||
const query = program.args.join(' '); | ||
applyFlags(); | ||
// Overfetch a bit in case there are @types results. | ||
const {num} = program; | ||
PARAMS.hitsPerPage = Math.floor(num * 1.5); | ||
const params = new URLSearchParams(); | ||
@@ -200,14 +335,43 @@ for (const [k, v] of Object.entries(PARAMS)) { | ||
const qs = params.toString(); | ||
const url = `${SEARCH_ENDPOINT}?${qs}`; | ||
const response = await fetch(`${SEARCH_ENDPOINT}?${qs}`); | ||
if (program.debug) { | ||
console.log('Algolia query params:', params); | ||
console.log('Fetching', url); | ||
} | ||
const startMs = Date.now(); | ||
const response = await fetch(url); | ||
if (!response.ok) { | ||
throw new Error(`${response.status} ${response.statusText}`); | ||
} | ||
const elapsedMs = Date.now() - startMs; | ||
if (program.debug) { | ||
console.log('Algolia responded in', elapsedMs, 'ms'); | ||
} | ||
const result: AlgoliaResponse = await response.json(); | ||
const hits = result.hits.filter(hit => !hit.objectID.startsWith('@types/')); | ||
const table = hits.slice(0, 10).map(formatResult); | ||
printTable(table); | ||
let hits = result.hits.filter(hit => !hit.objectID.startsWith('@types/')); | ||
if (hits.length === 0) { | ||
console.log('No results. Try dtsearch -u to include packages without types.'); | ||
return; | ||
} | ||
if (program.debug) { | ||
console.log(`Got ${result.hits.length} results, pared down to ${hits.length}`); | ||
console.log(result); | ||
} | ||
hits = hits.slice(0, num); | ||
const table = hits.map(formatResult); | ||
printTable(table, hits); | ||
if (hits.length < num) { | ||
console.log( | ||
`\nOnly ${hits.length} result${hits.length > 1 ? 's' : ''}. ` + | ||
`Try dtsearch -u to include packages without types.` | ||
); | ||
} | ||
})().catch(e => { | ||
console.error(e); | ||
}); |
@@ -50,2 +50,3 @@ export interface AlgoliaResponse { | ||
export interface Description { | ||
/** Looks like "foo <em>bar</em> baz <em>quux</em>" */ | ||
value: string; | ||
@@ -57,3 +58,3 @@ matchLevel: MatchLevel; | ||
export type MatchLevel = "full" | "none"; | ||
export type MatchLevel = "full" | "partial" | "none"; | ||
@@ -60,0 +61,0 @@ export interface Owner { |
23
TODO.md
@@ -0,5 +1,7 @@ | ||
To-do: | ||
- [x] Show GitHub repo if homepage is missing | ||
- [x] Fill width of terminal | ||
- [x] Add a binary | ||
- [ ] Highlight matching terms | ||
- [x] Highlight matching terms | ||
- [ ] Distribute it | ||
@@ -9,12 +11,19 @@ - [x] Check on the name (dtsearch is good) | ||
- [ ] Fill out the readme | ||
- [ ] Flag parsing | ||
- [ ] -y/--yarn and -n/--npm | ||
- [ ] Number of results | ||
- [ ] Allow results w/o types | ||
- [x] Flag parsing | ||
- [x] -y/--yarn and -n/--npm | ||
- [x] Number of results | ||
- [x] Allow results w/o types | ||
- [ ] Nits | ||
- [ ] Only use the flame character if the terminal supports it | ||
- [ ] Exclude "POP" column if nothing is popular | ||
- [x] Show "for untyped results, use -u" | ||
- [x] Unescape html entities (dateformat / ') | ||
- [x] Refactor columns + formatting | ||
- [ ] Only use the flame character if the terminal supports it | ||
- [ ] Add a "no results" output | ||
- [x] Add a "no results" output | ||
Punt: | ||
- [ ] Get my own Algolia API key (they only offer a 14 day free trial) | ||
- [ ] Count emojis as double-wide for width (dtsearch --num 20 solar) | ||
This seems hard, see https://github.com/xtermjs/xterm.js/pull/2568 | ||
Alternatively, uses curses to write text at a specific position. |
@@ -50,2 +50,3 @@ { | ||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ | ||
// "resolveJsonModule": true, | ||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ | ||
@@ -52,0 +53,0 @@ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ |
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
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
8724259
11
942
1
86
7
5
+ Addedchalk@^3.0.0
+ Addedcommander@^5.0.0
+ Addedansi-styles@4.3.0(transitive)
+ Addedchalk@3.0.0(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedcommander@5.1.0(transitive)
+ Addedhas-flag@4.0.0(transitive)
+ Addedsupports-color@7.2.0(transitive)