From 0421305a2ede8b7f03b0fdd93eb0070b4a8425aa Mon Sep 17 00:00:00 2001 From: Miroslav Jancarik Date: Sat, 28 May 2022 15:42:23 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20package=20info=20t?= =?UTF-8?q?o=20reporting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 🧨 Change option --json to --pretty. --json option returns JSON.stringify result. --- package.json | 1 + src/__tests__/createResultSpec.mjs | 50 ++++++++++--------- src/__tests__/formatSpec.mjs | 54 +++++++++++++++++++- src/constant.mjs | 11 +++++ src/createResult.mjs | 79 ++++++++++++++++++++++++------ src/format.mjs | 40 +++++++++++++++ src/index.mjs | 36 +++++++++++--- src/reporter.mjs | 52 ++++++++++++++++---- 8 files changed, 266 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 70c096c..9a576e7 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "easy-uid": "^2.0.2", "execa": "^6.1.0", "fs-extra": "^10.1.0", + "got": "^12.1.0", "mini-css-extract-plugin": "^2.6.0", "ora": "^6.1.0", "postcss-loader": "^6.2.1", diff --git a/src/__tests__/createResultSpec.mjs b/src/__tests__/createResultSpec.mjs index 9f626ae..a16477b 100644 --- a/src/__tests__/createResultSpec.mjs +++ b/src/__tests__/createResultSpec.mjs @@ -16,35 +16,37 @@ beforeAll(async () => { describe('createResult', () => { it('should generate result object', async () => { - const result = await module.createResult({ TMP: 'folder' }); + const result = await module.createBundleResult({ TMP: 'folder' }); expect(result).toMatchInlineSnapshot(` Object { - "brotli": Object { - "size": 33, - "speed": Object { - "2g": 2.75, - "3g": 0.66, - "4g": 0.037714285714285714, - "5g": 0.014666666666666666, + "bundle": Object { + "brotli": Object { + "size": 33, + "speed": Object { + "2g": 2.75, + "3g": 0.66, + "4g": 0.037714285714285714, + "5g": 0.014666666666666666, + }, }, - }, - "gzip": Object { - "size": 122, - "speed": Object { - "2g": 10.166666666666666, - "3g": 2.44, - "4g": 0.13942857142857143, - "5g": 0.05422222222222222, + "gzip": Object { + "size": 122, + "speed": Object { + "2g": 10.166666666666666, + "3g": 2.44, + "4g": 0.13942857142857143, + "5g": 0.05422222222222222, + }, }, - }, - "minify": Object { - "size": 26000, - "speed": Object { - "2g": 2166.6666666666665, - "3g": 520, - "4g": 29.714285714285715, - "5g": 11.555555555555555, + "minify": Object { + "size": 26000, + "speed": Object { + "2g": 2166.6666666666665, + "3g": 520, + "4g": 29.714285714285715, + "5g": 11.555555555555555, + }, }, }, } diff --git a/src/__tests__/formatSpec.mjs b/src/__tests__/formatSpec.mjs index 788f084..2bb2766 100644 --- a/src/__tests__/formatSpec.mjs +++ b/src/__tests__/formatSpec.mjs @@ -1,5 +1,5 @@ import '@jest/globals'; -import { formatSize, formatTime } from '../format'; +import { formatSize, formatTime, formatNumber, formatDate } from '../format'; describe('format', () => { describe('formatTime', () => { @@ -24,7 +24,7 @@ describe('format', () => { }); }); - describe('formatsize', () => { + describe('formatSize', () => { it('should return 0 Byte', () => { expect(formatSize(0)).toEqual('0 Byte'); }); @@ -45,4 +45,54 @@ describe('format', () => { expect(formatSize(1024 * 1024)).toEqual('1 MB'); }); }); + + describe('formatNumber', () => { + it('should return 0', () => { + expect(formatNumber(0)).toEqual('0'); + }); + + it('should return 500', () => { + expect(formatNumber(500)).toEqual('500'); + }); + + it('should return 1K', () => { + expect(formatNumber(1000)).toEqual('1K'); + }); + + it('should return 1.5 M', () => { + expect(formatNumber(1500000)).toEqual('1.5M'); + }); + }); + + describe('formatDate', () => { + it('should return 2022-04-01', () => { + expect(formatDate(new Date('2022-04-01T09:29:40.128Z'))).toEqual( + '2022-4-1' + ); + }); + + it('should return 0 seconds ago', () => { + expect(formatDate(new Date())).toEqual('0 seconds ago'); + }); + + it('should return 5 seconds ago', () => { + expect(formatDate(new Date(Date.now() - 5000))).toEqual('5 seconds ago'); + }); + + it('should return 1 minutes ago', () => { + expect(formatDate(new Date(Date.now() - 60000))).toEqual('1 minutes ago'); + }); + + it('should return 1 hours ago', () => { + expect(formatDate(new Date(Date.now() - 60 * 60000))).toEqual( + '1 hours ago' + ); + }); + + it('should return 1 days ago', () => { + expect(formatDate(new Date(Date.now() - 24 * 60 * 60000))).toEqual( + '1 days ago' + ); + }); + }); }); diff --git a/src/constant.mjs b/src/constant.mjs index f73dc7e..1c74437 100644 --- a/src/constant.mjs +++ b/src/constant.mjs @@ -4,3 +4,14 @@ export const SPEED = { ['4g']: 875, ['5g']: 2250, }; + +export const API = { + NPM_DOWNLOADS: 'https://api.npmjs.org/downloads/point', + REGISTRY_PACKAGE_INFO: 'https://registry.npmjs.org', +}; + +export const PERIOD = { + LAST_DAY: 'last-day', + LAST_WEEK: 'last-week', + LAST_MONTH: 'last-month', +}; diff --git a/src/createResult.mjs b/src/createResult.mjs index 8d19100..911b2dd 100644 --- a/src/createResult.mjs +++ b/src/createResult.mjs @@ -2,10 +2,11 @@ import zlib from 'node:zlib'; import { promisify } from 'node:util'; import fs from 'fs-extra'; +import got from 'got'; -import { SPEED } from './constant.mjs'; +import { SPEED, API, PERIOD } from './constant.mjs'; -export async function createResult({ TMP }) { +export async function createBundleResult({ TMP }) { const file = await fs.readFile(`${TMP}/blank.bundle.js`); const gzipFile = await promisify(zlib.gzip)(file, { level: 9 }); const brotliFile = await promisify(zlib.brotliCompress)(file, { @@ -14,25 +15,73 @@ export async function createResult({ TMP }) { }); const result = { - minify: { - size: file.toString().length, - speed: {}, - }, - gzip: { - size: gzipFile.toString().length, - speed: {}, - }, - brotli: { - size: brotliFile.toString().length, - speed: {}, + bundle: { + minify: { + size: file.toString().length, + speed: {}, + }, + gzip: { + size: gzipFile.toString().length, + speed: {}, + }, + brotli: { + size: brotliFile.toString().length, + speed: {}, + }, }, }; - Object.keys(result).forEach((type) => { + Object.keys(result.bundle).forEach((type) => { Object.keys(SPEED).forEach((wifi) => { - result[type].speed[wifi] = result[type].size / SPEED[wifi]; + result.bundle[type].speed[wifi] = result.bundle[type].size / SPEED[wifi]; }); }); return result; } + +export async function createDownloadsResult({ packages, result }) { + for (let packageName of packages) { + const [day, week, month] = await Promise.all( + Object.keys(PERIOD).map((key) => { + return got + .get(`${API.NPM_DOWNLOADS}/${PERIOD[key]}/${packageName}`) + .json(); + }) + ); + + result[packageName] = { + ...result[packageName], + downloads: { + day: day.downloads, + week: week.downloads, + month: month.downloads, + }, + }; + } + + return result; +} + +export async function createPackageInfo({ packages, result, options }) { + for (let packageName of packages) { + const packageInfo = await got + .get(`${options.registry ?? API.REGISTRY_PACKAGE_INFO}/${packageName}`) + .json(); + + result[packageName] = { + ...result[packageName], + info: { + license: packageInfo.license, + created: new Date(packageInfo.time.created), + updated: new Date(packageInfo.time.modified), + version: packageInfo['dist-tags'].latest, + unpackedSize: + packageInfo.versions[packageInfo['dist-tags'].latest]?.dist + ?.unpackedSize, + }, + }; + } + + return result; +} diff --git a/src/format.mjs b/src/format.mjs index 5007763..a12f856 100644 --- a/src/format.mjs +++ b/src/format.mjs @@ -1,3 +1,43 @@ +export function formatNumber(number) { + const sizes = ['', 'K', 'M', 'G', 'T']; + + if (number == 0) { + return '0'; + } + + const index = parseInt(Math.floor(Math.log(number) / Math.log(1000))); + + return ( + Math.round((number / Math.pow(1000, index) + Number.EPSILON) * 100) / 100 + + sizes[index] + ); +} + +export function formatDate(date) { + const seconds = Math.floor((new Date() - date) / 1000); + + let interval = seconds / 2592000; + + if (interval >= 1) { + return `${date.getUTCFullYear()}-${ + date.getUTCMonth() + 1 + }-${date.getUTCDate()}`; + } + interval = seconds / 86400; + if (interval >= 1) { + return Math.floor(interval) + ' days ago'; + } + interval = seconds / 3600; + if (interval >= 1) { + return Math.floor(interval) + ' hours ago'; + } + interval = seconds / 60; + if (interval >= 1) { + return Math.floor(interval) + ' minutes ago'; + } + return Math.floor(seconds) + ' seconds ago'; +} + export function formatSize(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; diff --git a/src/index.mjs b/src/index.mjs index fc5db5c..cc5b162 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,17 +1,26 @@ import path from 'node:path'; +import util from 'node:util'; import ora from 'ora'; import fs from 'fs-extra'; import { Command } from 'commander'; -import { renderSizeTable, renderTimeTable } from './reporter.mjs'; +import { + renderPackageInfo, + renderSizeTable, + renderTimeTable, +} from './reporter.mjs'; import { bundle, getExternals } from './webpack.mjs'; import { createEmptyModule, createIndex, installDependencies, } from './createModule.mjs'; -import { createResult } from './createResult.mjs'; +import { + createBundleResult, + createDownloadsResult, + createPackageInfo, +} from './createResult.mjs'; const dir = path.resolve('./'); let packageJson = null; @@ -33,6 +42,7 @@ program .option('--external', 'external dependencies to webpack config') .option('--explain', 'log webpack stats') .option('--json', 'log only json format') + .option('--pretty', 'log only pretty print object') .option( '--bundle', 'bundle all dependencies with external dependencies and tree shaking' @@ -66,15 +76,29 @@ program.parse(process.argv); ); } - const result = await createResult({ TMP }); + let result = await createBundleResult({ TMP }); await fs.remove(TMP); - await (!options.json && renderSizeTable({ result, packages })); - await (!options.json && renderTimeTable({ result })); + result = await createDownloadsResult({ result, packages }); + result = await createPackageInfo({ result, packages, options }); + + await (!options.json && + !options.pretty && + renderSizeTable({ result, packages })); + await (!options.json && !options.pretty && renderTimeTable({ result })); + await (!options.json && + !options.pretty && + renderPackageInfo({ result, packages })); if (options.json) { - console.log(result); + console.log(JSON.stringify(result)); + } + + if (options.pretty) { + console.log( + util.inspect(result, { showHidden: false, depth: null, colors: true }) + ); } } catch (error) { console.error(error); diff --git a/src/reporter.mjs b/src/reporter.mjs index 1d58085..dfaa080 100644 --- a/src/reporter.mjs +++ b/src/reporter.mjs @@ -1,6 +1,6 @@ import Table from 'cli-table3'; -import { formatSize, formatTime } from './format.mjs'; +import { formatSize, formatTime, formatNumber, formatDate } from './format.mjs'; import { SPEED } from './constant.mjs'; export async function renderSizeTable({ result, packages }) { @@ -11,9 +11,9 @@ export async function renderSizeTable({ result, packages }) { sizeTable.push([ packages.join(), - formatSize(result.minify.size), - formatSize(result.gzip.size), - formatSize(result.brotli.size), + formatSize(result.bundle.minify.size), + formatSize(result.bundle.gzip.size), + formatSize(result.bundle.brotli.size), ]); console.log(sizeTable.toString()); @@ -25,17 +25,49 @@ export async function renderTimeTable({ result }) { colWidths: [20, 20, 20, 20, 20], }); - Object.keys(result).forEach((type) => { + Object.keys(result.bundle).forEach((type) => { Object.keys(SPEED).forEach((wifi) => { - result[type].speed[wifi] = result[type].size / SPEED[wifi]; + result.bundle[type].speed[wifi] = result.bundle[type].size / SPEED[wifi]; }); timeTable.push([ type, - formatTime(result[type].speed['2g']), - formatTime(result[type].speed['3g']), - formatTime(result[type].speed['4g']), - formatTime(result[type].speed['5g']), + formatTime(result.bundle[type].speed['2g']), + formatTime(result.bundle[type].speed['3g']), + formatTime(result.bundle[type].speed['4g']), + formatTime(result.bundle[type].speed['5g']), + ]); + }); + + console.log(timeTable.toString()); +} + +export async function renderPackageInfo({ result, packages }) { + const timeTable = new Table({ + head: [ + 'package', + 'downloads day / week / month', + 'version', + 'license', + 'created', + 'updated', + 'unpacked size', + ], + colWidths: [20, 30, 10, 10, 15, 15, 15], + }); + + packages.forEach((packageName) => { + const packageData = result[packageName]; + timeTable.push([ + packageName, + `${formatNumber(packageData.downloads.day)} / ${formatNumber( + packageData.downloads.week + )} / ${formatNumber(packageData.downloads.month)}`, + packageData.info.version, + packageData.info.license, + formatDate(packageData.info.created), + formatDate(packageData.info.updated), + formatSize(packageData.info.unpackedSize), ]); });