diff --git a/README.md b/README.md index 0cb83f5..ff74826 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,9 @@ interface GrabOptions { // archive name without the platform suffix; if not specified, it will be // inferred from the first archive asset found for the current platform platformSuffixes?: PlatformSuffixes + // directory to write the archive or executable to; if not specified, + // files will be written to the current directory + targetDirectory?: string // unpack the executable and remove the archive unpackExecutable?: boolean } diff --git a/bin/grab-github-release.js b/bin/grab-github-release.js index 8c4e35f..a198954 100755 --- a/bin/grab-github-release.js +++ b/bin/grab-github-release.js @@ -12,6 +12,7 @@ Options: -i|--version-spec semantic version specifier or "latest" -n|--name archive name without the platform suffix -p|--platform-suffixes unpack the executable and remove the archive + -t|--target-dir directory to write the output files to -e|--unpack-exe unpack the executable and remove the archive -v|--verbose prints extra information on the console -V|--version print version number and exit @@ -31,7 +32,7 @@ function fail(message) { } const { argv } = process -let repository, version, name, platformSuffixes, unpackExecutable, verbose +let repository, version, name, platformSuffixes, targetDirectory, unpackExecutable, verbose for (let i = 2, l = argv.length; i < l; ++i) { const arg = argv[i] @@ -58,6 +59,9 @@ for (let i = 2, l = argv.length; i < l; ++i) { platformSuffixes[key.trim()] = val.trim() } return + case 't': case 'target-dir': + targetDirectory = match[4] || argv[++i] + return case 'e': case 'unpack-exe': unpackExecutable = flag return diff --git a/package.json b/package.json index 323bc55..b44d73f 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "check-coverage": "true", "reporter": [ "lcov", - "text-summary" + "text" ], "branches": 100, "functions": 100, diff --git a/rollup.config.js b/rollup.config.js index c076fdb..9136b36 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,7 +14,7 @@ export default [ sourcemap: true } ], - external: ['debug', 'fs', 'os', 'semver', 'stream', 'yauzl'], + external: ['debug', 'fs', 'os', 'path', 'semver', 'stream', 'yauzl'], plugins: [cleanup()] } ] diff --git a/src/index.d.ts b/src/index.d.ts index 7f253f2..a05bf23 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -23,6 +23,11 @@ interface GrabOptions { * inferred from the first archive asset found for the current platform */ platformSuffixes?: PlatformSuffixes + /** + * directory to write the archive or executable to; if not specified, + * files will be written to the current directory + */ + targetDirectory?: string /** * unpack the executable and remove the archive */ diff --git a/src/index.js b/src/index.js index 948f530..c0aa724 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import debug from 'debug' import { createWriteStream, promises } from 'fs' import { platform, arch } from 'os' +import { join } from 'path' import { clean, satisfies, valid } from 'semver' import { Readable } from 'stream' import { open as openArchive } from 'yauzl' @@ -63,7 +64,7 @@ async function download(url, archive) { }) } -function unpack(archive) { +function unpack(archive, targetDirectory) { log('unpack "%s"', archive) return new Promise((resolve, reject) => openArchive(archive, { lazyEntries: true }, (err, zip) => { @@ -74,14 +75,15 @@ function unpack(archive) { const { fileName } = entry /* c8 ignore next */ if (fileName.endsWith('/')) return new Error('directory in archive') - log('write "%s"', fileName) + const filePath = targetDirectory ? join(targetDirectory, fileName) : fileName + log('write "%s"', filePath) zip.openReadStream(entry, (err, stream) => { /* c8 ignore next */ if (err) return reject(err) stream .on('error', reject) - .pipe(createWriteStream(fileName)) - .on('finish', () => resolve(fileName)) + .pipe(createWriteStream(filePath)) + .on('finish', () => resolve(filePath)) .on('error', reject) }) }) @@ -99,7 +101,7 @@ async function makeExecutable(executable) { } } -export default async function grab({ name, repository, version, platformSuffixes, unpackExecutable, verbose }) { +export default async function grab({ name, repository, version, platformSuffixes, targetDirectory, unpackExecutable, verbose }) { if (verbose) log = console.log.bind(console) if (!version) version = 'latest' const verspec = clean(version) || version @@ -111,9 +113,10 @@ export default async function grab({ name, repository, version, platformSuffixes } else { ({ name, version, archive, url } = await getRelease(name, repository, verspec, platformSuffixes)) } + if (targetDirectory) archive = join(targetDirectory, archive) await download(url, archive) if (unpackExecutable) { - const executable = await unpack(archive) + const executable = await unpack(archive, targetDirectory) await makeExecutable(executable) log('remove "%s"', archive) await unlink(archive) diff --git a/test/mocked.js b/test/mocked.js index 6d0e715..2909203 100644 --- a/test/mocked.js +++ b/test/mocked.js @@ -1,5 +1,5 @@ -import { strictEqual } from 'assert' -import { access, readFile, rm } from 'fs/promises' +import { ok, strictEqual } from 'assert' +import { access, mkdir, readFile, rm } from 'fs/promises' import { after, before, beforeEach, test, mock } from 'node:test' import { arch, platform } from 'os' import { dirname, join } from 'path' @@ -13,7 +13,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)) const repository = 'prantlf/v-jsonlint' const name = 'jsonlint' -const executable = platform() != 'win32' ? name : `${name}.exe` +const executable = join('.', platform() != 'win32' ? name : `${name}.exe`) const version = '0.0.6' const platformSuffixes = { linux: 'linux', @@ -25,11 +25,15 @@ const content = new Blob( [await readFile(join(__dirname, `data/${archive}`))], { type: 'applicaiton.zip' } ) +const targetDirectory = join(__dirname, 'tmp') -function cleanup() { - return Promise.all([ +async function cleanup() { + // failed on Windows on GitHub + if (platform() == 'win32') return + await Promise.all([ rm(archive, { force: true }), - rm(executable, { force: true }) + rm(executable, { force: true }), + rm(targetDirectory, { recursive: true, force: true }) ]) } @@ -54,8 +58,7 @@ before(() => { beforeEach(cleanup) after(async () => { - // failed on Windows on GitHub - if (platform() != 'win32') await cleanup() + await cleanup() mock.reset() }) @@ -114,5 +117,13 @@ test('download archive from the latest implicit version and unpack executable', if (!await exists(executable)) throw new Error('executable not found') strictEqual(actualVersion, version) strictEqual(actualExecutable, executable) - strictEqual(actualExecutable, executable) +}) + +test('download archive from the latest implicit version and unpack executable to a custom directory', async () => { + await mkdir(targetDirectory, { recursive: true }) + const { executable: actualExecutable, version: actualVersion } = await grab( + { name, repository, platformSuffixes, targetDirectory, unpackExecutable: true }) + if (!await exists(actualExecutable)) throw new Error('executable not found') + strictEqual(actualVersion, version) + ok(actualExecutable.endsWith(executable)) }) diff --git a/test/types.test.ts b/test/types.test.ts index 7d07f13..974dae3 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -17,6 +17,7 @@ test('Type declarations for TypeScript', () => { darwin: '', win32: '' }, + targetDirectory: '', unpackExecutable: true }) })