UNPKG

install-purescript-cli

Version:
513 lines (429 loc) 12.5 kB
#!/usr/bin/env node 'use strict'; const {dirname, resolve} = require('path'); const {promisify} = require('util'); const {stat} = require('fs'); const chalk = require('chalk'); const isPrettyMode = process.stdout && process.stdout.isTTY && !/^1|true$/ui.test(process.env.CI) && !process.env.GITHUB_ACTION; chalk.enabled = chalk.enabled && isPrettyMode; const filesize = require('filesize'); const getCacheInfo = require('npcache').get.info; const installPurescript = require('install-purescript'); const logUpdate = require('log-update'); const logSymbols = require('log-symbols'); const minimist = require('minimist'); const ms = require('ms'); const neatStack = require('neat-stack'); const once = require('once'); const platformName = require('platform-name'); const SizeRate = require('size-rate'); const tildePath = require('tilde-path'); const ttyTruncate = require('tty-truncate'); const ttyWidthFrame = require('tty-width-frame'); const verticalMeter = require('vertical-meter'); const {blue, cyan, dim, magenta, red, strikethrough, underline, yellow} = chalk; const failure = `${logSymbols.error} `; const info = isPrettyMode ? `${logSymbols.info} ` : ''; const success = `${logSymbols.success} `; const warning = `${logSymbols.warning} `; const defaultBinName = `purs${process.platform === 'win32' ? '.exe' : ''}`; const stackArgs = []; const filesizeOptions = { base: 10, round: 2, standard: 'iec' }; const argv = minimist(process.argv.slice(2), { alias: { h: 'help', n: 'name', v: 'version', V: 'purs-ver' }, boolean: [ 'help', 'version' ], string: [ 'name', 'purs-ver' ], default: { 'purs-ver': installPurescript.defaultVersion }, unknown(flag) { if (!installPurescript.supportedBuildFlags.has(flag)) { return; } stackArgs.push(flag); } }); if (argv.help) { console.log(`install-purescript v${require('./package.json').version} Install PureScript to the current working directory Usage: install-purescript [options] Options: --purs-ver, -V <string> Specify PureScript version Default: ${installPurescript.defaultVersion} --name, -n <string> Change a binary name Default: 'purs.exe' on Windows, 'purs' on others Or, if the current working directory contains package.json with \`bin\` field specifying a path of \`purs\` command, this option defaults to its value --help, -h Print usage information --version, -v Print version Also, these flags are passed to \`stack install\` command if provided: ${[...installPurescript.supportedBuildFlags].join('\n')} `); process.exit(); } if (argv.version) { console.log(require('./package.json').version); process.exit(); } if (!argv.name) { try { const {purs} = require(resolve('package.json')).bin; argv.name = purs !== undefined ? purs : defaultBinName; } catch (shouldBeRemovedInTheFuture) { // eslint-disable-line no-unused-vars argv.name = defaultBinName; } } const platform = platformName(); class TaskGroup extends Map { constructor(iterable) { super(iterable); const pairs = [...this.entries()]; for (const [index, [_, task]] of pairs.entries()) { if (index < pairs.length - 1) { task.nextTask = pairs[index + 1][1]; } } } } const taskGroups = [ new TaskGroup([ [ 'search-cache', { head: '' } ] ]), new TaskGroup([ [ 'restore-cache', { head: `Restore the cached ${cyan(argv['purs-ver'])} binary for ${platform}` } ], [ 'check-binary', { head: 'Verify the restored binary works correctly' } ] ]), new TaskGroup([ [ 'head', { head: `Check if a prebuilt ${cyan(argv['purs-ver'])} binary is provided for ${platform}`, status: 'processing' } ], [ 'download-binary', { head: 'Download the prebuilt PureScript binary', byteFormatter: null } ], [ 'check-binary', { head: 'Verify the prebuilt binary works correctly' } ], [ 'write-cache', { head: 'Save the downloaded binary to the npm cache directory', allowFailure: true } ] ]), new TaskGroup([ [ 'check-stack', { head: 'Check if \'stack\' command is available', status: 'processing', noClear: true } ], [ 'download-source', { head: `Download the PureScript ${cyan(argv['purs-ver'])} source`, status: 'processing', byteFormatter: null } ], [ 'setup', { head: 'Ensure the appropriate GHC is installed' } ], [ 'build', { head: 'Build a binary from source' } ], [ 'write-cache', { head: 'Save the built binary to the npm cache directory', allowFailure: true } ] ]) ]; const path = resolve(argv.name); const spinnerFrames = [4, 18, 50, 49, 53, 45, 31, 32, 0, 8].map(code => String.fromCharCode(10247 + code)); let time = Date.now(); let frame = 0; let loop = 0; let cacheWritten = false; const render = isPrettyMode ? () => { const lines = []; for (const [taskName, {allowFailure, byteFormatter, duration, head, message, status, subhead}] of taskGroups[0]) { let willEnd = false; if (status === 'done' || status === 'failed') { const timeInfo = ` (${ms(duration)})`; let mark; if (status === 'done') { mark = success; } else if (allowFailure) { mark = warning; } else { mark = failure; } if (process.stdout.columns > head.length + timeInfo.length + 2 && duration >= 100) { lines.push(ttyTruncate(`${mark}${head}${dim.gray(timeInfo)}`)); } else { lines.push(ttyTruncate(`${mark}${head}`)); } willEnd = true; } else if (status === 'processing') { lines.push(ttyTruncate(`${yellow(spinnerFrames[Math.floor(frame)])} ${head}`)); } else if (status === 'skipped') { lines.push(ttyTruncate(`${yellow('▬')} ${strikethrough(head)}`)); willEnd = true; } else { lines.push(ttyTruncate(` ${head}`)); } if (subhead) { lines.push(ttyTruncate(dim(` ${subhead}`))); } if (message) { if (status === 'failed') { lines.push(`${red(` ${message.replace(/^[ \t]+/u, '')}`)}`); } else { lines.push(ttyTruncate(dim(` ${ byteFormatter ? `⢸${verticalMeter(byteFormatter.bytes / byteFormatter.max)}⡇ ` : '' }${message}`))); } } if (willEnd && taskGroups[0].size > 1) { taskGroups[0].delete(taskName); logUpdate(`${lines.join('\n')}`); logUpdate.done(); lines.splice(0); } } logUpdate(`${lines.join('\n')}\n`); } : () => { for (const [taskName, {allowFailure, duration, message, status, head}] of taskGroups[0]) { if (status !== 'done' && status !== 'failed') { continue; } const durationStr = duration < 100 ? '' : ` (${ms(duration)})`; if (status === 'done') { console.log(`[ SUCCESS ] ${head}${durationStr}`); } else { console.log(`[ ${allowFailure ? 'WARNING' : 'FAILURE'} ] ${head}${durationStr}`); console.log(`${message}\n`); } taskGroups[0].delete(taskName); } }; const initialize = once(firstEvent => { if (firstEvent.id === 'search-cache' && firstEvent.found) { console.log(`${info}Found a cache at ${magenta(tildePath(dirname(firstEvent.path)))}\n`); } if (!isPrettyMode) { return; } loop = setInterval(() => { frame += 0.5; if (frame === spinnerFrames.length) { frame = 0; } render(); }, 40); }); function calcDuration(task) { const newTime = Date.now(); task.duration = newTime - time; time = newTime; } function getCurrentTask(currentId) { while (!taskGroups[0].has(currentId)) { taskGroups.shift(); } return taskGroups[0].get(currentId); } function showError(erroredTask, err) { const showLongMessage = isPrettyMode ? ttyWidthFrame : str => str.replace(/(?:\r?\n)+/ug, ' '); // https://github.com/nodejs/node/blob/v12.0.0/lib/child_process.js#L310 // https://github.com/sindresorhus/execa/blob/4692dcd4cec9097ded284ed6f9a71666bd560564/index.js#L167 const erroredCommand = err.command || err.cmd; erroredTask.status = 'failed'; if (!erroredTask.subhead && erroredCommand) { erroredTask.subhead = erroredCommand; } if (err.code === 'ERR_UNSUPPORTED_PLATFORM' || err.code === 'ERR_UNSUPPORTED_ARCH') { const environment = err.code === 'ERR_UNSUPPORTED_PLATFORM' ? platform : `${err.currentArch} architecture`; erroredTask.message = showLongMessage(`Prebuilt PureScript binary is not provided for ${environment}. Although this program still tries to install PureScript by compiling the source code, it will take much, so much more time to finish than just downloading a prebuilt one. To make installation faster on ${environment}, submit a new issue "Provide a prebuilt binary for ${environment}" to ${underline('https://github.com/purescript/purescript/issues/new')} unless it already exists.`); } else if (err.INSTALL_URL) { erroredTask.message = showLongMessage(`${'\'stack\' command is required for building PureScript from source, ' + 'but it\'s not found in your PATH. Make sure you have installed Stack and try again.\n\n' + '→ '}${underline(err.INSTALL_URL)}`); } else { erroredTask.message = neatStack(err); } calcDuration(erroredTask); for (const task of taskGroups[0].values()) { if (task.status !== 'done' && task.status !== 'failed') { task.status = 'skipped'; } } } installPurescript({ args: stackArgs, rename: () => argv.name, version: argv['purs-ver'], headers: { 'user-agent': 'install-purescript-cli (https://github.com/shinnn/install-purescript-cli)' } }).subscribe({ next(event) { initialize(event); const task = getCurrentTask(event.id.replace(/:.*$/u, '')); if (event.id.endsWith(':fail')) { showError(task, event.error); render(); if (task.allowFailure) { return; } if (isPrettyMode) { logUpdate.done(); } taskGroups.shift(); console.log(`${blue('↓')} ${taskGroups.length === 2 ? 'Reinstall a binary since the cache is broken' : 'Fallback: building from source'}\n`); return; } if (event.id.endsWith(':complete')) { task.status = 'done'; calcDuration(task); if (!task.noClear) { task.subhead = ''; task.message = ''; } if (task.nextTask && !task.nextTask.status) { task.nextTask.status = 'processing'; } if (event.id === 'write-cache:complete') { cacheWritten = true; } render(); return; } if (!isPrettyMode) { if (event.output !== undefined) { if (!task.subhead) { task.subhead = event.command; console.log(`[ RUNNING ] command: ${task.subhead}`); } console.log(` ${event.output}`); } return; } if (event.output !== undefined) { task.status = 'processing'; task.subhead = event.command; task.message = event.output; return; } if (event.id === 'download-binary') { task.subhead = event.response.url; task.status = 'processing'; if (!task.byteFormatter) { task.byteFormatter = new SizeRate({max: event.entry.size}); } task.message = task.byteFormatter.format(event.entry.size - event.entry.remain); return; } if (event.id === 'download-source') { task.subhead = event.response.url; task.status = 'processing'; if (event.entry.header.size !== 0) { task.byteFormatter = new SizeRate({max: event.entry.size}); task.message = `${task.byteFormatter.format(event.entry.size - event.entry.remain)}${event.entry.path}`; } return; } if (event.id === 'check-stack') { task.message = `${event.version} found at ${event.path}`; return; } if (event.id === 'write-cache' && event.brotli) { task.message = `It takes a while to convert the ${filesize(event.originalSize, filesizeOptions)} binary into a few MB cache.`; } }, error(err) { clearInterval(loop); if (err.id) { const task = getCurrentTask(err.id); showError(task, err); render(); } else { console.error(neatStack(err)); } process.exitCode = 1; }, async complete() { render(); clearInterval(loop); if (isPrettyMode) { logUpdate.done(); } else { console.log(); } const [{size: bytes}, {path: cachePath, size: cacheBytes}] = await Promise.all([ promisify(stat)(path), cacheWritten ? getCacheInfo(installPurescript.cacheKey) : {} ]); console.log(`Installed to ${magenta(tildePath(path))} ${dim(filesize(bytes, filesizeOptions))}`); if (cachePath) { console.log(`Cached to ${magenta(tildePath(dirname(cachePath)))} ${dim(filesize(cacheBytes, filesizeOptions))}`); } console.log(); } });