From 1a049cf7d302aa49d2616e31174bbb62697ef418 Mon Sep 17 00:00:00 2001 From: Ramon Brullo Date: Sun, 14 Jan 2024 13:41:30 +0100 Subject: [PATCH] refactor: extract table formatting (#103) * refactor: extract table formatting function Extract logic for formatting packument for viewing into separate function. * refactor: extract helper type Extract type that represents the version-part of a packument. Shorter than always rewriting the properties manually. * refactor: extract module Extract table formatting logic to own module. This way other output formatting functions can be grouped together an reused. --- src/cmd-search.ts | 59 ++++++++++------------------------------ src/output-formatting.ts | 45 ++++++++++++++++++++++++++++++ src/types/packument.ts | 19 ++++++++----- 3 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 src/output-formatting.ts diff --git a/src/cmd-search.ts b/src/cmd-search.ts index b45b1137..bc51b124 100644 --- a/src/cmd-search.ts +++ b/src/cmd-search.ts @@ -1,20 +1,15 @@ import npmSearch, { Options } from "libnpmsearch"; import npmFetch from "npm-registry-fetch"; -import Table from "cli-table"; import log from "./logger"; import { is404Error, isHttpError } from "./utils/error-type-guards"; import * as os from "os"; -import assert from "assert"; -import { tryGetLatestVersion, UnityPackument } from "./types/packument"; +import { UnityPackument } from "./types/packument"; import { parseEnv } from "./utils/env"; import { DomainName } from "./types/domain-name"; import { SemanticVersion } from "./types/semantic-version"; import { CmdOptions } from "./types/options"; import { Registry } from "./registry-client"; - -type DateString = string; - -type TableRow = [DomainName, SemanticVersion, DateString, ""]; +import { formatAsTable } from "./output-formatting"; type SearchResultCode = 0 | 1; @@ -45,14 +40,14 @@ const getNpmFetchOptions = function (registry: Registry): Options { const searchEndpoint = async function ( registry: Registry, keyword: string -): Promise { +): Promise { try { // NOTE: The results of the search will be Packument objects so we can change the type const results = ( await npmSearch(keyword, getNpmFetchOptions(registry)) ); log.verbose("npmsearch", results.join(os.EOL)); - return results.map(getTableRow); + return results; } catch (err) { if (isHttpError(err) && !is404Error(err)) { log.error("", err.message); @@ -64,32 +59,28 @@ const searchEndpoint = async function ( const searchOld = async function ( registry: Registry, keyword: string -): Promise { +): Promise { // all endpoint try { const results = ( await npmFetch.json("/-/all", getNpmFetchOptions(registry)) ); - let objects: SearchedPackument[] = []; + let packuments: SearchedPackument[] = []; if (results) { if (Array.isArray(results)) { // results is an array of objects - objects = results; + packuments = results; } else { // results is an object if ("_updated" in results) delete results["_updated"]; - objects = Object.values(results); + packuments = Object.values(results); } } - log.verbose("endpoint.all", objects.join(os.EOL)); - // prepare rows - const rows = objects.map((packument) => { - return getTableRow(packument); - }); + log.verbose("endpoint.all", packuments.join(os.EOL)); // filter keyword const klc = keyword.toLowerCase(); - return rows.filter( - (row) => row.filter((x) => x.toLowerCase().includes(klc)).length > 0 + return packuments.filter((packument) => + packument.name.toLowerCase().includes(klc) ); } catch (err) { if (isHttpError(err) && !is404Error(err)) { @@ -99,26 +90,6 @@ const searchOld = async function ( } }; -const getTable = function () { - return new Table({ - head: ["Name", "Version", "Date"], - colWidths: [42, 20, 12], - }); -}; - -const getTableRow = function (packument: SearchedPackument): TableRow { - const name = packument.name; - const version = tryGetLatestVersion(packument); - let date = ""; - if (packument.time && packument.time.modified) - date = packument.time.modified.split("T")[0]!; - if (packument.date) { - date = packument.date.toISOString().slice(0, 10); - } - assert(version !== undefined); - return [name, version, date, ""]; -}; - export async function search( keyword: string, options: SearchOptions @@ -127,17 +98,15 @@ export async function search( const env = await parseEnv(options, false); if (env === null) return 1; - const table = getTable(); // search endpoint let results = await searchEndpoint(env.registry, keyword); // search old search if (results === undefined) { - results = (await searchOld(env.registry, keyword)) || []; + results = await searchOld(env.registry, keyword); } // search upstream - if (results && results.length) { - results.forEach((x) => table.push(x.slice(0, -1))); - console.log(table.toString()); + if (results !== undefined && results.length > 0) { + console.log(formatAsTable(results)); } else log.notice("", `No matches found for "${keyword}"`); return 0; } diff --git a/src/output-formatting.ts b/src/output-formatting.ts new file mode 100644 index 00000000..62939478 --- /dev/null +++ b/src/output-formatting.ts @@ -0,0 +1,45 @@ +import Table from "cli-table"; +import { + tryGetLatestVersion, + UnityPackument, + VersionedPackument, +} from "./types/packument"; +import assert from "assert"; + +/** + * A type describing the minimum required properties of a packument + * for being able to be formatted as a table. + */ +type TableablePackument = Pick & + VersionedPackument; + +/** + * Formats an array of packuments as a table. The table is returned + * as a single string (Includes line breaks). + * @param packuments The packuments + */ +export function formatAsTable(packuments: TableablePackument[]): string { + const table = new Table({ + head: ["Name", "Version", "Date"], + colWidths: [42, 20, 12], + }); + + function getTableRow( + packument: TableablePackument + ): [string, string, string] { + const name = packument.name; + const version = tryGetLatestVersion(packument); + let date = ""; + if (packument.time && packument.time.modified) + date = packument.time.modified.split("T")[0]!; + if (packument.date) { + date = packument.date.toISOString().slice(0, 10); + } + assert(version !== undefined); + return [name, version, date]; + } + + const rows = packuments.map(getTableRow); + rows.forEach((row) => table.push(row)); + return table.toString(); +} diff --git a/src/types/packument.ts b/src/types/packument.ts index 3c270df4..ad060f61 100644 --- a/src/types/packument.ts +++ b/src/types/packument.ts @@ -72,9 +72,15 @@ export type UnityPackument = { users?: Record; }; -const hasLatestDistTag = ( - packument: Partial -): packument is Partial & { +/** + * The minimum properties a Packument must have in order for it's version + * to be determined. + */ +export type VersionedPackument = Pick; + +const hasLatestDistTag = ( + packument: T +): packument is T & { "dist-tags": { latest: SemanticVersion }; } => { return packument["dist-tags"]?.["latest"] !== undefined; @@ -84,10 +90,9 @@ const hasLatestDistTag = ( * Attempt to get the latest version from a package * @param packument The package. All properties are assumed to be potentially missing */ -export const tryGetLatestVersion = function (packument: { - "dist-tags"?: { latest?: SemanticVersion }; - version?: SemanticVersion; -}): SemanticVersion | undefined { +export const tryGetLatestVersion = function ( + packument: VersionedPackument +): SemanticVersion | undefined { if (hasLatestDistTag(packument)) return packument["dist-tags"].latest; else if (packument.version) return packument.version; };