diff --git a/download/install.js b/download/install.js new file mode 100644 index 00000000000..1edb6645dd6 --- /dev/null +++ b/download/install.js @@ -0,0 +1,16 @@ +/* eslint-disable no-var */ + +// Native +var path = require('path') +var fs = require('fs') + +var dist = path.join(__dirname, 'dist') +var src = path.join(__dirname, 'src') + +// Don't install when developing locally +if (fs.existsSync(src)) { + // eslint-disable-next-line unicorn/no-process-exit + process.exit(0) +} + +require(path.join(dist, 'download.js')) diff --git a/download/src/chmod.js b/download/src/chmod.js new file mode 100644 index 00000000000..52d8f427f0f --- /dev/null +++ b/download/src/chmod.js @@ -0,0 +1,10 @@ +// Native +import fs from 'fs' + +export default function (file) { + const s = fs.statSync(file) + const newMode = s.mode | 64 | 8 | 1 + if (s.mode === newMode) return + const base8 = newMode.toString(8).slice(-3) + fs.chmodSync(file, base8) +} diff --git a/download/src/index.js b/download/src/index.js new file mode 100644 index 00000000000..39b566ba8e7 --- /dev/null +++ b/download/src/index.js @@ -0,0 +1,148 @@ +/* eslint-disable unicorn/no-process-exit */ + +// Native +import fs from 'fs' +import path from 'path' +import zlib from 'zlib' + +// Packages +import onDeath from 'death' +import fetch from 'node-fetch' +import retry from 'async-retry' + +// Utilities +import plusxSync from './chmod' +import { + disableProgress, + enableProgress, + info, + showProgress, + warn +} from './log' + +fetch.Promise = Promise +global.Promise = Promise +const now = path.join(__dirname, 'now') +const targetWin32 = path.join(__dirname, 'now.exe') +const target = process.platform === 'win32' ? targetWin32 : now +const partial = target + '.partial' + +const packagePath = path.join(__dirname, '../../package.json') +const packageJSON = JSON.parse(fs.readFileSync(packagePath, 'utf8')) + +const platformToName = { + darwin: 'now-macos', + linux: 'now-linux', + win32: 'now-win.exe' +} + +async function main() { + try { + fs.writeFileSync( + now, + '#!/usr/bin/env node\n' + + 'console.log("Please wait until the \'now\' installation completes!")\n' + ) + } catch (err) { + if (err.code === 'EACCES') { + warn( + 'Please try installing now CLI again with the `--unsafe-perm` option.' + ) + info('Example: `npm i -g --unsafe-perm now`') + + process.exit() + } + + throw err + } + + onDeath(() => { + fs.writeFileSync( + now, + '#!/usr/bin/env node\n' + + 'console.log("The \'now\' installation did not complete successfully.")\n' + + 'console.log("Please run \'npm i -g now\' to reinstall!")\n' + ) + process.exit() + }) + + info('For the source code, check out: https://github.com/zeit/now-cli') + + // Print an empty line + console.log('') + + await retry(async () => { + enableProgress('Downloading now CLI ' + packageJSON.version) + showProgress(0) + + try { + const name = platformToName[process.platform] + const url = `https://cdn.zeit.co/releases/now-cli/${packageJSON.version}/${name}` + const resp = await fetch(url, { compress: false }) + + if (resp.status !== 200) { + throw new Error(resp.statusText + ' ' + url) + } + + const size = resp.headers.get('content-length') + const ws = fs.createWriteStream(partial) + + await new Promise((resolve, reject) => { + let bytesRead = 0 + + resp.body + .on('error', reject) + .on('data', chunk => { + bytesRead += chunk.length + showProgress(100 * bytesRead / size) + }) + + const gunzip = zlib.createGunzip() + + gunzip + .on('error', reject) + + resp.body.pipe(gunzip).pipe(ws) + + ws + .on('error', reject) + .on('close', () => { + showProgress(100) + resolve() + }) + }) + } finally { + disableProgress() + } + }, { + retries: 500, + onRetry: (err) => console.error(err) + }) + + fs.renameSync(partial, target) + + if (process.platform === 'win32') { + // Now.exe is executed only + fs.unlinkSync(now) + // Workaround for https://github.com/npm/cmd-shim/pull/25 + const gitBashFile = path.join(process.env.APPDATA, 'npm/now') + fs.writeFileSync( + gitBashFile, + '#!/bin/sh\n' + + 'basedir=$(dirname "$(echo "$0" | sed -e \'s,\\\\,/,g\')")\n' + + '\n' + + 'case `uname` in\n' + + ' *CYGWIN*) basedir=`cygpath -w "$basedir"`;;\n' + + 'esac\n' + + '\n' + + fs.readFileSync(gitBashFile, 'utf8') + ) + } else { + plusxSync(now) + } +} + +main().catch(err => { + console.error(err) + process.exit(2) +}) diff --git a/download/src/log.js b/download/src/log.js new file mode 100644 index 00000000000..aa5e5df1ed3 --- /dev/null +++ b/download/src/log.js @@ -0,0 +1,43 @@ +// Packages +import assert from 'assert' +import chalk from 'chalk' +import Progress from 'progress' + +let bar + +export function enableProgress(text) { + assert(!bar) + + bar = new Progress(`> ${text} [:bar] :percent`, { + stream: process.stdout, + width: 20, + complete: '=', + incomplete: ' ', + total: 100 + }) +} + +export function info(text) { + console.log(`> ${text}`) +} + +export function warn(text) { + console.log(chalk.red('> Warning!'), text) +} + +export function showProgress(percentage) { + assert(bar) + bar.update(percentage / 100) +} + +export function disableProgress() { + assert(bar) + + // It is auto-completed once it updates to 100 + // otherwise it outputs a blank line + if (!bar.complete) { + bar.terminate() + } + + bar = undefined +} diff --git a/download/webpack.config.js b/download/webpack.config.js new file mode 100644 index 00000000000..b935ed94f15 --- /dev/null +++ b/download/webpack.config.js @@ -0,0 +1,34 @@ +// Native +const path = require('path') + +module.exports = { + target: 'node', + node: { + __dirname: false, + __filename: false, + process: false + }, + entry: [ + './src/index.js' + ], + output: { + path: path.join(__dirname, 'dist'), + filename: 'download.js' + }, + module: { + loaders: [ { + test: /.js$/, + loader: 'babel-loader', + exclude: /node_modules/, + query: { + plugins: [ + 'transform-async-to-generator', + 'transform-runtime' + ], + presets: [ + 'es2015' + ] + } + } ] + } +} diff --git a/package.json b/package.json index da33a67b76d..60ab6218641 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,12 @@ "scripts": { "test": "eslint .", "prepublish": "webpack", - "lint:staged": "lint-staged", "dev": "webpack -w", - "precommit": "lint-staged" + "precommit": "lint-staged", + "postinstall": "node download/install.js", + "prepublish": "in-install || (npm run prepare && cp /dev/null download/dist/now)", + "pack": "pkg dist/now.js -c package.json -o packed/now", + "prepare": "webpack --context download --config download/webpack.config.js" }, "lint-staged": { "*.js": [ @@ -16,15 +19,28 @@ "git add" ] }, - "main": "./dist/now.js", + "bin": { + "now": "download/dist/now" + }, "files": [ - "dist" + "download/dist", + "download/install.js" ], - "bin": { - "now": "./dist/now.js" + "pkg": { + "scripts": [ + "bin/*", + "lib/**/*" + ], + "targets": [ + "node7-alpine-x64", + "node7-linux-x64", + "node7-macos-x64", + "node7-win-x64" + ] }, "eslintIgnore": [ "src/providers/sh/legacy/**", + "download/**", "dist" ], "eslintConfig": { @@ -63,6 +79,7 @@ "ansi-escapes": "2.0.0", "archiver": "2.0.0", "array-unique": "0.3.2", + "assert": "1.4.1", "async-retry": "1.1.3", "aws-sdk": "2.98.0", "babel-core": "6.25.0", @@ -76,6 +93,7 @@ "chalk": "2.1.0", "clipboardy": "1.1.4", "convert-stream": "1.0.2", + "death": "1.1.0", "debug": "3.0.0", "deployment-type": "1.0.1", "docker-file-parser": "1.0.2", diff --git a/webpack.config.js b/webpack.config.js index 6f909d333a2..b999b427126 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,7 +13,7 @@ module.exports = { loaders: [ { test: /\.js$/, - exclude: [/node_modules/], + exclude: /node_modules/, loaders: ['shebang-loader', 'babel-loader'] } ] diff --git a/yarn.lock b/yarn.lock index ea3ef2a00a3..e553b46e721 100644 --- a/yarn.lock +++ b/yarn.lock @@ -205,7 +205,7 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" -assert@^1.1.1: +assert@1.4.1, assert@^1.1.1: version "1.4.1" resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" dependencies: @@ -1253,6 +1253,10 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" +death@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/death/-/death-1.1.0.tgz#01aa9c401edd92750514470b8266390c66c67318" + debug@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.0.tgz#1d2feae53349047b08b264ec41906ba17a8516e4"