UNPKG

node-jq

Version:
203 lines (167 loc) 4.98 kB
#!/usr/bin/env node 'use strict' // First check if the user has configured a local jq binary if (process.env.npm_config_jq_path) { console.log(`Using configured local jq binary: ${process.env.npm_config_jq_path}`) process.exit(0) } import { spawn } from 'node:child_process' import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, } from 'node:fs' import { dirname, join } from 'node:path' import { Readable } from 'node:stream' import { fileURLToPath } from 'node:url' import { extract } from 'tar' import { temporaryDirectory } from 'tempy' const PLATFORM = process.platform const ARCH = process.arch const JQ_INFO = { name: 'jq', url: 'https://github.com/jqlang/jq/releases/download/', version: 'jq-1.7.1', } const JQ_NAME_MAP = { def: 'jq', win32: 'jq.exe', } const JQ_NAME = PLATFORM in JQ_NAME_MAP ? JQ_NAME_MAP[PLATFORM] : JQ_NAME_MAP.def const __dirname = dirname(fileURLToPath(import.meta.url)) const OUTPUT_DIR = join(__dirname, '..', 'bin') const fileExist = (path) => { try { return existsSync(path) } catch (err) { return false } } if (!existsSync(OUTPUT_DIR)) { mkdirSync(OUTPUT_DIR) console.info(`${OUTPUT_DIR} directory was created`) } if (fileExist(join(OUTPUT_DIR, JQ_NAME))) { console.log('jq is already installed') process.exit(0) } if (process.env.NODE_JQ_SKIP_INSTALL_BINARY === 'true') { console.log('node-jq is skipping the download of jq binary') process.exit(0) } // if platform or arch is missing, download source instead of executable const DOWNLOAD_MAP = { win32: { x64: 'jq-windows-amd64.exe', ia32: 'jq-windows-i386.exe', }, darwin: { x64: 'jq-macos-amd64', arm64: 'jq-macos-arm64', }, linux: { x64: 'jq-linux-amd64', ia32: 'jq-linux-i386', arm64: 'jq-linux-arm64', }, } try { if (PLATFORM in DOWNLOAD_MAP && ARCH in DOWNLOAD_MAP[PLATFORM]) { const filename = DOWNLOAD_MAP[PLATFORM][ARCH] const url = `${JQ_INFO.url}${JQ_INFO.version}/${filename}` downloadJqBinary(url, filename) } else { const url = `${JQ_INFO.url}${JQ_INFO.version}/${JQ_INFO.version}.tar.gz` buildJqSource(url) } } catch (err) { console.error(err) process.exit(1) } async function downloadJqBinary(url, filename) { console.log(`Downloading jq from ${url}`) await downloadFile(url, `${OUTPUT_DIR}/${filename}`) const distPath = join(OUTPUT_DIR, JQ_NAME) renameSync(join(OUTPUT_DIR, filename), distPath) if (fileExist(distPath)) { // fs.chmodSync(distPath, fs.constants.S_IXUSR || 0o100) // Huan(202111): we need the read permission so that the build system can pack the node_modules/ folder, // i.e. build with Heroku CI/CD, docker build, etc. chmodSync(distPath, 0o755) } console.log(`Downloaded in ${OUTPUT_DIR}`) } async function buildJqSource(url) { const tempDir = temporaryDirectory() console.log(`Building jq from ${url}`) try { const tarballPath = join(tempDir, `${JQ_INFO.version}.tar.gz`) await downloadFile(url, tarballPath) await extract({ file: tarballPath, cwd: tempDir, }) const sourceDir = join(tempDir, JQ_INFO.version) await runCommand( './configure', [ '--with-oniguruma=builtin', `--prefix=${tempDir}`, `--bindir=${OUTPUT_DIR}`, ], { cwd: sourceDir }, ) await runCommand('make', ['-j8'], { cwd: sourceDir }) await runCommand('make', ['install'], { cwd: sourceDir }) console.log(`jq installed successfully in ${OUTPUT_DIR}`) } catch (err) { console.error(err) process.exit(1) } } async function downloadFile(url, dest) { const res = await fetch(url) if (!res.ok) throw new Error(`Failed to download jq: ${res.statusText}`) const totalSize = parseInt(res.headers.get('content-length'), 10) let downloadedSize = 0 const fileStream = createWriteStream(dest) const dataStream = Readable.from(res.body) return new Promise((resolve, reject) => { dataStream.on('data', (chunk) => { downloadedSize += chunk.length const percentage = ((downloadedSize / totalSize) * 100).toFixed(2) process.stdout.write(`Downloading jq: ${percentage}%\r`) }) dataStream.pipe(fileStream) dataStream.on('end', () => { console.log('\nDownload complete') fileStream.end() resolve() }) dataStream.on('error', (err) => { fileStream.end() reject(err) }) }) } async function runCommand(command, args, options) { return new Promise((resolve, reject) => { const proc = spawn(command, args, options) proc.stdout.on('data', (data) => { process.stdout.write(`${command}: ${data}`) }) proc.stderr.on('data', (data) => { process.stderr.write(`${command}: error: ${data}`) }) proc.on('close', (code) => { if (code !== 0) { reject(new Error(`Command failed with exit code ${code}`)) } else { resolve() } }) }) }