From f535700114a6c90974d693c5994fae0e4e2f6de2 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 08:35:10 +0000 Subject: [PATCH 01/12] :sparkles: Upsert directory util. --- utils/upsert-dir.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 utils/upsert-dir.js diff --git a/utils/upsert-dir.js b/utils/upsert-dir.js new file mode 100644 index 0000000..a244b8d --- /dev/null +++ b/utils/upsert-dir.js @@ -0,0 +1,23 @@ +import { existsSync, mkdirSync } from 'fs' +import { logError } from './loggers.js' + +/** + * Creates a directory if it does not exist. + * + * @param {string} pathLike + * @param {import('fs').MakeDirectoryOptions} opts + */ +const upsertDir = (pathLike, opts = {}) => { + try { + if (!existsSync(pathLike)) { + mkdirSync(pathLike, { recursive: true, ...opts }) + } + + return pathLike + } catch (error) { + logError(error) + return process.exit(1) + } +} + +export default upsertDir From a47be2551899b5737711b11e68f28ed2a29edabb Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 08:35:38 +0000 Subject: [PATCH 02/12] :sparkles: New unload module to download packages. --- actions/download.js | 60 ------------------------ actions/packages/unload.js | 96 ++++++++++++++++++++++++++++++++++++++ jscrates.js | 18 ++++--- 3 files changed, 108 insertions(+), 66 deletions(-) delete mode 100644 actions/download.js create mode 100644 actions/packages/unload.js diff --git a/actions/download.js b/actions/download.js deleted file mode 100644 index fad15f9..0000000 --- a/actions/download.js +++ /dev/null @@ -1,60 +0,0 @@ -// @ts-check - -import { get } from 'https' -import { createWriteStream } from 'fs' -import Spinner from 'mico-spinner' -import semver from 'semver' -import api from '../lib/api/index.js' -import { logError } from '../utils/loggers.js' - -/** - * Action to download packages from repository. - * - * @param {string} name - * @param {string | semver.SemVer} version - */ -async function downloadPackage(name, version) { - // Initialize a spinner instance - const downloadingSpinner = Spinner(`Downloading ${name}`) - - try { - if (version) { - // Validating version against Semantic Versioning rules - if (!semver.valid(version)) { - throw `Invalid version` - } - } - - // Initiate the spinner - downloadingSpinner.start() - - const endpoint = ['pkg', name, version].filter(Boolean).join('/') - - const res = (await api.get(endpoint)).data - - // Create a write file stream to download the tar file - const file = createWriteStream( - `./tars/${res?.dist?.tarball?.substring( - res?.dist?.tarball?.lastIndexOf('/') + 1 - )}` - ) - - // Initiate the HTTP request to download package archive - // (.targz) files from the cloud repository - get(res?.dist?.tarball, function (response) { - response.pipe(file) - }) - - downloadingSpinner.succeed() - } catch (error) { - downloadingSpinner.fail() - - if (error?.isAxiosError) { - return logError(error?.response?.data?.message) - } - - logError(error) - } -} - -export default downloadPackage diff --git a/actions/packages/unload.js b/actions/packages/unload.js new file mode 100644 index 0000000..cd037fe --- /dev/null +++ b/actions/packages/unload.js @@ -0,0 +1,96 @@ +// @ts-check + +import https from 'https' +import { createWriteStream } from 'fs' +import Spinner from 'mico-spinner' +import tempDirectory from 'temp-dir' +import tar from 'tar' +import { getPackages } from '../../lib/api/actions.js' +import { logError } from '../../utils/loggers.js' +import upsertDir from '../../utils/upsert-dir.js' +import { createReadStream } from 'fs' + +// This is the directory on the OS's temp location where +// crates will be cached to enable offline operations. +const cacheDir = '.jscrates-cache' +// Directory in the current project where packages will +// be installed (unzipped). Consider this as `node_modules` +// for JSCrates +const installDir = './jscrates' + +const generateCacheDirPath = (packageName = '') => { + return `${tempDirectory}/${cacheDir}/${packageName}` +} + +const generateCratesInstallDir = (packageName = '') => { + return `${installDir}/${packageName}` +} + +const getTarballName = (tarballURL) => { + return tarballURL.substring(tarballURL.lastIndexOf('/') + 1) +} + +/** + * Action to download packages from repository. + * + * @param {string[]} packages + */ +async function unloadPackages(packages, ...args) { + // Since we are accepting variadic arguments, other arguments can only + // be accessing by spreading them. + const store = args[1].__store + const downloadingSpinner = Spinner(`Downloading`) + + try { + if (!store?.isOnline) { + return logError('Internet connection is required to download packages.') + } + + downloadingSpinner.start() + + const response = await getPackages(packages) + + if (response?.errors?.length) { + logError(response?.errors?.join('\n')) + } + + response?.data?.map((res) => { + const tarballFileName = getTarballName(res?.dist?.tarball) + const cacheLocation = upsertDir(generateCacheDirPath(res?.name)) + const installLocation = upsertDir(generateCratesInstallDir(res?.name)) + + // Create a write file stream to download the tar file + const file = createWriteStream(`${cacheLocation}/${tarballFileName}`) + + // Initiate the HTTP request to download package archive + // (.tgz) files from the cloud repository + https.get(res?.dist?.tarball, function (response) { + response + .on('error', function () { + throw 'Something went wrong downloading the package.' + }) + .on('data', function (data) { + file.write(data) + }) + .on('end', function () { + file.end() + createReadStream(`${cacheLocation}/${tarballFileName}`).pipe( + tar.x({ cwd: installLocation }) + ) + }) + }) + }) + + downloadingSpinner.succeed() + } catch (error) { + downloadingSpinner.fail() + + if (Array.isArray(error)) { + return logError(error.join('\n')) + } + + return logError(error) + } +} + +export default unloadPackages diff --git a/jscrates.js b/jscrates.js index 1081e52..547d296 100644 --- a/jscrates.js +++ b/jscrates.js @@ -6,7 +6,7 @@ import Configstore from 'configstore' import checkOnlineStatus from 'is-online' import { CONFIG_FILE } from './lib/constants.js' -import downloadPackage from './actions/download.js' +import unloadPackages from './actions/packages/unload.js' import publishPackage from './actions/publish.js' import login from './actions/auth/login.js' import register from './actions/auth/register.js' @@ -48,11 +48,17 @@ async function jscratesApp() { .action(logout(configStore)) program - .command('download') - .description(`Download a package from official JSCrates registry`) - .argument('', 'package to download') - .argument('[version]', 'version of the package to download') - .action(downloadPackage) + .command('unload') + .description('🔽 Download package(s) from the JSCrates registry') + .argument('', 'List of packages delimited by a space') + .action(unloadPackages) + .addHelpText( + 'after', + '\nExamples:\n jscrates unload bodmas' + + '\n jscrates unload physics-formulae@1.0.0' + + '\n jscrates unload binary-search merge-sort bodmas@1.0.0' + ) + .aliases(['u']) program .command('publish') From ea33cee9ea1fdb1b496ce484443418d6f9255c89 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 08:36:00 +0000 Subject: [PATCH 03/12] :art: Option to pass custom error handler. --- lib/api/actions.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/api/actions.js b/lib/api/actions.js index df8787b..891b430 100644 --- a/lib/api/actions.js +++ b/lib/api/actions.js @@ -7,6 +7,14 @@ const apiErrorHandler = (error) => { } } +const apiAction = async (bodyFn, errorHandlerFn = undefined) => { + try { + return await bodyFn() + } catch (error) { + return errorHandlerFn ? errorHandlerFn(error) : apiErrorHandler(error) + } +} + export const registerUser = async ({ email, password }) => { try { const { data: apiResponse } = await api.post('/auth/register', { @@ -31,3 +39,14 @@ export const loginUser = async ({ email, password }) => { return apiErrorHandler(error) } } + +export const getPackages = async (packages) => { + return await apiAction( + async () => { + return (await api.put('/pkg', { packages })).data + }, + (error) => { + throw error?.response?.data?.errors + } + ) +} From eeaddb65c60d5b107a7134d512ada880be0a8246 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 08:36:23 +0000 Subject: [PATCH 04/12] :wrench: Add JSCrates dirs. --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dba131f..c78bbb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +.jscratesrc.json +.jscrates-cache bin node_modules tars -.jscratesrc.json +jscrates From ecae92980cf2742e50b1189f55d7b3399013b7b6 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 08:36:34 +0000 Subject: [PATCH 05/12] :bookmark: v2.6.0 --- package-lock.json | 46 ++-------------------------------------------- package.json | 3 +-- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 987121c..9e8e55a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jscrates/cli", - "version": "2.5.1", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@jscrates/cli", - "version": "2.5.1", + "version": "2.6.0", "dependencies": { "axios": "^0.24.0", "chalk": "^4.1.2", @@ -18,7 +18,6 @@ "lodash.kebabcase": "^4.1.1", "mico-spinner": "^1.4.0", "prompt": "^1.2.0", - "semver": "^7.3.5", "tar": "^6.1.11", "temp-dir": "^2.0.0" }, @@ -750,17 +749,6 @@ "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1233,20 +1221,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/signal-exit": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", @@ -1952,14 +1926,6 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2289,14 +2255,6 @@ "queue-microtask": "^1.2.2" } }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, "signal-exit": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", diff --git a/package.json b/package.json index 81320a8..b007bcf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jscrates/cli", - "version": "2.5.1", + "version": "2.6.0", "description": "Official CLI client for JSCrates.", "main": "jscrates.js", "author": "Team JSCrates", @@ -24,7 +24,6 @@ "lodash.kebabcase": "^4.1.1", "mico-spinner": "^1.4.0", "prompt": "^1.2.0", - "semver": "^7.3.5", "tar": "^6.1.11", "temp-dir": "^2.0.0" }, From 96b1eebf34a080b10d22811faf95830f1fb6af11 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:06:57 +0000 Subject: [PATCH 06/12] :art: Add timers to show download time. --- actions/packages/unload.js | 62 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/actions/packages/unload.js b/actions/packages/unload.js index cd037fe..330f6e0 100644 --- a/actions/packages/unload.js +++ b/actions/packages/unload.js @@ -1,31 +1,35 @@ // @ts-check import https from 'https' -import { createWriteStream } from 'fs' +import { createWriteStream, createReadStream } from 'fs' import Spinner from 'mico-spinner' import tempDirectory from 'temp-dir' +import chalk from 'chalk' import tar from 'tar' import { getPackages } from '../../lib/api/actions.js' import { logError } from '../../utils/loggers.js' import upsertDir from '../../utils/upsert-dir.js' -import { createReadStream } from 'fs' // This is the directory on the OS's temp location where // crates will be cached to enable offline operations. -const cacheDir = '.jscrates-cache' +const cacheDir = tempDirectory + '/.jscrates-cache' // Directory in the current project where packages will // be installed (unzipped). Consider this as `node_modules` // for JSCrates const installDir = './jscrates' -const generateCacheDirPath = (packageName = '') => { - return `${tempDirectory}/${cacheDir}/${packageName}` -} +// Generates directory path suffixed with the package name. +const suffixPackageName = (baseDir, packageName) => baseDir + '/' + packageName -const generateCratesInstallDir = (packageName = '') => { - return `${installDir}/${packageName}` -} +// Used for storing packages in cache. +const generateCacheDirPath = (packageName = '') => + suffixPackageName(cacheDir, packageName) + +// Used for unzipping packages in the CWD. +const generateCratesInstallDir = (packageName = '') => + suffixPackageName(installDir, packageName) +// Extracts tarball name from the provided URL. const getTarballName = (tarballURL) => { return tarballURL.substring(tarballURL.lastIndexOf('/') + 1) } @@ -33,28 +37,33 @@ const getTarballName = (tarballURL) => { /** * Action to download packages from repository. * + * TODO: Implement logic to check packages in cache before + * requesting the API. + * * @param {string[]} packages */ async function unloadPackages(packages, ...args) { // Since we are accepting variadic arguments, other arguments can only // be accessing by spreading them. const store = args[1].__store - const downloadingSpinner = Spinner(`Downloading`) + const spinner = Spinner(`Downloading packages`) try { if (!store?.isOnline) { return logError('Internet connection is required to download packages.') } - downloadingSpinner.start() + spinner.start() const response = await getPackages(packages) - if (response?.errors?.length) { - logError(response?.errors?.join('\n')) - } - + // `data` contains all the resolved packages metadata. + // 1. Download the tarball to cache directory. + // 2. Read the cached tarball & install in CWD. response?.data?.map((res) => { + const timerLabel = chalk.green(`Installed \`${res.name}\` in`) + console.time(timerLabel) + const tarballFileName = getTarballName(res?.dist?.tarball) const cacheLocation = upsertDir(generateCacheDirPath(res?.name)) const installLocation = upsertDir(generateCratesInstallDir(res?.name)) @@ -79,12 +88,31 @@ async function unloadPackages(packages, ...args) { ) }) }) + + console.timeEnd(timerLabel) }) - downloadingSpinner.succeed() + console.log('\n') + + // When only a few packages are resolved, the errors array + // contains list of packages that were not resolved. + // We shall display these for better UX. + console.group(chalk.yellow('The following packages could not be resolved:')) + + if (response?.errors?.length) { + logError(response?.errors?.join('\n')) + } + + console.groupEnd() + + console.log('\n') + + spinner.succeed() } catch (error) { - downloadingSpinner.fail() + spinner.fail() + // When all the requested packages could not be resolved + // API responds with status 404 and list of errors. if (Array.isArray(error)) { return logError(error.join('\n')) } From ea271953315ef643e5a0a7df2fe98040d4e60008 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:15:17 +0000 Subject: [PATCH 07/12] :art: Modify error message. --- actions/packages/unload.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/actions/packages/unload.js b/actions/packages/unload.js index 330f6e0..e5600b8 100644 --- a/actions/packages/unload.js +++ b/actions/packages/unload.js @@ -97,7 +97,9 @@ async function unloadPackages(packages, ...args) { // When only a few packages are resolved, the errors array // contains list of packages that were not resolved. // We shall display these for better UX. - console.group(chalk.yellow('The following packages could not be resolved:')) + console.group( + chalk.yellow('The following errors occured during this operation:') + ) if (response?.errors?.length) { logError(response?.errors?.join('\n')) From 03d4c1e81e4179431928b0765af38e0593f1eacd Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:18:08 +0000 Subject: [PATCH 08/12] :memo: Update unload documentation. --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e5e1b9..f9d3168 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,23 @@ docker run -e HOME=/tmp -v $HOME/.jscrates/docker:/tmp/.jscrates -it --rm jscrat ## Commands -1. `jscrates download` +1. `jscrates unload` #### Description -Downloads the specified package from the official repository of JSCrates. +Downloads the specified package(s) from official repository of JSCrates. #### Usage ```bash -$ jscrates download [version] +$ jscrates unload +``` + +### Example + +```bash +jscrates unload physics bodmas@1.0.0 +jscrates unload @jscrates/cli @jscrates/unload@1.0.0 ``` 2. `publish` From 9620cf803db1176e036f0bf784a31a4f4c223d8a Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:18:46 +0000 Subject: [PATCH 09/12] :memo: Remove $ --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9d3168..a6a1cb8 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Downloads the specified package(s) from official repository of JSCrates. #### Usage ```bash -$ jscrates unload +jscrates unload ``` ### Example @@ -73,7 +73,7 @@ Have a package that you want to share with the world? This command will help you This command requires you to set or open the terminal in your project directory. ```bash -$ jscrates publish +jscrates publish ``` --- From e50ff1a54886f19b65d6d6991f5d06e447f5f48f Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:19:39 +0000 Subject: [PATCH 10/12] :memo: Normalize commands. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6a1cb8..54a2bfd 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ docker run -e HOME=/tmp -v $HOME/.jscrates/docker:/tmp/.jscrates -it --rm jscrat ## Commands -1. `jscrates unload` +1. `unload` #### Description From 5b18925c4506f92ebae1c9dff8fc3ca2300ec6d5 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:39:43 +0000 Subject: [PATCH 11/12] :wrench: Add shebang. --- jscrates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jscrates.js b/jscrates.js index 547d296..abb21d2 100644 --- a/jscrates.js +++ b/jscrates.js @@ -1,4 +1,4 @@ -// @ts-check +#!/usr/bin/env node import { readFile } from 'fs/promises' import { Command } from 'commander' From 7a48e3764bdf92695a0169a578eedf77643c7280 Mon Sep 17 00:00:00 2001 From: "D. Kasi Pavan Kumar" <44864604+kasipavankumar@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:52:38 +0000 Subject: [PATCH 12/12] =?UTF-8?q?=E2=9B=91=20Remove=20reading=20verison.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jscrates.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jscrates.js b/jscrates.js index abb21d2..8aa1061 100644 --- a/jscrates.js +++ b/jscrates.js @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { readFile } from 'fs/promises' import { Command } from 'commander' import Configstore from 'configstore' import checkOnlineStatus from 'is-online' @@ -13,7 +12,6 @@ import register from './actions/auth/register.js' import logout from './actions/auth/logout.js' async function jscratesApp() { - const packageJSON = JSON.parse(await readFile('./package.json', 'utf-8')) const isOnline = await checkOnlineStatus() const program = new Command() const configStore = new Configstore(CONFIG_FILE, { @@ -27,7 +25,8 @@ async function jscratesApp() { program .name('jscrates') .description(`Welcome to JSCrates 📦, yet another package manager for Node`) - .version(packageJSON.version, '-v, --version', 'display current version') + // TODO: Find a way to read version build time. + .version('0.0.0-alpha', '-v, --version', 'display current version') .hook('preAction', (_, actionCommand) => { actionCommand['__store'] = appState })