Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.jscratesrc.json
.jscrates-cache
bin
node_modules
tars
.jscratesrc.json
jscrates
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,23 @@ docker run -e HOME=/tmp -v $HOME/.jscrates/docker:/tmp/.jscrates -it --rm jscrat

## Commands

1. `jscrates download`
1. `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 <package-name> [version]
jscrates unload <packages>
```

### Example

```bash
jscrates unload physics bodmas@1.0.0
jscrates unload @jscrates/cli @jscrates/unload@1.0.0
```

2. `publish`
Expand All @@ -66,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
```

---
Expand Down
60 changes: 0 additions & 60 deletions actions/download.js

This file was deleted.

126 changes: 126 additions & 0 deletions actions/packages/unload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// @ts-check

import https from 'https'
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'

// This is the directory on the OS's temp location where
// crates will be cached to enable offline operations.
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'

// Generates directory path suffixed with the package name.
const suffixPackageName = (baseDir, packageName) => baseDir + '/' + 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)
}

/**
* 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 spinner = Spinner(`Downloading packages`)

try {
if (!store?.isOnline) {
return logError('Internet connection is required to download packages.')
}

spinner.start()

const response = await getPackages(packages)

// `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))

// 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 })
)
})
})

console.timeEnd(timerLabel)
})

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 errors occured during this operation:')
)

if (response?.errors?.length) {
logError(response?.errors?.join('\n'))
}

console.groupEnd()

console.log('\n')

spinner.succeed()
} catch (error) {
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'))
}

return logError(error)
}
}

export default unloadPackages
25 changes: 15 additions & 10 deletions jscrates.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
// @ts-check
#!/usr/bin/env node

import { readFile } from 'fs/promises'
import { Command } from 'commander'
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'
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, {
Expand All @@ -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
})
Expand All @@ -48,11 +47,17 @@ async function jscratesApp() {
.action(logout(configStore))

program
.command('download')
.description(`Download a package from official JSCrates registry`)
.argument('<package name>', 'package to download')
.argument('[version]', 'version of the package to download')
.action(downloadPackage)
.command('unload')
.description('🔽 Download package(s) from the JSCrates registry')
.argument('<packages...>', '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')
Expand Down
19 changes: 19 additions & 0 deletions lib/api/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand All @@ -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
}
)
}
Loading