diff --git a/README.md b/README.md index 95596ca..17207c6 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,28 @@ USAGE # Commands +* [`cdt bundlephobia [FILE]`](#cdt-bundlephobia-file) * [`cdt crypto [STRING]`](#cdt-crypto-string) * [`cdt hash [STRING]`](#cdt-hash-string) * [`cdt help [COMMAND]`](#cdt-help-command) * [`cdt minify [FILE]`](#cdt-minify-file) +## `cdt bundlephobia [FILE]` + +describe the command here + +``` +USAGE + $ cdt bundlephobia [FILE] + +OPTIONS + -f, --force + -h, --help show CLI help + -n, --name=name name to print +``` + +_See code: [src/commands/bundlephobia.ts](https://github.com/codingtools/cdt/blob/v0.1.2/src/commands/bundlephobia.ts)_ + ## `cdt crypto [STRING]` Encryption and Decryption functionality for File/String diff --git a/error.json b/error.json new file mode 100644 index 0000000..8081885 --- /dev/null +++ b/error.json @@ -0,0 +1,17 @@ +{ + data: { + assets: [ [Object] ], + dependencyCount: 3, + dependencySizes: [ [Object] ], + description: 'React is a JavaScript library for building user interfaces.', + gzip: 2630, + hasJSModule: false, + hasJSNext: false, + hasSideEffects: true, + name: 'react', + repository: 'https://github.com/facebook/react.git', + scoped: false, + size: 6499, + version: '16.11.0' + } +} diff --git a/package-lock.json b/package-lock.json index 64ee1fb..5f44a59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -415,6 +415,15 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1100,6 +1109,29 @@ "is-buffer": "~2.0.3" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "foreground-child": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", @@ -1387,8 +1419,7 @@ "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" }, "is-callable": { "version": "1.1.4", @@ -2047,7 +2078,7 @@ "dependencies": { "find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "resolved": false, "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { "locate-path": "^3.0.0" @@ -2055,7 +2086,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "resolved": false, "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { "p-locate": "^3.0.0", @@ -2073,7 +2104,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "resolved": false, "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { "p-limit": "^2.0.0" @@ -2081,7 +2112,7 @@ }, "path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "resolved": false, "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" } } diff --git a/package.json b/package.json index 81cb362..e4e2cf6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@oclif/plugin-help": "^2.2.1", "@types/crypto-js": "^3.1.43", "@types/signale": "^1.2.1", + "axios": "^0.19.0", + "chalk": "^2.4.2", "crypto-js": "^3.1.9-1", "jshashes": "^1.0.7", "minify": "^4.1.3", diff --git a/src/commands/bundlephobia.ts b/src/commands/bundlephobia.ts new file mode 100644 index 0000000..5c51752 --- /dev/null +++ b/src/commands/bundlephobia.ts @@ -0,0 +1,123 @@ +import {Command, flags} from '@oclif/command' +import axios from 'axios' +import chalk from 'chalk' + +import Logger from '../utilities/logger' + +// TODO: +// ADD package.json support +// ADD VALID tests ( for now they just ignoring ) +export default class Bundlephobia extends Command { + static description = 'Find cost of adding a npm/yarn package' + + static flags = { + help: flags.help({char: 'h'}), + packages: flags.string({ + char: 'p', + description: 'packages for which cost is required, pass more than one separated by space', + multiple: true // can get multiple package names + }), + } + + static args = [{name: 'package'}] // only one can be passed club which one passed through flag and arg + + private static getPackages(flags: any, args: any) { + let packages = [] + + if (args.package) + packages.push(args.package) + if (flags.packages) + packages = packages.concat(flags.packages) // not inplace operation + return packages + } + + private static getErrorMessage(pkg: string, message: string) { + // replacing will be useful when we do not have specific version + // output will be like below +/* + ⚠ @codingtools/cdt@1.2.3 This package has not been published with this particular version. + Valid versions - `latest`, `0.1.1` and `0.1.2` +*/ + if (message.includes('This package has not been published with this particular version.')) + message = message.replace(/`|<\/code>`/g, '') + + return `${chalk.red(pkg)} ${message}` + } + + private static getSize(byteSize: number) { + if (byteSize >= 1024 * 1024) + return `${chalk.red((byteSize / (1024 * 1024)).toFixed(1) + 'MB')}` + else if (byteSize >= 1024) + return `${chalk.blue((byteSize / (1024)).toFixed(1) + 'KB')}` + else //if (byteSize < 1024) + return `${chalk.green(byteSize.toFixed(1) + 'B')}` + } + + // values needed package + async run() { + const {args, flags} = this.parse(Bundlephobia) + + args.packages = Bundlephobia.getPackages(flags, args) // get a list + + this.checkParameters(flags, args) + this.bundlePhobia(flags, args) + } + + // tslint:disable-next-line:no-unused + private checkParameters(flags: unknown, args: any) { + if (args.packages.length === 0) + Logger.error(this, 'At least one package must be passed') + } + + // tslint:disable-next-line:no-unused + private bundlePhobia(flags: any, args: any) { + Logger.progressStart(this, 'finding size...') + + let size = 0 + let gzip = 0 + let dependencyCount = 0 + let packagesResolved = 0 + + let packagesInfo: any[] = args.packages.map( + (pkg: string) => { + return { + url: `https://bundlephobia.com/api/size?package=${pkg}`, + pkg + } + } + ) + + // tslint:disable-next-line:no-unsafe-any no-unused + let x = axios.all(packagesInfo.map((packageInfo: any) => { // have to use x for removing TSLintError: promises must be handled appropriately + return axios.get(packageInfo.url).then(successResponse => { + packagesResolved ++ + size += successResponse.data.size + gzip += successResponse.data.gzip + dependencyCount += successResponse.data.dependencyCount + Logger.progressStop(this, this.getSuccessMessage(successResponse.data)) + }).catch(errorResponse => { + Logger.progressStopError(this, Bundlephobia.getErrorMessage(packageInfo.pkg, errorResponse.response.data.error.message)) + }) + // tslint:disable-next-line:no-unused + })) + .then(() => {}).catch(() => {}) + . finally(() => { + Logger.success(this, this.getFinalMessage({ + count: packagesResolved, + dependencyCount, + size, + gzip + })) + }) + + } + + private getFinalMessage(data: any) { + return `${chalk.magenta('Total')} [${chalk.cyan(data.count + ' packages resolved')}] has ${data.dependencyCount} dependencies with size of ${Bundlephobia.getSize(data.size)}(${Bundlephobia.getSize(data.gzip)} gzipped)` + } + + private getSuccessMessage(data: any) { + return `${chalk.magenta(data.name)}@${chalk.cyan(data.version)} has ${data.dependencyCount} dependencies with size of ${Bundlephobia.getSize(data.size)}(${Bundlephobia.getSize(data.gzip)} gzipped)` + } + +} diff --git a/src/commands/minify.ts b/src/commands/minify.ts index 3103f1f..1f765d6 100644 --- a/src/commands/minify.ts +++ b/src/commands/minify.ts @@ -10,7 +10,7 @@ export default class Minify extends Command { static flags = { help: flags.help({char: 'h'}), - type: flags.string({char: 't' , description: 'type of file to be minified, it will try to find type with extension'}), + type: flags.string({char: 't' , description: 'type of file to be minified, it will try to find type with extension supported: JS, HTML/HTM, CSS'}), file: flags.string({char: 'f' , description: 'file to be minified'}), } diff --git a/src/utilities/logger.ts b/src/utilities/logger.ts index 154fc47..a72d6d4 100644 --- a/src/utilities/logger.ts +++ b/src/utilities/logger.ts @@ -19,6 +19,12 @@ export default class Logger { signale.error(`${message}`) thisRef.exit(0) //added to exit command } + + // tslint:disable-next-line:no-unused + public static warn(thisRef: any, message: any) { + signale.warn(`${message}`) + } + // tslint:disable-next-line:no-unused public static progressStart(thisRef: any, message: string) { // signale.watch(`${message}`) @@ -31,6 +37,11 @@ export default class Logger { Logger.spinner.succeed(message) } + // tslint:disable-next-line:no-unused + public static progressStopError(thisRef: any, message: string) { + Logger.spinner.warn(message) + } + // public static logSuccess(thisRef: any, message: string) { // thisRef.log(` › Success: ${message}`) // } @@ -40,5 +51,4 @@ export default class Logger { // public static logError(thisRef: any, message: string) { // thisRef.error(`${message}`) // } - } diff --git a/test/commands/bundlephobia.test.ts b/test/commands/bundlephobia.test.ts new file mode 100644 index 0000000..7de47c4 --- /dev/null +++ b/test/commands/bundlephobia.test.ts @@ -0,0 +1,31 @@ +import {expect, test} from '@oclif/test' + +// TODO: add test for invalid package +// test for valid with matching +describe('bundlephobia', () => { + test + .stdout() + .command(['bundlephobia']) + .exit(0) + .it('if no package passed', ctx => { + expect(ctx.stdout).to.contain('At least one package must be passed') + }) + + test + .stdout() + .command(['bundlephobia', 'react@16.10.2']) + .it('if package passed with argument', ctx => { + setTimeout(() => // TODO: can we remove it and check if we can resolve promise here + expect(ctx.stdout).to.contain(' [react@16.10.2] minified:6.5 kB gzip:2.6 kB') + , 5000) // proving 5 seconds just to be safe + }) + + test + .stdout() + .command(['bundlephobia', '-p', 'react@16.10.2', 'react@15.10.2']) + .it('if package passed with flag', ctx => { + setTimeout(() => // TODO: can we remove it and check if we can resolve promise here + expect(ctx.stdout).to.contain(' [react@16.10.2] minified:6.5 kB gzip:2.6 kB') + , 5000) + }) +}) diff --git a/test/commands/crypto.test.ts b/test/commands/crypto.test.ts index b1e8e3a..98da100 100644 --- a/test/commands/crypto.test.ts +++ b/test/commands/crypto.test.ts @@ -19,49 +19,49 @@ describe('crypto', () => { test .stdout() - .command(['crypto', '-d', 'aes', '-s','U2FsdGVkX1/OLQ6Lp+V3O1d5SaxEf9pAf8CV7ErBC9o=', '-k', 'Secret Passphrase']) + .command(['crypto', '-d', 'aes', '-s', 'U2FsdGVkX1/OLQ6Lp+V3O1d5SaxEf9pAf8CV7ErBC9o=', '-k', 'Secret Passphrase']) .it('AES Decryption string passed as flag', ctx => { expect(ctx.stdout).to.contain('Message') }) test .stdout() - .command(['crypto', '-d', 'des', '-s','U2FsdGVkX180d+J1kUcxGL9bbBAErXAw', '-k', 'Secret Passphrase']) + .command(['crypto', '-d', 'des', '-s', 'U2FsdGVkX180d+J1kUcxGL9bbBAErXAw', '-k', 'Secret Passphrase']) .it('DES Decryption', ctx => { expect(ctx.stdout).to.contain('Message') }) test .stdout() - .command(['crypto', '-d', 'des', '-s','U2FsdGVkX186YglxZ7yF7aqTjFQA3Yzs', '-k', 'Secret Passphrase','-m','ECB']) + .command(['crypto', '-d', 'des', '-s', 'U2FsdGVkX186YglxZ7yF7aqTjFQA3Yzs', '-k', 'Secret Passphrase', '-m', 'ECB']) .it('DES Decryption with Mode ECB', ctx => { expect(ctx.stdout).to.contain('Message') }) test .stdout() - .command(['crypto', '-d', '3des', '-s','U2FsdGVkX1+2jkjCxuWwL8uMgdu6SXJc', '-k', 'Secret Passphrase']) + .command(['crypto', '-d', '3des', '-s', 'U2FsdGVkX1+2jkjCxuWwL8uMgdu6SXJc', '-k', 'Secret Passphrase']) .it('3DES Decryption', ctx => { expect(ctx.stdout).to.contain('Message') }) test .stdout() - .command(['crypto', '-d', 'RABBIT', '-s','U2FsdGVkX185oOsUqvpF+7x0zPUxNJw=', '-k', 'Secret Passphrase']) + .command(['crypto', '-d', 'RABBIT', '-s', 'U2FsdGVkX185oOsUqvpF+7x0zPUxNJw=', '-k', 'Secret Passphrase']) .it('RABBIT Decryption', ctx => { expect(ctx.stdout).to.contain('Message') }) test .stdout() - .command(['crypto', '-d', 'RC4', '-s','U2FsdGVkX1+/oErpaqQQk1Fj2eXwL1o=', '-k', 'Secret Passphrase']) + .command(['crypto', '-d', 'RC4', '-s', 'U2FsdGVkX1+/oErpaqQQk1Fj2eXwL1o=', '-k', 'Secret Passphrase']) .it('RC4 Decryption', ctx => { expect(ctx.stdout).to.contain('Message') }) test .stdout() - .command(['crypto', '-d', 'RC4DROP', '-s','U2FsdGVkX18+D1WNQ64XzaCwkUM6moE=', '-k', 'Secret Passphrase']) + .command(['crypto', '-d', 'RC4DROP', '-s', 'U2FsdGVkX18+D1WNQ64XzaCwkUM6moE=', '-k', 'Secret Passphrase']) .it('RC4Drop Decryption', ctx => { expect(ctx.stdout).to.contain('Message') }) @@ -92,7 +92,7 @@ describe('crypto', () => { test .stdout() - .command(['crypto', '-e', 'aes','-d', 'aes', '-s', 'Message', '-k', 'Secret Passphrase']) + .command(['crypto', '-e', 'aes', '-d', 'aes', '-s', 'Message', '-k', 'Secret Passphrase']) .exit(0) .it('Both encryption and decryption methods passed', ctx => { expect(ctx.stdout).to.contain('Both encryption and decryption methods passed') @@ -106,5 +106,4 @@ describe('crypto', () => { expect(ctx.stdout).to.contain('Neither encryption or decryption methods passed') }) - })