diff --git a/.gitignore b/.gitignore index e9a016a..e950a29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # build output -build -out +packed # dependencies node_modules diff --git a/.npmrc b/.npmrc deleted file mode 100644 index cffe8cd..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true diff --git a/.travis.yml b/.travis.yml index 16c715b..b37faa2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ { "language": "node_js", - "sudo": false, "node_js": "node" } diff --git a/README.md b/README.md index 8170c93..9e7ccb6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,52 @@ -# now +# now CLI -[![Build Status](https://travis-ci.org/zeit/now.svg?branch=master)](https://travis-ci.org/zeit/now) +[![Build Status](https://travis-ci.org/zeit/now-cli.svg?branch=master)](https://travis-ci.org/zeit/now-cli) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) [![Slack Channel](https://zeit-slackin.now.sh/badge.svg)](https://zeit.chat) -Realtime global deployments served over HTTP/2. You can find the FAQ [here](https://github.com/zeit/now/wiki/FAQ). +Realtime global deployments served over HTTP/2. You can find the FAQs [here](https://zeit.co/now#frequently-asked-questions). ## Usage -Firstly, make sure to install the package: +Firstly, make sure to install the package globally: ```bash -$ npm install -g now +npm install -g now ``` -Run this in any directory: +Run this command in your terminal: ```bash -$ now +now ``` For more examples, usage instructions and other commands run: ```bash -$ now help +now help ``` -## Contribute +### Options + +Run this command to get a list of all available commands: + +```bash +now help +``` + +## Caught a Bug? 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 2. Link the package to the global module directory: `npm link` -3. Transpile the source code and watch for changes: `npm start` +3. Generate a [testing token](https://zeit.co/account#api-tokens) and put it into the `token` property within `~/.now.json` 4. You can now start using `now` from the command line! As always, you can use `npm test` to run the tests and see if your changes have broken anything. + +## Authors + +- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) - [▲ZEIT](https://zeit.co) +- Leo Lamprecht ([@notquiteleo](https://twitter.com/notquiteleo)) - [▲ZEIT](https://zeit.co) +- Tony Kovanen ([@TonyKovanen](https://twitter.com/TonyKovanen)) - [▲ZEIT](https://zeit.co) +- Olli Vanhoja ([@OVanhoja](https://twitter.com/OVanhoja)) - [▲ZEIT](https://zeit.co) +- Naoyuki Kanezawa ([@nkzawa](https://twitter.com/nkzawa)) - [▲ZEIT](https://zeit.co) diff --git a/bin/now-alias.js b/bin/now-alias.js index 1ac35f7..a66a7c9 100755 --- a/bin/now-alias.js +++ b/bin/now-alias.js @@ -1,19 +1,19 @@ #!/usr/bin/env node // Packages -import chalk from 'chalk' -import minimist from 'minimist' -import table from 'text-table' -import ms from 'ms' +const chalk = require('chalk') +const minimist = require('minimist') +const table = require('text-table') +const ms = require('ms') // Ours -import strlen from '../lib/strlen' -import NowAlias from '../lib/alias' -import login from '../lib/login' -import * as cfg from '../lib/cfg' -import {error} from '../lib/error' -import toHost from '../lib/to-host' -import readMetaData from '../lib/read-metadata' +const strlen = require('../lib/strlen') +const NowAlias = require('../lib/alias') +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const {error} = require('../lib/error') +const toHost = require('../lib/to-host') +const readMetaData = require('../lib/read-metadata') const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], @@ -25,6 +25,7 @@ const argv = minimist(process.argv.slice(2), { token: 't' } }) + const subcommand = argv._[0] // options @@ -120,8 +121,8 @@ async function run(token) { const args = argv._.slice(1) switch (subcommand) { - case 'list': case 'ls': + case 'list': { if (args.length !== 0) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls`')}`) return exit(1) @@ -160,9 +161,9 @@ async function run(token) { } break - - case 'remove': + } case 'rm': + case 'remove': { const _target = String(args[0]) if (!_target) { const err = new Error('No alias id specified') @@ -201,17 +202,17 @@ async function run(token) { } break - + } case 'add': - case 'set': + case 'set': { if (args.length !== 2) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias set `')}`) return exit(1) } await alias.set(String(args[0]), String(args[1])) break - - default: + } + default: { if (argv._.length === 0) { await realias(alias) break @@ -228,6 +229,7 @@ async function run(token) { help() exit(1) } + } } alias.close() @@ -247,7 +249,7 @@ async function readConfirmation(alias, _alias) { process.stdout.write('> The following alias will be removed permanently\n') process.stdout.write(' ' + tbl + '\n') - process.stdout.write(` ${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) + process.stdout.write(` ${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdin.on('data', d => { process.stdin.pause() diff --git a/bin/now-certs.js b/bin/now-certs.js index 4a7e69c..e86d830 100755 --- a/bin/now-certs.js +++ b/bin/now-certs.js @@ -1,21 +1,21 @@ #!/usr/bin/env node // Native -import path from 'path' +const path = require('path') // Packages -import chalk from 'chalk' -import table from 'text-table' -import minimist from 'minimist' -import fs from 'fs-promise' -import ms from 'ms' +const chalk = require('chalk') +const table = require('text-table') +const minimist = require('minimist') +const fs = require('fs-promise') +const ms = require('ms') // Ours -import strlen from '../lib/strlen' -import * as cfg from '../lib/cfg' -import {handleError, error} from '../lib/error' -import NowCerts from '../lib/certs' -import login from '../lib/login' +const strlen = require('../lib/strlen') +const cfg = require('../lib/cfg') +const {handleError, error} = require('../lib/error') +const NowCerts = require('../lib/certs') +const login = require('../lib/login') const argv = minimist(process.argv.slice(2), { string: ['config', 'token', 'crt', 'key', 'ca'], @@ -108,7 +108,7 @@ if (argv.help || !subcommand) { function formatExpirationDate(date) { const diff = date - Date.now() - return diff < 0 ? chalk.gray(ms(new Date(-diff)) + ' ago') : chalk.gray('in ' + ms(new Date(diff))) + return diff < 0 ? chalk.gray(ms(-diff) + ' ago') : chalk.gray('in ' + ms(diff)) } async function run(token) { @@ -132,7 +132,7 @@ async function run(token) { list.sort((a, b) => { return a.cn.localeCompare(b.cn) }) - const header = [['', 'id', 'cn', 'created', 'expiration'].map(s => chalk.dim(s))] + const header = [['', 'id', 'cn', 'created', 'expiration', 'auto-renew'].map(s => chalk.dim(s))] const out = table(header.concat(list.map(cert => { const cn = chalk.bold(cert.cn) const time = chalk.gray(ms(cur - new Date(cert.created)) + ' ago') @@ -142,7 +142,8 @@ async function run(token) { cert.uid ? cert.uid : 'unknown', cn, time, - expiration + expiration, + cert.autoRenew ? 'yes' : 'no' ] })), {align: ['l', 'r', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) @@ -172,6 +173,10 @@ async function run(token) { } else { // Issue a standard certificate cert = await certs.create(cn) } + if (!cert) { + // Cert is undefined and "Cert is already issued" has been printed to stdout + return exit(1) + } const elapsed = ms(new Date() - start) console.log(`${chalk.cyan('> Success!')} Certificate entry ${chalk.bold(cn)} ${chalk.gray(`(${cert.uid})`)} created ${chalk.gray(`[${elapsed}]`)}`) } else if (subcommand === 'renew') { @@ -260,7 +265,7 @@ function readConfirmation(cert, msg) { process.stdout.write(`> ${msg}`) process.stdout.write(' ' + tbl + '\n') - process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) + process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdin.on('data', d => { process.stdin.pause() diff --git a/bin/now-deploy.js b/bin/now-deploy.js index 84f0922..540b832 100755 --- a/bin/now-deploy.js +++ b/bin/now-deploy.js @@ -1,38 +1,46 @@ #!/usr/bin/env node // Native -import {resolve} from 'path' +const {resolve} = require('path') // Packages -import Progress from 'progress' -import {stat} from 'fs-promise' -import bytes from 'bytes' -import chalk from 'chalk' -import minimist from 'minimist' -import ms from 'ms' +const Progress = require('progress') +const fs = require('fs-promise') +const bytes = require('bytes') +const chalk = require('chalk') +const minimist = require('minimist') +const ms = require('ms') +const publicSuffixList = require('psl') +const flatten = require('arr-flatten') // Ours -import copy from '../lib/copy' -import login from '../lib/login' -import * as cfg from '../lib/cfg' -import {version} from '../../package' -import Logger from '../lib/build-logger' -import Now from '../lib' -import toHumanPath from '../lib/utils/to-human-path' -import promptOptions from '../lib/utils/prompt-options' -import {handleError, error} from '../lib/error' -import readMetaData from '../lib/read-metadata' +const copy = require('../lib/copy') +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const {version} = require('../package') +const Logger = require('../lib/build-logger') +const Now = require('../lib') +const toHumanPath = require('../lib/utils/to-human-path') +const promptOptions = require('../lib/utils/prompt-options') +const {handleError, error} = require('../lib/error') +const {fromGit, isRepoPath, gitPathParts} = require('../lib/git') +const readMetaData = require('../lib/read-metadata') +const checkPath = require('../lib/utils/check-path') +const NowAlias = require('../lib/alias') const argv = minimist(process.argv.slice(2), { string: [ 'config', - 'token' + 'token', + 'name', + 'alias' ], boolean: [ 'help', 'version', 'debug', 'force', + 'links', 'login', 'no-clipboard', 'forward-npm', @@ -49,10 +57,13 @@ const argv = minimist(process.argv.slice(2), { force: 'f', token: 't', forceSync: 'F', + links: 'l', login: 'L', public: 'p', 'no-clipboard': 'C', - 'forward-npm': 'N' + 'forward-npm': 'N', + name: 'n', + alias: 'a' } }) @@ -69,21 +80,25 @@ const help = () => { domains [name] Manages your domain names certs [cmd] Manages your SSL certificates secrets [name] Manages your secret environment variables + dns [name] Manages your DNS records help [cmd] Displays complete help for [cmd] ${chalk.dim('Options:')} -h, --help Output usage information -v, --version Output the version number + -n, --name Set the name of the deployment -c ${chalk.underline('FILE')}, --config=${chalk.underline('FILE')} Config file -d, --debug Debug mode [off] -f, --force Force a new deployment even if nothing has changed -t ${chalk.underline('TOKEN')}, --token=${chalk.underline('TOKEN')} Login token -L, --login Configure login + -l, --links Copy symlinks without resolving their target -p, --public Deployment is public (${chalk.dim('`/_src`')} is exposed) [on for oss, off for premium] -e, --env Include an env var (e.g.: ${chalk.dim('`-e KEY=value`')}). Can appear many times. -C, --no-clipboard Do not attempt to copy URL to clipboard -N, --forward-npm Forward login information to install private NPM modules + -a, --alias Reassign an existing alias to the deployment ${chalk.dim('Enforcable Types (when both package.json and Dockerfile exist):')} @@ -101,19 +116,15 @@ const help = () => { ${chalk.cyan('$ now /usr/src/project')} - ${chalk.gray('–')} Lists all deployments with their IDs + ${chalk.gray('–')} Deploys a GitHub repository - ${chalk.cyan('$ now ls')} + ${chalk.cyan('$ now user/repo#ref')} - ${chalk.gray('–')} Associates deployment ${chalk.dim('`deploymentId`')} with ${chalk.dim('`custom-domain.com`')} + ${chalk.gray('–')} Deploys a GitHub, GitLab or Bitbucket repo using its URL - ${chalk.cyan('$ now alias deploymentId custom-domain.com')} + ${chalk.cyan('$ now https://gitlab.com/user/repo')} - ${chalk.gray('–')} Stores a secret - - ${chalk.cyan('$ now secret add mysql-password 123456')} - - ${chalk.gray('–')} Deploys with ENV vars (using the ${chalk.dim('`mysql-password`')} secret stored above) + ${chalk.gray('–')} Deploys with ENV vars ${chalk.cyan('$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password')} @@ -133,6 +144,9 @@ if (path) { path = process.cwd() } +// If the current deployment is a repo +const gitRepo = {} + const exit = code => { // we give stdout some time to flush out // because there's a node bug where @@ -142,21 +156,32 @@ const exit = code => { } // options +let forceNew = argv.force const debug = argv.debug const clipboard = !argv['no-clipboard'] const forwardNpm = argv['forward-npm'] -const forceNew = argv.force const forceSync = argv.forceSync const shouldLogin = argv.login +const followSymlinks = !argv.links const wantsPublic = argv.public +const deploymentName = argv.name || false const apiUrl = argv.url || 'https://api.zeit.co' const isTTY = process.stdout.isTTY const quiet = !isTTY +const autoAliases = argv.alias ? flatten([argv.alias]) : [] if (argv.config) { cfg.setConfigFile(argv.config) } +// Create a new deployment if user changed +// the name or made _src public. +// This should just work fine because it doesn't +// force a new sync, it just forces a new deployment. +if (deploymentName || wantsPublic) { + forceNew = true +} + const config = cfg.read() const alwaysForwardNpm = config.forwardNpm @@ -192,16 +217,62 @@ if (argv.h || argv.help) { async function sync(token) { const start = Date.now() + const rawPath = argv._[0] - if (!quiet) { - console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`) + const stopDeployment = msg => { + error(msg) + process.exit(1) } + const isValidRepo = isRepoPath(rawPath) + try { - await stat(path) + await fs.stat(path) } catch (err) { - error(`Could not read directory ${chalk.bold(path)}`) - process.exit(1) + let repo + + if (isValidRepo && isValidRepo !== 'no-valid-url') { + const gitParts = gitPathParts(rawPath) + Object.assign(gitRepo, gitParts) + + const searchMessage = setTimeout(() => { + console.log(`> Didn't find directory. Searching on ${gitRepo.type}...`) + }, 500) + + try { + repo = await fromGit(rawPath, debug) + } catch (err) {} + + clearTimeout(searchMessage) + } + + if (repo) { + // Tell now which directory to deploy + path = repo.path + + // Set global variable for deleting tmp dir later + // once the deployment has finished + Object.assign(gitRepo, repo) + } else if (isValidRepo === 'no-valid-url') { + stopDeployment(`This URL is neither a valid repository from GitHub, nor from GitLab.`) + } else if (isValidRepo) { + const gitRef = gitRepo.ref ? `with "${chalk.bold(gitRepo.ref)}" ` : '' + stopDeployment(`There's no repository named "${chalk.bold(gitRepo.main)}" ${gitRef}on ${gitRepo.type}`) + } else { + stopDeployment(`Could not read directory ${chalk.bold(path)}`) + } + } + + // Make sure that directory is not too big + await checkPath(path) + + if (!quiet) { + if (gitRepo.main) { + const gitRef = gitRepo.ref ? ` at "${chalk.bold(gitRepo.ref)}" ` : '' + console.log(`> Deploying ${gitRepo.type} repository "${chalk.bold(gitRepo.main)}"` + gitRef) + } else { + console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`) + } } let deploymentType @@ -227,7 +298,7 @@ async function sync(token) { isStatic = true } else { try { - await stat(resolve(path, 'package.json')) + await fs.stat(resolve(path, 'package.json')) } catch (err) { hasPackage = true } @@ -235,7 +306,7 @@ async function sync(token) { [hasPackage, hasDockerfile] = await Promise.all([ await (async () => { try { - await stat(resolve(path, 'package.json')) + await fs.stat(resolve(path, 'package.json')) } catch (err) { return false } @@ -243,7 +314,7 @@ async function sync(token) { })(), await (async () => { try { - await stat(resolve(path, 'Dockerfile')) + await fs.stat(resolve(path, 'Dockerfile')) } catch (err) { return false } @@ -273,19 +344,19 @@ async function sync(token) { } } else if (hasPackage) { if (debug) { - console.log('[debug] `package.json` found, assuming `deploymentType` = `npm`') + console.log('> [debug] `package.json` found, assuming `deploymentType` = `npm`') } deploymentType = 'npm' } else if (hasDockerfile) { if (debug) { - console.log('[debug] `Dockerfile` found, assuming `deploymentType` = `docker`') + console.log('> [debug] `Dockerfile` found, assuming `deploymentType` = `docker`') } deploymentType = 'docker' } else { if (debug) { - console.log('[debug] No manifest files found, assuming static deployment') + console.log('> [debug] No manifest files found, assuming static deployment') } isStatic = true @@ -294,6 +365,7 @@ async function sync(token) { const {pkg: {now: pkgConfig = {}} = {}} = await readMetaData(path, { deploymentType, + deploymentName, isStatic, quiet: true }) @@ -323,6 +395,7 @@ async function sync(token) { error('Env key and value missing') return process.exit(1) } + const [key, ...rest] = kv.split('=') let val @@ -344,7 +417,7 @@ async function sync(token) { if ((key in process.env)) { console.log(`> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`) // escape value if it begins with @ - val = process.env[key].replace(/^\@/, '\\@') + val = process.env[key].replace(/^@/, '\\@') } else { error(`No value specified for env ${chalk.bold(`"${chalk.bold(key)}"`)} and it was not found in your env.`) return process.exit(1) @@ -390,6 +463,8 @@ async function sync(token) { await now.create(path, { env, deploymentType, + deploymentName, + followSymlinks, forceNew, forceSync, forwardNpm: alwaysForwardNpm || forwardNpm, @@ -437,7 +512,7 @@ async function sync(token) { now.close() // show build logs - printLogs(now.host) + printLogs(now.host, token) } if (now.syncAmount) { @@ -474,17 +549,67 @@ async function sync(token) { now.close() // show build logs - printLogs(now.host) + printLogs(now.host, token) } } -function printLogs(host) { +const assignAlias = async (autoAlias, token, deployment) => { + const type = publicSuffixList.isValid(autoAlias) ? 'alias' : 'uid' + + const aliases = new NowAlias(apiUrl, token, {debug}) + const list = await aliases.ls() + + let related + + // Check if alias even exists + for (const alias of list) { + if (alias[type] === autoAlias) { + related = alias + break + } + } + + // If alias doesn't exist + if (!related) { + // Check if the uid was actually an alias + if (type === 'uid') { + return assignAlias(`${autoAlias}.now.sh`, token, deployment) + } + + // If not, throw an error + const withID = type === 'uid' ? 'with ID ' : '' + error(`Alias ${withID}"${autoAlias}" doesn't exist`) + return + } + + console.log(`> Assigning alias ${chalk.bold.underline(related.alias)} to deployment...`) + + // Assign alias + await aliases.set(String(deployment), String(related.alias)) +} + +function printLogs(host, token) { // log build const logger = new Logger(host, {debug, quiet}) - logger.on('close', () => { + + logger.on('close', async () => { + for (const autoAlias of autoAliases) { + await assignAlias(autoAlias, token, host) + } + if (!quiet) { console.log(`${chalk.cyan('> Deployment complete!')}`) } + + if (gitRepo && gitRepo.cleanup) { + // Delete temporary directory that contains repository + gitRepo.cleanup() + + if (debug) { + console.log(`> [debug] Removed temporary repo directory`) + } + } + process.exit(0) }) } diff --git a/bin/now-dns.js b/bin/now-dns.js index 08aede7..b521350 100755 --- a/bin/now-dns.js +++ b/bin/now-dns.js @@ -1,18 +1,18 @@ #!/usr/bin/env node // Packages -import chalk from 'chalk' -import minimist from 'minimist' -import ms from 'ms' -import table from 'text-table' +const chalk = require('chalk') +const minimist = require('minimist') +const ms = require('ms') +const table = require('text-table') // Ours -import * as cfg from '../lib/cfg' -import DomainRecords from '../lib/domain-records' -import indent from '../lib/indent' -import login from '../lib/login' -import strlen from '../lib/strlen' -import {handleError, error} from '../lib/error' +const cfg = require('../lib/cfg') +const DomainRecords = require('../lib/domain-records') +const indent = require('../lib/indent') +const login = require('../lib/login') +const strlen = require('../lib/strlen') +const {handleError, error} = require('../lib/error') const argv = minimist(process.argv.slice(2), { string: ['config'], @@ -24,13 +24,14 @@ const argv = minimist(process.argv.slice(2), { token: 't' } }) + const subcommand = argv._[0] // options const help = () => { console.log(` ${chalk.bold('𝚫 now dns ls')} [domain] - ${chalk.bold('𝚫 now dns add')} [mx_priority] + ${chalk.bold('𝚫 now dns add')} [mx_priority] ${chalk.bold('𝚫 now dns rm')} ${chalk.dim('Options:')} @@ -192,7 +193,7 @@ function readConfirmation(record, msg) { process.stdout.write(`> ${msg}`) process.stdout.write(' ' + tbl + '\n') - process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) + process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdin.on('data', d => { process.stdin.pause() diff --git a/bin/now-domains.js b/bin/now-domains.js index 2cfc992..a039583 100755 --- a/bin/now-domains.js +++ b/bin/now-domains.js @@ -1,30 +1,32 @@ #!/usr/bin/env node // Packages -import chalk from 'chalk' -import minimist from 'minimist' -import table from 'text-table' -import ms from 'ms' +const chalk = require('chalk') +const minimist = require('minimist') +const table = require('text-table') +const ms = require('ms') // Ours -import login from '../lib/login' -import * as cfg from '../lib/cfg' -import {error} from '../lib/error' -import toHost from '../lib/to-host' -import strlen from '../lib/strlen' -import NowDomains from '../lib/domains' +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const {error} = require('../lib/error') +const toHost = require('../lib/to-host') +const strlen = require('../lib/strlen') +const NowDomains = require('../lib/domains') const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], - boolean: ['help', 'debug', 'force'], + boolean: ['help', 'debug', 'external', 'force'], alias: { help: 'h', config: 'c', debug: 'd', + external: 'e', force: 'f', token: 't' } }) + const subcommand = argv._[0] // options @@ -37,6 +39,7 @@ const help = () => { -h, --help Output usage information -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -d, --debug Debug mode [off] + -e, --external Use external DNS server -f, --force Skip DNS verification -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token @@ -77,6 +80,18 @@ const help = () => { ${chalk.cyan('$ now domain rm domainId')} To get the list of domain ids, use ${chalk.dim('`now domains ls`')}. + + ${chalk.gray('–')} Adding and verifying a domain name using zeit.world nameservers: + + ${chalk.cyan('$ now domain add my-app.com')} + + The command will tell you if the domain was verified succesfully. In case the domain was not verified succesfully you should retry adding the domain after some time. + + ${chalk.gray('–')} Adding and verifying a domain name using an external nameserver: + + ${chalk.cyan('$ now domain add -e my-app.com')} + + and follow the verification instructions if requested. Finally, rerun the same command after completing the verification step. `) } @@ -127,7 +142,7 @@ async function run(token) { switch (subcommand) { case 'ls': - case 'list': + case 'list': { if (args.length !== 0) { error('Invalid number of arguments') return exit(1) @@ -137,7 +152,7 @@ async function run(token) { const domains = await domain.ls() domains.sort((a, b) => new Date(b.created) - new Date(a.created)) const current = new Date() - const header = [['', 'id', 'dns', 'url', 'created'].map(s => chalk.dim(s))] + const header = [['', 'id', 'dns', 'url', 'verified', 'created'].map(s => chalk.dim(s))] const out = domains.length === 0 ? null : table(header.concat(domains.map(domain => { const ns = domain.isExternal ? 'external' : 'zeit.world' const url = chalk.underline(`https://${domain.name}`) @@ -147,9 +162,10 @@ async function run(token) { domain.uid, ns, url, + domain.verified, time ] - })), {align: ['l', 'r', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) + })), {align: ['l', 'r', 'l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) const elapsed_ = ms(new Date() - start_) console.log(`> ${domains.length} domain${domains.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed_}]`)}`) @@ -159,9 +175,9 @@ async function run(token) { } break - + } case 'rm': - case 'remove': + case 'remove': { if (args.length !== 1) { error('Invalid number of arguments') return exit(1) @@ -199,9 +215,9 @@ async function run(token) { exit(1) } break - + } case 'add': - case 'set': + case 'set': { if (args.length !== 1) { error('Invalid number of arguments') return exit(1) @@ -209,15 +225,21 @@ async function run(token) { const start = new Date() const name = String(args[0]) - const {uid, created} = await domain.add(name, argv.force) + const {uid, code, verified, verifyToken, created} = await domain.add(name, argv.force, argv.external) const elapsed = ms(new Date() - start) - if (created) { + if (created && verified) { console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} added [${elapsed}]`) - } else { + } else if (verified) { + console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} verified [${elapsed}]`) + } else if (verifyToken) { + console.log(`> Verification required: Please add the following TXT record on the external DNS server: _now.${name}: ${verifyToken}`) + } else if (code === 'not_modified') { console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} already exists [${elapsed}]`) + } else { + console.log('> Verification required: Please rerun this command after some time') } break - + } default: error('Please specify a valid subcommand: ls | add | rm') help() @@ -244,7 +266,7 @@ async function readConfirmation(domain, _domain) { `will be removed. Run ${chalk.dim('`now alias ls`')} to list.\n`) } - process.stdout.write(` ${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) + process.stdout.write(` ${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdin.on('data', d => { process.stdin.pause() diff --git a/bin/now-list.js b/bin/now-list.js index 61dc539..ff7b581 100755 --- a/bin/now-list.js +++ b/bin/now-list.js @@ -1,19 +1,19 @@ #!/usr/bin/env node // Packages -import fs from 'fs-promise' -import minimist from 'minimist' -import chalk from 'chalk' -import table from 'text-table' -import ms from 'ms' +const fs = require('fs-promise') +const minimist = require('minimist') +const chalk = require('chalk') +const table = require('text-table') +const ms = require('ms') // Ours -import strlen from '../lib/strlen' -import indent from '../lib/indent' -import Now from '../lib' -import login from '../lib/login' -import * as cfg from '../lib/cfg' -import {handleError, error} from '../lib/error' +const strlen = require('../lib/strlen') +const indent = require('../lib/indent') +const Now = require('../lib') +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const {handleError, error} = require('../lib/error') const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], @@ -108,7 +108,7 @@ async function list(token) { const text = sorted.map(([name, deps]) => { const t = table(deps.map(({uid, url, created}) => { - const _url = chalk.underline(`https://${url}`) + const _url = url ? chalk.underline(`https://${url}`) : 'incomplete' const time = chalk.gray(ms(current - created) + ' ago') return [uid, _url, time] }), {align: ['l', 'r', 'l'], hsep: ' '.repeat(6), stringLength: strlen}) diff --git a/bin/now-remove.js b/bin/now-remove.js index e9537eb..b54472d 100755 --- a/bin/now-remove.js +++ b/bin/now-remove.js @@ -1,25 +1,26 @@ #!/usr/bin/env node // Packages -import minimist from 'minimist' -import chalk from 'chalk' -import ms from 'ms' -import table from 'text-table' +const minimist = require('minimist') +const chalk = require('chalk') +const ms = require('ms') +const table = require('text-table') // Ours -import Now from '../lib' -import login from '../lib/login' -import * as cfg from '../lib/cfg' -import {handleError, error} from '../lib/error' +const Now = require('../lib') +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const {handleError, error} = require('../lib/error') const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], - boolean: ['help', 'debug', 'hard'], + boolean: ['help', 'debug', 'hard', 'yes'], alias: { help: 'h', config: 'c', debug: 'd', - token: 't' + token: 't', + yes: 'y' } }) @@ -36,6 +37,7 @@ const help = () => { -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -d, --debug Debug mode [off] -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token + -y, --yes Skip confirmation ${chalk.dim('Examples:')} @@ -64,6 +66,7 @@ if (argv.help || ids.length === 0) { const debug = argv.debug const apiUrl = argv.url || 'https://api.zeit.co' const hard = argv.hard || false +const skipConfirmation = argv.yes || false if (argv.config) { cfg.setConfigFile(argv.config) @@ -78,7 +81,7 @@ function readConfirmation(matches) { const tbl = table( matches.map(depl => { const time = chalk.gray(ms(new Date() - depl.created) + ' ago') - const url = chalk.underline(`https://${depl.url}`) + const url = depl.url ? chalk.underline(`https://${depl.url}`) : '' return [depl.uid, url, time] }), {align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} @@ -94,7 +97,7 @@ function readConfirmation(matches) { } } - process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) + process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdin.on('data', d => { process.stdin.pause() @@ -125,7 +128,7 @@ async function remove(token) { const matches = deployments.filter(d => { return ids.find(id => { // `url` should match the hostname of the deployment - let u = id.replace(/^https\:\/\//i, '') + let u = id.replace(/^https:\/\//i, '') if (u.indexOf('.') === -1) { // `.now.sh` domain is implied if just the subdomain is given @@ -147,10 +150,13 @@ async function remove(token) { } try { - const confirmation = (await readConfirmation(matches)).toLowerCase() - if (confirmation !== 'y' && confirmation !== 'yes') { - console.log('\n> Aborted') - process.exit(0) + if (!skipConfirmation) { + const confirmation = (await readConfirmation(matches)).toLowerCase() + + if (confirmation !== 'y' && confirmation !== 'yes') { + console.log('\n> Aborted') + process.exit(0) + } } const start = new Date() diff --git a/bin/now-secrets.js b/bin/now-secrets.js index 1388164..4570c7f 100755 --- a/bin/now-secrets.js +++ b/bin/now-secrets.js @@ -1,17 +1,17 @@ #!/usr/bin/env node // Packages -import chalk from 'chalk' -import table from 'text-table' -import minimist from 'minimist' -import ms from 'ms' +const chalk = require('chalk') +const table = require('text-table') +const minimist = require('minimist') +const ms = require('ms') // Ours -import strlen from '../lib/strlen' -import * as cfg from '../lib/cfg' -import {handleError, error} from '../lib/error' -import NowSecrets from '../lib/secrets' -import login from '../lib/login' +const strlen = require('../lib/strlen') +const cfg = require('../lib/cfg') +const {handleError, error} = require('../lib/error') +const NowSecrets = require('../lib/secrets') +const login = require('../lib/login') const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], @@ -229,7 +229,7 @@ function readConfirmation(secret) { process.stdout.write('> The following secret will be removed permanently\n') process.stdout.write(' ' + tbl + '\n') - process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) + process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdin.on('data', d => { process.stdin.pause() diff --git a/bin/now.js b/bin/now.js index ba5050f..38d1641 100755 --- a/bin/now.js +++ b/bin/now.js @@ -1,34 +1,38 @@ #!/usr/bin/env node // Native -import {resolve} from 'path' +const {resolve} = require('path') // Packages -import minimist from 'minimist' -import {spawn} from 'cross-spawn' +const nodeVersion = require('node-version') +const updateNotifier = require('update-notifier') // Ours -import checkUpdate from '../lib/check-update' +const {error} = require('../lib/error') +const pkg = require('../package') -const argv = minimist(process.argv.slice(2)) - -// options -const debug = argv.debug || argv.d +// Support for keywords "async" and "await" +require('async-to-gen/register')({ + excludes: null +}) -// auto-update checking -const update = checkUpdate({debug}) +// Throw an error if node version is too low +if (nodeVersion.major < 6) { + error('Now requires at least version 6 of Node. Please upgrade!') + process.exit(1) +} -const exit = code => { - update.then(() => process.exit(code)) - // don't wait for updates more than a second - // when the process really wants to exit - setTimeout(() => process.exit(code), 1000) +// Only check for updates in the npm version +if (!process.pkg) { + updateNotifier({pkg}).notify() } +// This command will be run if no other sub command is specified const defaultCommand = 'deploy' const commands = new Set([ defaultCommand, + 'help', 'list', 'ls', 'rm', @@ -55,37 +59,33 @@ const aliases = new Map([ ['secret', 'secrets'] ]) -let cmd = argv._[0] -let args = [] +let cmd = defaultCommand +const args = process.argv.slice(2) +const index = args.findIndex(a => commands.has(a)) -if (cmd === 'help') { - cmd = argv._[1] +if (index > -1) { + cmd = args[index] + args.splice(index, 1) - if (!commands.has(cmd)) { - cmd = defaultCommand - } + if (cmd === 'help') { + if (index < args.length && commands.has(args[index])) { + cmd = args[index] + args.splice(index, 1) + } else { + cmd = defaultCommand + } - args.push('--help') -} + args.unshift('--help') + } -if (commands.has(cmd)) { cmd = aliases.get(cmd) || cmd - args = args.concat(process.argv.slice(3)) -} else { - cmd = defaultCommand - args = args.concat(process.argv.slice(2)) } -let bin = resolve(__dirname, 'now-' + cmd) -if (process.pkg) { - args.unshift('--entrypoint', bin) - bin = process.execPath -} +const bin = resolve(__dirname, 'now-' + cmd + '.js') -const proc = spawn(bin, args, { - stdio: 'inherit', - customFds: [0, 1, 2] -}) +// Prepare process.argv for subcommand +process.argv = process.argv.slice(0, 2).concat(args) -proc.on('close', code => exit(code)) -proc.on('error', () => exit(1)) +// Load sub command +// With custom parameter to make "pkg" happy +require(bin, 'may-exclude') diff --git a/gulpfile.babel.js b/gulpfile.babel.js deleted file mode 100644 index a85344e..0000000 --- a/gulpfile.babel.js +++ /dev/null @@ -1,31 +0,0 @@ -// Packages -import gulp from 'gulp' -import del from 'del' -import babel from 'gulp-babel' -import help from 'gulp-task-listing' -import {crop as cropExt} from 'gulp-ext' - -gulp.task('help', help) - -gulp.task('compile', [ - 'compile-lib', - 'compile-bin' -]) - -gulp.task('compile-lib', () => - gulp.src('lib/**/*.js') - .pipe(babel()) - .pipe(gulp.dest('build/lib'))) - -gulp.task('compile-bin', () => - gulp.src('bin/*') - .pipe(babel()) - .pipe(cropExt()) - .pipe(gulp.dest('build/bin'))) - -gulp.task('watch-lib', () => gulp.watch('lib/**/*.js', ['compile-lib'])) -gulp.task('watch-bin', () => gulp.watch('bin/*', ['compile-bin'])) -gulp.task('clean', () => del(['build'])) - -gulp.task('watch', ['watch-lib', 'watch-bin']) -gulp.task('default', ['compile', 'watch']) diff --git a/lib/agent.js b/lib/agent.js index 17f6fb3..7b03011 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,7 +1,7 @@ // Packages -import {parse} from 'url' -import http2 from 'spdy' -import fetch from 'node-fetch' +const {parse} = require('url') +const http2 = require('spdy') +const fetch = require('node-fetch') /** * Returns a `fetch` version with a similar @@ -14,7 +14,7 @@ import fetch from 'node-fetch' * @return {Function} fetch */ -export default class Agent { +module.exports = class Agent { constructor(url, {tls = true, debug} = {}) { this._url = url const parsed = parse(url) diff --git a/lib/alias.js b/lib/alias.js index fd3236f..51d9ce3 100644 --- a/lib/alias.js +++ b/lib/alias.js @@ -1,20 +1,31 @@ // Packages -import chalk from 'chalk' +const publicSuffixList = require('psl') +const minimist = require('minimist') +const chalk = require('chalk') // Ours -import toHost from './to-host' -import resolve4 from './dns' -import isZeitWorld from './is-zeit-world' -import {DOMAIN_VERIFICATION_ERROR} from './errors' -import Now from './' - +const copy = require('./copy') +const toHost = require('./to-host') +const resolve4 = require('./dns') +const isZeitWorld = require('./is-zeit-world') +const {DOMAIN_VERIFICATION_ERROR} = require('./errors') +const Now = require('./') + +const argv = minimist(process.argv.slice(2), { + boolean: ['no-clipboard'], + alias: {'no-clipboard': 'C'} +}) + +const isTTY = process.stdout.isTTY +const clipboard = !argv['no-clipboard'] const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/ -export default class Alias extends Now { +module.exports = class Alias extends Now { async ls(deployment) { if (deployment) { const target = await this.findDeployment(deployment) + if (!target) { const err = new Error(`Aliases not found by "${deployment}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`) err.userError = true @@ -63,6 +74,7 @@ export default class Alias extends Now { if (this._debug) { console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`) } + return true } @@ -71,6 +83,7 @@ export default class Alias extends Now { if (this._debug) { console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`) } + return true } @@ -118,21 +131,42 @@ export default class Alias extends Now { console.log(`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`) console.log(`> Verifying the DNS settings for ${chalk.bold(chalk.underline(alias))} (see ${chalk.underline('https://zeit.world')} for help)`) - const {domain, nameservers} = await this.getNameservers(alias) + const _domain = publicSuffixList.parse(alias).domain + const _domainInfo = await this.getDomain(_domain) + const domainInfo = _domainInfo && !_domainInfo.error ? _domainInfo : undefined + const {domain, nameservers} = domainInfo ? {domain: _domain} : await this.getNameservers(alias) + const usingZeitWorld = domainInfo ? !domainInfo.isExternal : isZeitWorld(nameservers) + let skipDNSVerification = false if (this._debug) { - console.log(`> [debug] Found domain ${domain} and nameservers ${nameservers}`) + if (domainInfo) { + console.log(`> [debug] Found domain ${domain} with verified:${domainInfo.verified}`) + } else { + console.log(`> [debug] Found domain ${domain} and nameservers ${nameservers}`) + } + } + + if (!usingZeitWorld && domainInfo) { + if (domainInfo.verified) { + skipDNSVerification = true + } else if (domainInfo.uid) { + const e = new Error(`> The domain ${domain} is already registered with now but additional verification is needed, please refer to 'now domain --help'.`) + e.userError = true + throw e + } } try { - await this.verifyOwnership(alias) + if (!skipDNSVerification) { + await this.verifyOwnership(alias) + } } catch (err) { if (err.userError) { // a user error would imply that verification failed // in which case we attempt to correct the dns // configuration (if we can!) try { - if (isZeitWorld(nameservers)) { + if (usingZeitWorld) { console.log(`> Detected ${chalk.bold(chalk.underline('zeit.world'))} nameservers! Configuring records.`) const record = alias.substr(0, alias.length - domain.length) @@ -178,12 +212,19 @@ export default class Alias extends Now { } } - if (!isZeitWorld(nameservers)) { + if (!usingZeitWorld && !skipDNSVerification) { if (this._debug) { console.log(`> [debug] Trying to register a non-ZeitWorld domain ${domain} for the current user`) } - await this.setupDomain(domain, {isExternal: true}) + const {uid, verified, verifyToken, created} = await this.setupDomain(domain, {isExternal: true}) + if (created && verified) { + console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domain))} ${chalk.dim(`(${uid})`)} added`) + } else if (verifyToken) { + const e = new Error(`> Verification required: Please add the following TXT record on the external DNS server: _now.${domain}: ${verifyToken}`) + e.userError = true + throw e + } } console.log(`> Verification ${chalk.bold('OK')}!`) @@ -196,9 +237,27 @@ export default class Alias extends Now { this._agent.close() this._agent._initAgent() - const {created, uid} = await this.createAlias(depl, alias) + const newAlias = await this.createAlias(depl, alias) + if (!newAlias) { + throw new Error(`Unexpected error occurred while setting up alias: ${JSON.stringify(newAlias)}`) + } + const {created, uid} = newAlias if (created) { - console.log(`${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}: ${chalk.bold(chalk.underline(`https://${alias}`))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}`) + const pretty = `https://${alias}` + const output = `${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}:\n${chalk.bold(chalk.underline(pretty))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}` + if (isTTY && clipboard) { + let append + try { + await copy(pretty) + append = '[copied to clipboard]' + } catch (err) { + append = '' + } finally { + console.log(`${output} ${append}`) + } + } else { + console.log(output) + } } else { console.log(`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`) } @@ -354,9 +413,12 @@ export default class Alias extends Now { if (this._debug) { console.log(`> [debug] No records found for "${domain}"`) } - } else { - throw err + + const err = new Error(DOMAIN_VERIFICATION_ERROR) + err.userError = true + return bail(err) } + throw err } if (ips.length <= 0) { diff --git a/lib/build-logger.js b/lib/build-logger.js index 6f8fdfe..81c2cb3 100644 --- a/lib/build-logger.js +++ b/lib/build-logger.js @@ -1,10 +1,10 @@ // Native -import EventEmitter from 'events' +const EventEmitter = require('events') // Packages -import ansi from 'ansi-escapes' -import io from 'socket.io-client' -import chalk from 'chalk' +const ansi = require('ansi-escapes') +const io = require('socket.io-client') +const chalk = require('chalk') class Lines { constructor(maxLines = 100) { @@ -30,7 +30,7 @@ class Lines { } } -export default class Logger extends EventEmitter { +module.exports = class Logger extends EventEmitter { constructor(host, {debug = false, quiet = false} = {}) { super() this.host = host diff --git a/lib/certs.js b/lib/certs.js index 2646fa9..46d78ed 100644 --- a/lib/certs.js +++ b/lib/certs.js @@ -1,7 +1,7 @@ // Ours -import Now from '../lib' +const Now = require('../lib') -export default class Certs extends Now { +module.exports = class Certs extends Now { ls() { return this.retry(async (bail, attempt) => { diff --git a/lib/cfg.js b/lib/cfg.js index c238730..781be23 100644 --- a/lib/cfg.js +++ b/lib/cfg.js @@ -1,17 +1,17 @@ // Native -import {homedir} from 'os' -import path from 'path' +const {homedir} = require('os') +const path = require('path') // Packages -import fs from 'fs-promise' +const fs = require('fs-promise') let file = process.env.NOW_JSON ? path.resolve(process.env.NOW_JSON) : path.resolve(homedir(), '.now.json') -export function setConfigFile(nowjson) { +function setConfigFile(nowjson) { file = path.resolve(nowjson) } -export function read() { +function read() { let existing = null try { existing = fs.readFileSync(file, 'utf8') @@ -28,7 +28,13 @@ export function read() { * @param {Object} data */ -export function merge(data) { +function merge(data) { const cfg = Object.assign({}, read(), data) fs.writeFileSync(file, JSON.stringify(cfg, null, 2)) } + +module.exports = { + setConfigFile, + read, + merge +} diff --git a/lib/check-update.js b/lib/check-update.js deleted file mode 100644 index 23c6579..0000000 --- a/lib/check-update.js +++ /dev/null @@ -1,95 +0,0 @@ -// Packages -import ms from 'ms' -import fetch from 'node-fetch' -import chalk from 'chalk' -import compare from 'semver-compare' - -// Ours -import pkg from '../../package' - -const isTTY = process.stdout.isTTY - -// if we're not in a tty the update checker -// will always return a resolved promise -const resolvedPromise = new Promise(resolve => resolve()) - -/** - * Configures auto updates. - * Sets up a `exit` listener to report them. - */ - -export default function checkUpdate(opts = {}) { - if (!isTTY) { - // don't attempt to check for updates - // if the user is piping or redirecting - return resolvedPromise - } - - let updateData - - const update = check(opts).then(data => { - updateData = data - - // forces the `exit` event upon Ctrl + C - process.on('SIGINT', () => { - // clean up output after ^C - process.stdout.write('\n') - process.exit(1) - }) - }, err => console.error(err.stack)) - - process.on('exit', () => { - if (updateData) { - const {current, latest, at} = updateData - const ago = ms(Date.now() - at) - console.log(`> ${chalk.white.bgRed('UPDATE NEEDED')} ` + - `Current: ${current} – ` + - `Latest ${chalk.bold(latest)} (released ${ago} ago)`) - console.log('> Run `npm install -g now` to update') - } - }) - - return update -} - -function check({debug = false}) { - return new Promise(resolve => { - if (debug) { - console.log('> [debug] Checking for updates.') - } - - fetch('https://registry.npmjs.org/now').then(res => { - if (res.status !== 200) { - if (debug) { - console.log(`> [debug] Update check error. NPM ${res.status}.`) - } - - resolve(false) - return - } - - res.json().then(data => { - const {latest} = data['dist-tags'] - const current = pkg.version - - if (compare(latest, pkg.version) === 1) { - if (debug) { - console.log(`> [debug] Needs update. Current ${current}, latest ${latest}`) - } - - resolve({ - latest, - current, - at: new Date(data.time[latest]) - }) - } else { - if (debug) { - console.log(`> [debug] Up to date (${pkg.version}).`) - } - - resolve(false) - } - }, () => resolve(false)) - }, () => resolve(false)) - }) -} diff --git a/lib/copy.js b/lib/copy.js index 0d7eb87..63f3b22 100644 --- a/lib/copy.js +++ b/lib/copy.js @@ -1,7 +1,7 @@ // Packages -import {copy as _copy} from 'copy-paste' +const {copy: _copy} = require('copy-paste') -export default function copy(text) { +function copy(text) { return new Promise((resolve, reject) => { _copy(text, err => { if (err) { @@ -12,3 +12,5 @@ export default function copy(text) { }) }) } + +module.exports = copy diff --git a/lib/dns.js b/lib/dns.js index 16a3345..124840a 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -1,7 +1,7 @@ // Packages -import dns from 'dns' +const dns = require('dns') -export default function resolve4(host) { +function resolve4(host) { return new Promise((resolve, reject) => { return dns.resolve4(host, (err, answer) => { if (err) { @@ -12,3 +12,4 @@ export default function resolve4(host) { }) }) } +module.exports = resolve4 diff --git a/lib/domain-records.js b/lib/domain-records.js index 3d76524..fbc14f1 100644 --- a/lib/domain-records.js +++ b/lib/domain-records.js @@ -1,7 +1,7 @@ // Ours -import Now from '../lib' +const Now = require('../lib') -export default class DomainRecords extends Now { +module.exports = class DomainRecords extends Now { async getRecord(id) { const all = (await this.ls()).entries() diff --git a/lib/domains.js b/lib/domains.js index 74d18df..921da36 100644 --- a/lib/domains.js +++ b/lib/domains.js @@ -1,14 +1,14 @@ // Packages -import chalk from 'chalk' +const chalk = require('chalk') // Ours -import Now from '../lib' -import isZeitWorld from './is-zeit-world' -import {DNS_VERIFICATION_ERROR} from './errors' +const Now = require('../lib') +const isZeitWorld = require('./is-zeit-world') +const {DNS_VERIFICATION_ERROR} = require('./errors') const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/ -export default class Domains extends Now { +module.exports = class Domains extends Now { async ls() { return await this.listDomains() @@ -37,15 +37,15 @@ export default class Domains extends Now { }) } - async add(domain, skipVerification) { + async add(domain, skipVerification, isExternal) { if (!domainRegex.test(domain)) { const err = new Error(`The supplied value ${chalk.bold(`"${domain}"`)} is not a valid domain.`) err.userError = true throw err } - if (skipVerification) { - return this.setupDomain(domain) + if (skipVerification || isExternal) { + return this.setupDomain(domain, {isExternal}) } let ns diff --git a/lib/error.js b/lib/error.js index 9153d46..6fc7c59 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,8 +1,8 @@ // Packages -import ms from 'ms' -import chalk from 'chalk' +const ms = require('ms') +const chalk = require('chalk') -export function handleError(err) { +function handleError(err) { if (err.status === 403) { error('Authentication error. Run `now -L` or `now --login` to log-in again.') } else if (err.status === 429) { @@ -22,6 +22,11 @@ export function handleError(err) { } } -export function error(err) { +function error(err) { console.error(`> ${chalk.red('Error!')} ${err}`) } + +module.exports = { + handleError, + error +} diff --git a/lib/errors.js b/lib/errors.js index 40d1ea7..78ef0f2 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,12 +1,17 @@ // Packages -import chalk from 'chalk' +const chalk = require('chalk') -export const DNS_VERIFICATION_ERROR = `Please make sure that your nameservers point to ${chalk.underline('zeit.world')}. +const DNS_VERIFICATION_ERROR = `Please make sure that your nameservers point to ${chalk.underline('zeit.world')}. > Examples: (full list at ${chalk.underline('https://zeit.world')}) > ${chalk.gray('-')} ${chalk.underline('california.zeit.world')} ${chalk.dim('173.255.215.107')} > ${chalk.gray('-')} ${chalk.underline('newark.zeit.world')} ${chalk.dim('173.255.231.87')} > ${chalk.gray('-')} ${chalk.underline('london.zeit.world')} ${chalk.dim('178.62.47.76')} > ${chalk.gray('-')} ${chalk.underline('singapore.zeit.world')} ${chalk.dim('119.81.97.170')}` -export const DOMAIN_VERIFICATION_ERROR = DNS_VERIFICATION_ERROR + +const DOMAIN_VERIFICATION_ERROR = DNS_VERIFICATION_ERROR + `\n> Alternatively, ensure it resolves to ${chalk.underline('alias.zeit.co')} via ${chalk.dim('CNAME')} / ${chalk.dim('ALIAS')}.` + +module.exports = { + DNS_VERIFICATION_ERROR, + DOMAIN_VERIFICATION_ERROR +} diff --git a/lib/get-files.js b/lib/get-files.js index becb1cb..fa2ebdf 100644 --- a/lib/get-files.js +++ b/lib/get-files.js @@ -1,15 +1,15 @@ // Native -import {resolve} from 'path' +const {resolve} = require('path') // Packages -import flatten from 'arr-flatten' -import unique from 'array-unique' -import ignore from 'ignore' -import _glob from 'glob' -import {stat, readdir, readFile} from 'fs-promise' +const flatten = require('arr-flatten') +const unique = require('array-unique') +const ignore = require('ignore') +const _glob = require('glob') +const {stat, readdir, readFile} = require('fs-promise') // Ours -import IGNORED from './ignored' +const IGNORED = require('./ignored') /** * Returns a list of files in the given @@ -24,14 +24,11 @@ import IGNORED from './ignored' * @return {Array} comprehensive list of paths to sync */ -export async function npm(path, pkg, { +async function npm(path, pkg, { limit = null, debug = false } = {}) { const whitelist = pkg.now && pkg.now.files ? pkg.now.files : pkg.files - - // the package.json `files` whitelist still - // honors ignores: https://docs.npmjs.com/files/package.json#files const search_ = whitelist || ['.'] // always include the "main" file @@ -44,27 +41,38 @@ export async function npm(path, pkg, { // compile list of ignored patterns and files const npmIgnore = await maybeRead(resolve(path, '.npmignore'), null) + const gitIgnore = npmIgnore === null ? + await maybeRead(resolve(path, '.gitignore')) : + null const filter = ignore().add( IGNORED + '\n' + - clearRelative(npmIgnore === null ? await maybeRead(resolve(path, '.gitignore')) : npmIgnore) + clearRelative(npmIgnore === null ? gitIgnore : npmIgnore) ).createFilter() const prefixLength = path.length + 1 - const accepts = function (file) { - const relativePath = file.substr(prefixLength) - if (relativePath === '') { - return true - } + // the package.json `files` whitelist still + // honors npmignores: https://docs.npmjs.com/files/package.json#files + // but we don't ignore if the user is explicitly listing files + // under the now namespace, or using files in combination with gitignore + const overrideIgnores = (pkg.now && pkg.now.files) || (gitIgnore !== null && pkg.files) + const accepts = overrideIgnores ? + () => true : + file => { + const relativePath = file.substr(prefixLength) + + if (relativePath === '') { + return true + } - const accepted = filter(relativePath) - if (!accepted && debug) { - console.log('> [debug] ignoring "%s"', file) + const accepted = filter(relativePath) + if (!accepted && debug) { + console.log('> [debug] ignoring "%s"', file) + } + return accepted } - return accepted - } // locate files if (debug) { @@ -118,7 +126,7 @@ const asAbsolute = function (path, parent) { * @return {Array} comprehensive list of paths to sync */ -export async function docker(path, { +async function docker(path, { limit = null, debug = false } = {}) { @@ -269,3 +277,8 @@ const maybeRead = async function (path, default_ = '') { const clearRelative = function (str) { return str.replace(/(\n|^)\.\//g, '$1') } + +module.exports = { + npm, + docker +} diff --git a/lib/git.js b/lib/git.js new file mode 100644 index 0000000..4ef6337 --- /dev/null +++ b/lib/git.js @@ -0,0 +1,211 @@ +// Native +const path = require('path') +const url = require('url') +const childProcess = require('child_process') + +// Packages +const fs = require('fs-promise') +const download = require('download') +const tmp = require('tmp-promise') +const isURL = require('is-url') + +const cloneRepo = (parts, tmpDir) => new Promise((resolve, reject) => { + let host + + switch (parts.type) { + case 'GitLab': + host = `gitlab.com` + break + case 'Bitbucket': + host = `bitbucket.org` + break + default: + host = `github.com` + } + + const url = `https://${host}/${parts.main}` + const ref = parts.ref || (parts.type === 'Bitbucket' ? 'default' : 'master') + const cmd = `git clone ${url} --single-branch ${ref}` + + childProcess.exec(cmd, {cwd: tmpDir.path}, (err, stdout) => { + if (err) { + reject(err) + } + + resolve(stdout) + }) +}) + +const renameRepoDir = async (pathParts, tmpDir) => { + const tmpContents = await fs.readdir(tmpDir.path) + + const oldTemp = path.join(tmpDir.path, tmpContents[0]) + const newTemp = path.join(tmpDir.path, pathParts.main.replace('/', '-')) + + await fs.rename(oldTemp, newTemp) + tmpDir.path = newTemp + + return tmpDir +} + +const downloadRepo = async repoPath => { + const pathParts = gitPathParts(repoPath) + + const tmpDir = await tmp.dir({ + // We'll remove it manually once deployment is done + keep: true, + // Recursively remove directory when calling respective method + unsafeCleanup: true + }) + + let gitInstalled = true + + try { + await cloneRepo(pathParts, tmpDir) + } catch (err) { + gitInstalled = false + } + + if (gitInstalled) { + return await renameRepoDir(pathParts, tmpDir) + } + + let url + + switch (pathParts.type) { + case 'GitLab': { + const ref = pathParts.ref ? `?ref=${pathParts.ref}` : '' + url = `https://gitlab.com/${pathParts.main}/repository/archive.tar` + ref + break + } + case 'Bitbucket': + url = `https://bitbucket.org/${pathParts.main}/get/${pathParts.ref || 'default'}.zip` + break + default: + url = `https://api.github.com/repos/${pathParts.main}/tarball/${pathParts.ref}` + } + + try { + await download(url, tmpDir.path, { + extract: true + }) + } catch (err) { + tmpDir.cleanup() + return false + } + + return await renameRepoDir(pathParts, tmpDir) +} + +const capitalizePlatform = name => { + const names = { + github: 'GitHub', + gitlab: 'GitLab', + bitbucket: 'Bitbucket' + } + + return names[name] +} + +const splittedURL = fullURL => { + const parsedURL = url.parse(fullURL) + const pathParts = parsedURL.path.split('/') + + pathParts.shift() + + // Set path to repo... + const main = pathParts[0] + '/' + pathParts[1] + + // ...and then remove it from the parts + pathParts.splice(0, 2) + + // Assign Git reference + let ref = pathParts.length >= 2 ? pathParts[1] : '' + + // Firstly be sure that we haven know the ref type + if (pathParts[0]) { + // Then shorten the SHA of the commit + if (pathParts[0] === 'commit' || pathParts[0] === 'commits') { + ref = ref.substring(0, 7) + } + } + + // We're deploying master by default, + // so there's no need to indicate it explicitly + if (ref === 'master') { + ref = '' + } + + return { + main, + ref, + type: capitalizePlatform(parsedURL.host.split('.')[0]) + } +} + +const gitPathParts = main => { + let ref = '' + + if (isURL(main)) { + return splittedURL(main) + } + + if (main.split('/')[1].includes('#')) { + const parts = main.split('#') + + ref = parts[1] + main = parts[0] + } + + return { + main, + ref, + type: capitalizePlatform('github') + } +} + +const isRepoPath = path => { + if (!path) { + return false + } + + const allowedHosts = [ + 'github.com', + 'gitlab.com', + 'bitbucket.org' + ] + + if (isURL(path)) { + const urlParts = url.parse(path) + const slashSplitted = urlParts.path.split('/').filter(n => n) + const notBare = slashSplitted.length >= 2 + + if (allowedHosts.includes(urlParts.host) && notBare) { + return true + } + + return 'no-valid-url' + } + + return /[^\s\\]\/[^\s\\]/g.test(path) +} + +const fromGit = async (path, debug) => { + let tmpDir = false + + try { + tmpDir = await downloadRepo(path) + } catch (err) { + if (debug) { + console.log(`Could not download "${path}" repo from GitHub`) + } + } + + return tmpDir +} + +module.exports = { + gitPathParts, + isRepoPath, + fromGit +} diff --git a/lib/hash.js b/lib/hash.js index ef625f0..9cd1cab 100644 --- a/lib/hash.js +++ b/lib/hash.js @@ -1,9 +1,9 @@ // Native -import {createHash} from 'crypto' -import path from 'path' +const {createHash} = require('crypto') +const path = require('path') // Packages -import {readFile} from 'fs-promise' +const {readFile} = require('fs-promise') /** * Computes hashes for the contents of each file given. @@ -12,7 +12,7 @@ import {readFile} from 'fs-promise' * @return {Map} */ -export default async function hashes(files, isStatic, pkg) { +async function hashes(files, isStatic, pkg) { const map = new Map() await Promise.all(files.map(async name => { @@ -49,3 +49,5 @@ function hash(buf) { .update(buf) .digest('hex') } + +module.exports = hashes diff --git a/lib/ignored.js b/lib/ignored.js index be110fd..b623513 100644 --- a/lib/ignored.js +++ b/lib/ignored.js @@ -1,6 +1,6 @@ // base `.gitignore` to which we add entries // supplied by the user -export default `.hg +module.exports = `.hg .git .gitmodules .svn diff --git a/lib/indent.js b/lib/indent.js index a1fa3be..f934288 100644 --- a/lib/indent.js +++ b/lib/indent.js @@ -1,3 +1,5 @@ -export default function indent(text, n) { +function indent(text, n) { return text.split('\n').map(l => ' '.repeat(n) + l).join('\n') } + +module.exports = indent diff --git a/lib/index.js b/lib/index.js index d97609b..4cd6e99 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,23 +1,23 @@ // Native -import {homedir} from 'os' -import {resolve as resolvePath, join as joinPaths} from 'path' -import EventEmitter from 'events' +const {homedir} = require('os') +const {resolve: resolvePath, join: joinPaths} = require('path') +const EventEmitter = require('events') // Packages -import bytes from 'bytes' -import chalk from 'chalk' -import retry from 'async-retry' -import {parse as parseIni} from 'ini' -import {readFile} from 'fs-promise' -import resumer from 'resumer' -import splitArray from 'split-array' +const bytes = require('bytes') +const chalk = require('chalk') +const resumer = require('resumer') +const retry = require('async-retry').default +const splitArray = require('split-array') +const {parse: parseIni} = require('ini') +const {readFile, stat, lstat} = require('fs-promise') // Ours -import {npm as getNpmFiles, docker as getDockerFiles} from './get-files' -import ua from './ua' -import hash from './hash' -import Agent from './agent' -import readMetaData from './read-metadata' +const {npm: getNpmFiles, docker: getDockerFiles} = require('./get-files') +const ua = require('./ua') +const hash = require('./hash') +const Agent = require('./agent') +const readMetaData = require('./read-metadata') // how many concurrent HTTP/2 stream uploads const MAX_CONCURRENT = 10 @@ -26,7 +26,7 @@ const MAX_CONCURRENT = 10 const IS_WIN = /^win/.test(process.platform) const SEP = IS_WIN ? '\\' : '/' -export default class Now extends EventEmitter { +module.exports = class Now extends EventEmitter { constructor(url, token, {forceNew = false, debug = false}) { super() this._token = token @@ -40,10 +40,12 @@ export default class Now extends EventEmitter { wantsPublic, quiet = false, env = {}, + followSymlinks = true, forceNew = false, forceSync = false, forwardNpm = false, deploymentType = 'npm', + deploymentName, isStatic = false }) { this._path = path @@ -53,6 +55,7 @@ export default class Now extends EventEmitter { const {pkg, name, description} = await readMetaData(path, { deploymentType, + deploymentName, quiet, isStatic }) @@ -122,6 +125,39 @@ export default class Now extends EventEmitter { console.time('> [debug] /now/create') } + // Flatten the array to contain files to sync where each nested input + // array has a group of files with the same sha but different path + const files = await Promise.all(Array.prototype.concat.apply([], await Promise.all((Array.from(this._files)).map(async ([sha, {data, names}]) => { + const statFn = followSymlinks ? stat : lstat + + return await names.map(async name => { + let mode + + const getMode = async () => { + const st = await statFn(name) + return st.mode + } + + if (this._static) { + if (toRelative(name, this._path) === 'package.json') { + mode = 33261 + } else { + mode = await getMode() + name = this.pathInsideContent(name) + } + } else { + mode = await getMode() + } + + return { + sha, + size: data.length, + file: toRelative(name, this._path), + mode + } + }) + })))) + const res = await this._fetch('/now/create', { method: 'POST', body: { @@ -133,21 +169,7 @@ export default class Now extends EventEmitter { description, deploymentType, registryAuthToken: authToken, - // Flatten the array to contain files to sync where each nested input - // array has a group of files with the same sha but different path - files: Array.prototype.concat.apply([], Array.from(this._files).map(([sha, {data, names}]) => { - return names.map(n => { - if (this._static && toRelative(n, this._path) !== 'package.json') { - n = this.pathInsideContent(n) - } - - return { - sha, - size: data.length, - file: toRelative(n, this._path) - } - }) - })), + files, engines } }) @@ -208,7 +230,7 @@ export default class Now extends EventEmitter { } } - if (!quiet && deployment.nodeVersion) { + if (!quiet && deploymentType === 'npm' && deployment.nodeVersion) { if (engines && engines.node) { if (missingVersion) { console.log(`> Using Node.js ${chalk.bold(deployment.nodeVersion)} (default)`) @@ -385,8 +407,24 @@ export default class Now extends EventEmitter { }) } + async getDomain(domain) { + return this.retry(async (bail, attempt) => { + if (this._debug) { + console.time(`> [debug] #${attempt} GET /domains/${domain}`) + } + + const res = await this._fetch(`/domains/${domain}`) + + if (this._debug) { + console.timeEnd(`> [debug] #${attempt} GET /domains/${domain}`) + } + + return await res.json() + }) + } + getNameservers(domain) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { let fallback = false this.retry(async (bail, attempt) => { @@ -427,6 +465,8 @@ export default class Now extends EventEmitter { return ns.length }) resolve(body) + }).catch(err => { + reject(err) }) }) } @@ -447,8 +487,9 @@ export default class Now extends EventEmitter { console.timeEnd(`> [debug] #${attempt} POST /domains`) } + const body = await res.json() + if (res.status === 403) { - const body = await res.json() const code = body.error.code let err @@ -462,15 +503,13 @@ export default class Now extends EventEmitter { return bail(err) } - const body = await res.json() - // domain already exists if (res.status === 409) { if (this._debug) { console.log('> [debug] Domain already exists (noop)') } - return {uid: body.error.uid} + return {uid: body.error.uid, code: body.error.code} } if (res.status !== 200) { diff --git a/lib/is-zeit-world.js b/lib/is-zeit-world.js index 51290a8..d40ef43 100644 --- a/lib/is-zeit-world.js +++ b/lib/is-zeit-world.js @@ -20,8 +20,10 @@ const nameservers = new Set([ * by `resolveNs` from Node, assert that they're * zeit.world's. */ -export default function isZeitWorld(ns) { +function isZeitWorld(ns) { return ns.length > 1 && ns.every(host => { return nameservers.has(host) }) } + +module.exports = isZeitWorld diff --git a/lib/login.js b/lib/login.js index 2de8df3..c97ed03 100644 --- a/lib/login.js +++ b/lib/login.js @@ -1,17 +1,18 @@ // Native -import os from 'os' +const os = require('os') // Packages -import {stringify as stringifyQuery} from 'querystring' -import chalk from 'chalk' -import fetch from 'node-fetch' -import {validate} from 'email-validator' -import readEmail from 'email-prompt' +const {stringify: stringifyQuery} = require('querystring') +const chalk = require('chalk') +const fetch = require('node-fetch') +const {validate} = require('email-validator') +const readEmail = require('email-prompt') +const ora = require('ora') // Ours -import pkg from '../../package' -import ua from './ua' -import * as cfg from './cfg' +const pkg = require('../package') +const ua = require('./ua') +const cfg = require('./cfg') async function getVerificationData(url, email) { const tokenName = `Now CLI ${os.platform()}-${os.arch()} ${pkg.version} (${os.hostname()})` @@ -54,7 +55,14 @@ function sleep(ms) { } async function register(url, {retryEmail = false} = {}) { - const email = await readEmail({invalid: retryEmail}) + let email + try { + email = await readEmail({invalid: retryEmail}) + } catch (err) { + process.stdout.write('\n') + throw err + } + process.stdout.write('\n') if (!validate(email)) { @@ -62,13 +70,18 @@ async function register(url, {retryEmail = false} = {}) { } const {token, securityCode} = await getVerificationData(url, email) - console.log(`> Please follow the link sent to ${chalk.bold(email)} to log in.`) + if (securityCode) { console.log(`> Verify that the provided security code in the email matches ${chalk.cyan(chalk.bold(securityCode))}.`) } - process.stdout.write('> Waiting for confirmation..') + process.stdout.write('\n') + + const spinner = ora({ + text: 'Waiting for confirmation...', + color: 'black' + }).start() let final @@ -78,16 +91,17 @@ async function register(url, {retryEmail = false} = {}) { try { final = await verify(url, email, token) } catch (err) {} - - process.stdout.write('.') } while (!final) + spinner.text = 'Confirmed email address!' + spinner.stopAndPersist('✔') + process.stdout.write('\n') return {email, token: final} } -export default async function (url) { +module.exports = async function (url) { const loginData = await register(url) cfg.merge(loginData) return loginData.token diff --git a/lib/read-metadata.js b/lib/read-metadata.js index c76d5d4..9000be4 100644 --- a/lib/read-metadata.js +++ b/lib/read-metadata.js @@ -1,20 +1,20 @@ -import {basename, resolve as resolvePath} from 'path' -import chalk from 'chalk' -import {readFile} from 'fs-promise' -import {parse as parseDockerfile} from 'docker-file-parser' +const {basename, resolve: resolvePath} = require('path') +const chalk = require('chalk') +const {readFile} = require('fs-promise') +const {parse: parseDockerfile} = require('docker-file-parser') const listPackage = { - version: '0.0.0', scripts: { - start: 'list ./content' + start: 'serve ./content' }, dependencies: { - list: 'latest' + serve: '^2.4.1' } } -export default async function (path, { +module.exports = async function (path, { deploymentType = 'npm', + deploymentName, quiet = false, isStatic = false }) { @@ -44,13 +44,15 @@ export default async function (path, { throw e } - if (typeof pkg.name === 'string') { - name = pkg.name - } else { - name = basename(path) + if (!deploymentName) { + if (typeof pkg.name === 'string') { + name = pkg.name + } else { + name = basename(path) - if (!quiet && !isStatic) { - console.log(`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`) + if (!quiet && !isStatic) { + console.log(`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`) + } } } @@ -106,19 +108,25 @@ export default async function (path, { } }) - if (labels.name) { - name = labels.name - } else { - name = basename(path) + if (!deploymentName) { + if (labels.name) { + name = labels.name + } else { + name = basename(path) - if (!quiet) { - console.log(`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`) + if (!quiet) { + console.log(`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`) + } } } description = labels.description } + if (deploymentName) { + name = deploymentName + } + return { name, description, diff --git a/lib/secrets.js b/lib/secrets.js index 9b5105e..6b0451d 100644 --- a/lib/secrets.js +++ b/lib/secrets.js @@ -1,7 +1,7 @@ // Ours -import Now from '../lib' +const Now = require('../lib') -export default class Secrets extends Now { +module.exports = class Secrets extends Now { ls() { return this.listSecrets() } diff --git a/lib/strlen.js b/lib/strlen.js index 1817bea..1080706 100644 --- a/lib/strlen.js +++ b/lib/strlen.js @@ -1,3 +1,5 @@ -export default function strlen(str) { +function strlen(str) { return str.replace(/\x1b[^m]*m/g, '').length } + +module.exports = strlen diff --git a/lib/test.js b/lib/test.js index f9f0658..fae66c5 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,8 +1,8 @@ // Native -import {resolve} from 'path' +const {resolve} = require('path') // Ours -import {npm as getFiles} from './get-files' +const {npm: getFiles} = require('./get-files') getFiles(resolve('../mng-test/files-in-package')) .then(files => { diff --git a/lib/to-host.js b/lib/to-host.js index 1737102..daae30a 100644 --- a/lib/to-host.js +++ b/lib/to-host.js @@ -1,5 +1,5 @@ // Native -import {parse} from 'url' +const {parse} = require('url') /** * Converts a valid deployment lookup parameter to a hostname. @@ -7,12 +7,14 @@ import {parse} from 'url' * google.com => google.com */ -export default function toHost(url) { +function toHost(url) { if (/^https?:\/\//.test(url)) { return parse(url).host } // remove any path if present // `a.b.c/` => `a.b.c` - return url.replace(/(\/\/)?([^\/]+)(.*)/, '$2') + return url.replace(/(\/\/)?([^/]+)(.*)/, '$2') } + +module.exports = toHost diff --git a/lib/ua.js b/lib/ua.js index 3ec9f83..28712e9 100644 --- a/lib/ua.js +++ b/lib/ua.js @@ -1,7 +1,7 @@ // Native -import os from 'os' +const os = require('os') // Ours -import {version} from '../../package' +const {version} = require('../package') -export default `now ${version} node-${process.version} ${os.platform()} (${os.arch()})` +module.exports = `now ${version} node-${process.version} ${os.platform()} (${os.arch()})` diff --git a/lib/utils/check-path.js b/lib/utils/check-path.js new file mode 100644 index 0000000..8d71267 --- /dev/null +++ b/lib/utils/check-path.js @@ -0,0 +1,49 @@ +// Native +const os = require('os') +const path = require('path') + +const checkPath = async dir => { + if (!dir) { + return + } + + const home = os.homedir() + let location + + const paths = { + home, + desktop: path.join(home, 'Desktop'), + downloads: path.join(home, 'Downloads') + } + + for (const locationPath in paths) { + if (!{}.hasOwnProperty.call(paths, locationPath)) { + continue + } + + if (dir === paths[locationPath]) { + location = locationPath + } + } + + if (!location) { + return + } + + let locationName + + switch (location) { + case 'home': + locationName = 'user directory' + break + case 'downloads': + locationName = 'downloads directory' + break + default: + locationName = location + } + + throw new Error(`You're trying to deploy your ${locationName}.`) +} + +module.exports = checkPath diff --git a/lib/utils/prompt-options.js b/lib/utils/prompt-options.js index f1a09c3..b9db67e 100644 --- a/lib/utils/prompt-options.js +++ b/lib/utils/prompt-options.js @@ -1,6 +1,7 @@ -import chalk from 'chalk' +// Packages +const chalk = require('chalk') -export default function (opts) { +module.exports = function (opts) { return new Promise((resolve, reject) => { opts.forEach(([, text], i) => { console.log(`${chalk.gray('>')} [${chalk.bold(i + 1)}] ${text}`) diff --git a/lib/utils/to-human-path.js b/lib/utils/to-human-path.js index c22892b..282ec5f 100644 --- a/lib/utils/to-human-path.js +++ b/lib/utils/to-human-path.js @@ -1,5 +1,6 @@ -import {resolve} from 'path' -import {homedir} from 'os' +// Native +const {resolve} = require('path') +const {homedir} = require('os') // cleaned-up `$HOME` (i.e.: no trailing slash) const HOME = resolve(homedir()) @@ -10,6 +11,8 @@ const HOME = resolve(homedir()) * `/Users/rauchg/test.js` becomes `~/test.js` */ -export default function toHumanPath(path) { +function toHumanPath(path) { return path.replace(HOME, '~') } + +module.exports = toHumanPath diff --git a/package.json b/package.json index ccda899..0c91755 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,25 @@ { "name": "now", - "version": "0.30.0", - "description": "Realtime global deployments", - "repository": "zeit/now", - "main": "./build/lib/index", + "version": "2.0.5", + "description": "The command line interface for Now", + "repository": "zeit/now-cli", "license": "MIT", "files": [ - "build" + "bin", + "lib" ], "scripts": { - "start": "gulp", "test": "xo && ava", - "prepublish": "gulp compile", - "pkg": "pkg . --out-dir out" + "pack": "pkg . --out-dir packed" }, "pkg": { - "scripts": "build/**/*" + "scripts": [ + "./bin/*", + "./lib/**/*" + ] + }, + "bin": { + "now": "./bin/now.js" }, "ava": { "failFast": true, @@ -23,88 +27,75 @@ "test/*.js" ], "require": [ - "babel-register" + "async-to-gen/register" ] }, "greenkeeper": { "emails": false }, - "babel": { - "presets": [ - "es2015" - ], - "plugins": [ - "transform-runtime", - "transform-async-to-generator" - ] - }, "xo": { "esnext": true, "space": true, "semicolon": false, "ignores": [ - "build/**", - "out/**", + "packed/**", "test/_fixtures/**" ], "rules": { + "import/no-unassigned-import": 0, + "import/no-dynamic-require": 0, "import/no-unresolved": 0, - "ava/no-ignored-test-files": 0, "max-depth": 0, "no-use-before-define": 0, "complexity": 0, - "unicorn/no-process-exit": 0, - "no-control-regex": 0, - "no-case-declarations": 0, - "no-useless-escape": 0 + "no-control-regex": 0 } }, - "bin": { - "now": "./build/bin/now" + "engines": { + "node": ">=6.9.0" }, "dependencies": { - "ansi-escapes": "1.4.0", - "arr-flatten": "1.0.1", - "array-unique": "0.3.2", - "async-retry": "0.2.1", - "babel-runtime": "6.18.0", - "bytes": "2.4.0", - "chalk": "1.1.3", - "copy-paste": "1.3.0", - "cross-spawn": "5.0.1", - "docker-file-parser": "0.1.0", - "email-prompt": "0.1.8", - "email-validator": "1.0.7", - "fs-promise": "1.0.0", - "glob": "7.1.1", - "graceful-fs": "4.1.11", - "ignore": "3.2.0", - "ini": "1.3.4", - "minimist": "1.2.0", - "ms": "0.7.2", - "node-fetch": "1.6.3", - "progress": "1.1.8", - "resumer": "0.0.0", - "semver-compare": "1.0.0", - "socket.io-client": "1.6.0", - "spdy": "3.4.4", - "split-array": "1.0.1", - "text-table": "0.2.0" + "ansi-escapes": "^1.4.0", + "arr-flatten": "^1.0.1", + "array-unique": "^0.3.2", + "async-retry": "^0.2.1", + "async-to-gen": "^1.3.0", + "bytes": "^2.4.0", + "chalk": "^1.1.3", + "copy-paste": "^1.3.0", + "cross-spawn": "^5.0.1", + "docker-file-parser": "^0.1.0", + "download": "^5.0.2", + "email-prompt": "^0.2.0", + "email-validator": "^1.0.7", + "fs-promise": "^1.0.0", + "glob": "^7.1.1", + "graceful-fs": "^4.1.11", + "ignore": "^3.2.0", + "ini": "^1.3.4", + "is-url": "^1.2.2", + "minimist": "^1.2.0", + "ms": "^0.7.2", + "node-fetch": "^1.6.3", + "node-version": "^1.0.0", + "ora": "^1.0.0", + "progress": "^1.1.8", + "psl": "^1.1.15", + "resumer": "^0.0.0", + "semver-compare": "^1.0.0", + "socket.io-client": "^1.7.2", + "spdy": "^3.4.4", + "split-array": "^1.0.1", + "text-table": "^0.2.0", + "tmp-promise": "^1.0.3", + "update-notifier": "^1.0.3" }, "devDependencies": { - "alpha-sort": "1.0.2", - "ava": "0.17.0", - "babel-plugin-transform-async-to-generator": "6.16.0", - "babel-plugin-transform-runtime": "6.15.0", - "babel-preset-es2015": "6.18.0", - "babel-register": "6.18.0", - "del": "2.2.2", - "estraverse-fb": "1.3.1", - "gulp": "3.9.1", - "gulp-babel": "6.1.2", - "gulp-ext": "1.0.0", - "gulp-task-listing": "1.0.1", - "pkg": "3.0.0-beta.21", - "xo": "0.17.1" + "alpha-sort": "^2.0.0", + "ava": "^0.17.0", + "del": "^2.2.2", + "estraverse-fb": "^1.3.1", + "pkg": "^3.0.0-beta.22", + "xo": "^0.17.1" } } diff --git a/test/_fixtures/files-overrides-gitignore/.gitignore b/test/_fixtures/files-overrides-gitignore/.gitignore new file mode 100644 index 0000000..db54e80 --- /dev/null +++ b/test/_fixtures/files-overrides-gitignore/.gitignore @@ -0,0 +1,2 @@ +ignore-me.js +test.json diff --git a/test/_fixtures/files-overrides-gitignore/ignore-me.js b/test/_fixtures/files-overrides-gitignore/ignore-me.js new file mode 100644 index 0000000..36fb240 --- /dev/null +++ b/test/_fixtures/files-overrides-gitignore/ignore-me.js @@ -0,0 +1 @@ +// this should be ignored diff --git a/test/_fixtures/files-overrides-gitignore/package.json b/test/_fixtures/files-overrides-gitignore/package.json new file mode 100644 index 0000000..d7bb129 --- /dev/null +++ b/test/_fixtures/files-overrides-gitignore/package.json @@ -0,0 +1,6 @@ +{ + "files": [ + "test.js", + "test.json" + ] +} diff --git a/test/_fixtures/files-overrides-gitignore/test.js b/test/_fixtures/files-overrides-gitignore/test.js new file mode 100644 index 0000000..d08f33f --- /dev/null +++ b/test/_fixtures/files-overrides-gitignore/test.js @@ -0,0 +1 @@ +// include me diff --git a/test/_fixtures/files-overrides-gitignore/test.json b/test/_fixtures/files-overrides-gitignore/test.json new file mode 100644 index 0000000..4c82741 --- /dev/null +++ b/test/_fixtures/files-overrides-gitignore/test.json @@ -0,0 +1 @@ +{ "include": "me" } diff --git a/test/_fixtures/hashes/duplicate/dei.png b/test/_fixtures/hashes/duplicate/dei.png deleted file mode 100644 index 54279df..0000000 Binary files a/test/_fixtures/hashes/duplicate/dei.png and /dev/null differ diff --git a/test/_fixtures/now-files-overrides-npmignore/.npmignore b/test/_fixtures/now-files-overrides-npmignore/.npmignore new file mode 100644 index 0000000..db54e80 --- /dev/null +++ b/test/_fixtures/now-files-overrides-npmignore/.npmignore @@ -0,0 +1,2 @@ +ignore-me.js +test.json diff --git a/test/_fixtures/now-files-overrides-npmignore/ignore-me.js b/test/_fixtures/now-files-overrides-npmignore/ignore-me.js new file mode 100644 index 0000000..36fb240 --- /dev/null +++ b/test/_fixtures/now-files-overrides-npmignore/ignore-me.js @@ -0,0 +1 @@ +// this should be ignored diff --git a/test/_fixtures/now-files-overrides-npmignore/package.json b/test/_fixtures/now-files-overrides-npmignore/package.json new file mode 100644 index 0000000..7dc492c --- /dev/null +++ b/test/_fixtures/now-files-overrides-npmignore/package.json @@ -0,0 +1,8 @@ +{ + "now": { + "files": [ + "test.js", + "test.json" + ] + } +} diff --git a/test/_fixtures/now-files-overrides-npmignore/test.js b/test/_fixtures/now-files-overrides-npmignore/test.js new file mode 100644 index 0000000..d08f33f --- /dev/null +++ b/test/_fixtures/now-files-overrides-npmignore/test.js @@ -0,0 +1 @@ +// include me diff --git a/test/_fixtures/now-files-overrides-npmignore/test.json b/test/_fixtures/now-files-overrides-npmignore/test.json new file mode 100644 index 0000000..4c82741 --- /dev/null +++ b/test/_fixtures/now-files-overrides-npmignore/test.json @@ -0,0 +1 @@ +{ "include": "me" } diff --git a/test/args-parsing.js b/test/args-parsing.js new file mode 100644 index 0000000..d9de669 --- /dev/null +++ b/test/args-parsing.js @@ -0,0 +1,88 @@ +const path = require('path') +const test = require('ava') +const {spawn} = require('cross-spawn') + +const deployHelpMessage = '𝚫 now [options] ' +const aliasHelpMessage = '𝚫 now alias ' + +test('"now help" prints deploy help message', async t => { + const result = await now('help') + + t.is(result.code, 0) + const stdout = result.stdout.split('\n') + t.true(stdout.length > 1) + t.true(stdout[1].includes(deployHelpMessage)) +}) + +test('"now --help" prints deploy help message', async t => { + const result = await now('--help') + + t.is(result.code, 0) + const stdout = result.stdout.split('\n') + t.true(stdout.length > 1) + t.true(stdout[1].includes(deployHelpMessage)) +}) + +test('"now deploy --help" prints deploy help message', async t => { + const result = await now('deploy', '--help') + + t.is(result.code, 0) + const stdout = result.stdout.split('\n') + t.true(stdout.length > 1) + t.true(stdout[1].includes(deployHelpMessage)) +}) + +test('"now --help deploy" prints deploy help message', async t => { + const result = await now('--help', 'deploy') + + t.is(result.code, 0) + const stdout = result.stdout.split('\n') + t.true(stdout.length > 1) + t.true(stdout[1].includes(deployHelpMessage)) +}) + +test('"now help alias" prints alias help message', async t => { + const result = await now('help', 'alias') + + t.is(result.code, 0) + const stdout = result.stdout.split('\n') + t.true(stdout.length > 1) + t.true(stdout[1].includes(aliasHelpMessage)) +}) + +test('"now alias --help" is the same as "now --help alias"', async t => { + const [result1, result2] = await Promise.all([now('alias', '--help'), now('--help', 'alias')]) + + t.is(result1.code, 0) + t.is(result1.code, result2.code) + t.is(result1.stdout, result2.stdout) +}) + +/** + * Run the built now binary with given arguments + * + * @param {String} args string arguements + * @return {Promise} promise that resolves to an object {code, stdout} + */ +function now(...args) { + return new Promise((resolve, reject) => { + const command = path.resolve(__dirname, '../bin/now.js') + const now = spawn(command, args) + + let stdout = '' + now.stdout.on('data', data => { + stdout += data + }) + + now.on('error', err => { + reject(err) + }) + + now.on('close', code => { + resolve({ + code, + stdout + }) + }) + }) +} diff --git a/test/index.js b/test/index.js index 42c05b1..35686cc 100644 --- a/test/index.js +++ b/test/index.js @@ -1,14 +1,14 @@ // Native -import {join, resolve} from 'path' +const {join, resolve} = require('path') // Packages -import test from 'ava' -import {asc as alpha} from 'alpha-sort' -import {readFile} from 'fs-promise' +const test = require('ava') +const {asc: alpha} = require('alpha-sort') +const {readFile} = require('fs-promise') // Ours -import {npm as getNpmFiles_, docker as getDockerFiles} from '../lib/get-files' -import hash from '../lib/hash' +const {npm: getNpmFiles_, docker: getDockerFiles} = require('../lib/get-files') +const hash = require('../lib/hash') const prefix = join(__dirname, '_fixtures') + '/' const base = path => path.replace(prefix, '') @@ -43,6 +43,24 @@ test('`files` + `.*.swp` + `.npmignore`', async t => { t.is(base(files[2]), 'files-in-package-ignore/package.json') }) +test('`files` overrides `.gitignore`', async t => { + let files = await getNpmFiles(fixture('files-overrides-gitignore')) + files = files.sort(alpha) + t.is(files.length, 3) + t.is(base(files[0]), 'files-overrides-gitignore/package.json') + t.is(base(files[1]), 'files-overrides-gitignore/test.js') + t.is(base(files[2]), 'files-overrides-gitignore/test.json') +}) + +test('`now.files` overrides `.npmignore`', async t => { + let files = await getNpmFiles(fixture('now-files-overrides-npmignore')) + files = files.sort(alpha) + t.is(files.length, 3) + t.is(base(files[0]), 'now-files-overrides-npmignore/package.json') + t.is(base(files[1]), 'now-files-overrides-npmignore/test.js') + t.is(base(files[2]), 'now-files-overrides-npmignore/test.json') +}) + test('simple', async t => { let files = await getNpmFiles(fixture('simple')) files = files.sort(alpha) diff --git a/test/to-host.js b/test/to-host.js index 22ba84c..449dff0 100644 --- a/test/to-host.js +++ b/test/to-host.js @@ -1,5 +1,5 @@ -import test from 'ava' -import toHost from '../lib/to-host' +const test = require('ava') +const toHost = require('../lib/to-host') test('simple', async t => { t.is(toHost('zeit.co'), 'zeit.co')