Browse Source

Merge pull request #59 from zeit/alias

Alias
master
Guillermo Rauch 9 years ago
parent
commit
eb0522bf52
  1. 6
      History.md
  2. 4
      bin/now
  3. 280
      bin/now-alias
  4. 82
      bin/now-deploy
  5. 36
      bin/now-list
  6. 39
      bin/now-remove
  7. 26
      gulpfile.js
  8. 11
      lib/agent.js
  9. 246
      lib/alias.js
  10. 11
      lib/index.js
  11. 29
      lib/retry.js
  12. 17
      lib/to-host.js
  13. 4
      lib/ua.js
  14. 47
      package.json
  15. BIN
      test/_fixtures/hashes/duplicate/dei.png
  16. 56
      test/index.js
  17. 26
      test/to-host.js

6
History.md

@ -1,4 +1,10 @@
0.14.4 / 2016-05-23
===================
* implement `async-retry` [@rauchg]
* fix debug [@rauchg]
0.14.3 / 2016-05-23
===================

4
bin/now

@ -19,8 +19,8 @@ const exit = (code) => {
};
const defaultCommand = 'deploy';
const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove']);
const aliases = new Map([['ls', 'list'], ['rm', 'remove']]);
const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'ln']);
const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias']]);
let cmd = argv._[0];
let args = [];

280
bin/now-alias

@ -0,0 +1,280 @@
#!/usr/bin/env node
import chalk from 'chalk';
import minimist from 'minimist';
import table from 'text-table';
import ms from 'ms';
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';
const argv = minimist(process.argv.slice(2), {
boolean: ['help', 'debug'],
alias: {
help: 'h',
debug: 'd'
}
});
const subcommand = argv._[0];
// options
const help = () => {
console.log(`
${chalk.bold('𝚫 now alias')} <ls | set | rm> <deployment> <alias>
${chalk.dim('Options:')}
-h, --help output usage information
-d, --debug debug mode [off]
${chalk.dim('Examples:')}
${chalk.gray('–')} Lists all your aliases:
${chalk.cyan('$ now alias ls')}
${chalk.gray('–')} Adds a new alias to ${chalk.underline('my-api.now.sh')}:
${chalk.cyan(`$ now alias set ${chalk.underline('api-ownv3nc9f8.now.sh')} ${chalk.underline('my-api.now.sh')}`)}
The ${chalk.dim('`.now.sh`')} suffix can be ommited:
${chalk.cyan('$ now alias set api-ownv3nc9f8 my-api')}
The deployment id can be used as the source:
${chalk.cyan('$ now alias set deploymentId my-alias')}
Custom domains work as alias targets:
${chalk.cyan(`$ now alias set ${chalk.underline('api-ownv3nc9f8.now.sh')} ${chalk.underline('my-api.com')}`)}
${chalk.dim('–')} The subcommand ${chalk.dim('`set`')} is the default and can be skipped.
${chalk.dim('–')} ${chalk.dim('`http(s)://`')} in the URLs is unneeded / ignored.
${chalk.gray('–')} Removing an alias:
${chalk.cyan('$ now alias rm aliasId')}
To get the list of alias ids, use ${chalk.dim('`now alias ls`')}.
${chalk.dim('Alias:')} ln
`);
};
// options
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
const exit = (code) => {
// we give stdout some time to flush out
// because there's a node bug where
// stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456
setTimeout(() => process.exit(code || 0), 100);
};
if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(config.token || login(apiUrl))
.then(async (token) => {
try {
await run(token);
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
exit(1);
}
})
.catch((e) => {
error(`Authentication error – ${e.message}`);
exit(1);
});
}
async function run (token) {
const alias = new NowAlias(apiUrl, token, { debug });
const args = argv._.slice(1);
switch (subcommand) {
case 'list':
case 'ls':
const list = await alias.list();
const urls = new Map(list.map(l => [l.uid, l.url]));
const target = null != args[0] ? String(args[0]) : null;
const aliases = await alias.ls(target);
const byTarget = new Map();
if (target) {
byTarget.set(target, aliases);
} else {
aliases.forEach((_alias) => {
const _aliases = byTarget.get(_alias.deploymentId) || [];
byTarget.set(_alias.deploymentId, _aliases.concat(_alias));
});
}
const sorted = await sort([...byTarget]);
const current = new Date();
const text = sorted.map(([target, _aliases]) => {
return table(_aliases.map((_alias) => {
const _url = chalk.underline(`https://${_alias.alias}`);
const _sourceUrl = chalk.underline(`https://${urls.get(target)}`);
const time = chalk.gray(ms(current - new Date(_alias.created)) + ' ago');
return [_alias.uid, _sourceUrl, _url, time];
}), { align: ['l', 'r', 'l'], hsep: ' '.repeat(3) });
}).join('\n\n');
if (text) console.log('\n' + text + '\n');
break;
case 'remove':
case 'rm':
const _target = String(args[0]);
if (!_target) {
const err = new Error('No alias id specified');
err.userError = true;
throw err;
}
const _aliases = await alias.ls();
const _alias = findAlias(_target, _aliases);
if (!_alias) {
const err = new Error(`Alias not found by "${_target}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`);
err.userError = true;
throw err;
}
try {
const confirmation = (await readConfirmation(alias, _alias)).toLowerCase();
if ('y' !== confirmation && 'yes' !== confirmation) {
console.log('\n> Aborted');
process.exit(0);
}
const start = new Date();
await alias.rm(_alias);
const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Alias ${chalk.bold(_alias.uid)} removed [${elapsed}]`);
} catch (err) {
error(err);
exit(1);
}
break;
case 'add':
case 'set':
if (2 !== args.length) {
error('Invalid number of arguments');
return;
}
await alias.set(String(args[0]), String(args[1]));
break;
default:
if (2 === argv._.length) {
await alias.set(String(argv._[0]), String(argv._[1]));
} else if (argv._.length >= 3) {
error('Invalid number of arguments');
help();
exit(1);
} else {
error('Please specify a valid subcommand: ls | set | rm');
help();
exit(1);
}
}
alias.close();
}
async function sort (aliases) {
let pkg;
try {
const json = await fs.readFile('package.json');
pkg = JSON.parse(json);
} catch (err) {
pkg = {};
}
return aliases
.map(([target, _aliases]) => {
_aliases = _aliases.slice().sort((a, b) => {
return b.created - a.created;
});
return [target, _aliases];
})
.sort(([targetA, aliasesA], [targetB, aliasesB]) => {
if (pkg.name === targetA) return -1;
if (pkg.name === targetB) return 1;
return aliasesB[0].created - aliasesA[0].created;
});
}
function indent (text, n) {
return text.split('\n').map((l) => ' '.repeat(n) + l).join('\n');
}
async function readConfirmation (alias, _alias) {
const list = await alias.list();
const urls = new Map(list.map(l => [l.uid, l.url]));
return new Promise((resolve, reject) => {
const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + ' ago');
const _sourceUrl = chalk.underline(`https://${urls.get(_alias.deploymentId)}`);
const tbl = table(
[[_alias.uid, _sourceUrl, chalk.underline(`https://${_alias.alias}`), time]],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
);
process.stdout.write('> The following deployment will be removed permanently\n');
process.stdout.write(' ' + tbl + '\n');
process.stdout.write(` ${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`);
process.stdin.on('data', (d) => {
process.stdin.pause();
resolve(d.toString().trim());
}).resume();
});
}
function findAlias (alias, list) {
let key, val;
if (/\./.test(alias)) {
val = toHost(alias);
key = 'alias';
} else {
val = alias;
key = 'uid';
}
const _alias = list.find((d) => {
if (d[key] === val) {
if (debug) console.log(`> [debug] matched alias ${d.uid} by ${key} ${val}`);
return true;
}
// match prefix
if (`${val}.now.sh` === d.alias) {
if (debug) console.log(`> [debug] matched alias ${d.uid} by url ${d.host}`);
return true;
}
return false;
});
return _alias;
}

82
bin/now-deploy

@ -13,25 +13,61 @@ import Now from '../lib';
import ms from 'ms';
import { handleError, error } from '../lib/error';
const argv = minimist(process.argv.slice(2));
const argv = minimist(process.argv.slice(2), {
boolean: ['help', 'version', 'debug', 'force', 'login', 'no-clipboard'],
alias: {
help: 'h',
debug: 'd',
version: 'v',
force: 'f',
forceSync: 'F',
login: 'L',
'no-clipboard': 'C'
}
});
const help = () => {
console.log(`
𝚫 now [options] <command|path>
${chalk.bold('𝚫 now')} [options] <command | path>
Commands:
${chalk.dim('Commands:')}
ls | list [app] output list of instances
rm | remove [id] alias of remove
help [cmd] display help for [cmd]
deploy [path] performs a deployment ${chalk.bold('(default)')}
ls | list [app] list deployments
rm | remove [id] remove a deployment
ln | alias [id] [url] configures an alias / domain
help [cmd] displays complete help for [cmd]
Options:
${chalk.dim('Options:')}
-h, --help output usage information
-v, --version output the version number
-d, --debug Debug mode [off]
-f, --force Force a new deployment even if nothing has changed
-L, --login Configure login
-C, --no-clipboard Do not attempt to copy URL to clipboard
-d, --debug debug mode [off]
-f, --force force a new deployment even if nothing has changed
-L, --login configure login
-C, --no-clipboard do not attempt to copy URL to clipboard
${chalk.dim('Examples:')}
${chalk.gray('–')} Deploys the current directory
${chalk.cyan('$ now')}
${chalk.gray('–')} Deploys a custom path ${chalk.dim('`/usr/src/project`')}
${chalk.cyan('$ now /usr/src/project')}
${chalk.gray('–')} Lists all deployments with their IDs
${chalk.cyan('$ now ls')}
${chalk.gray('–')} Associates deployment ${chalk.dim('`deploymentId`')} with ${chalk.dim('`custom-domain.com`')}
${chalk.cyan('$ now alias deploymentId custom-domain.com')}
${chalk.gray('–')} Displays comprehensive help for the subcommand ${chalk.dim('`list`')}
${chalk.cyan('$ now help list')}
`);
};
@ -45,19 +81,27 @@ if (path) {
path = process.cwd();
}
const exit = (code) => {
// we give stdout some time to flush out
// because there's a node bug where
// stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456
setTimeout(() => process.exit(code || 0), 100);
};
// options
const debug = argv.debug || argv.d;
const clipboard = !(argv.noClipboard || argv.C);
const force = argv.f || argv.force;
const forceSync = argv.F || argv.forceSync;
const shouldLogin = argv.L || argv.login;
const debug = argv.debug;
const clipboard = !argv['no-clipboard'];
const force = argv.force;
const forceSync = argv.forceSync;
const shouldLogin = argv.login;
const apiUrl = argv.url || 'https://api.zeit.co';
const config = cfg.read();
if (argv.h || argv.help) {
help();
process.exit(0);
exit(0);
} else if (argv.v || argv.version) {
console.log(chalk.bold('𝚫 now'), version);
process.exit(0);
@ -135,10 +179,10 @@ async function sync (token) {
now.upload();
now.on('upload', ({ name, data }) => {
now.on('upload', ({ names, data }) => {
const amount = data.length;
if (debug) {
console.log(`> [debug] Uploaded: ${name} (${bytes(data.length)})`);
console.log(`> [debug] Uploaded: ${names.join(' ')} (${bytes(data.length)})`);
}
bar.tick(amount);
});

36
bin/now-list

@ -10,21 +10,38 @@ import login from '../lib/login';
import * as cfg from '../lib/cfg';
import { handleError, error } from '../lib/error';
const argv = minimist(process.argv.slice(2));
const argv = minimist(process.argv.slice(2), {
boolean: ['help', 'debug'],
alias: {
help: 'h',
debug: 'd'
}
});
const help = () => {
console.log(`
𝚫 now list [app]
${chalk.bold('𝚫 now list')} [app]
Alias: ls
Options:
${chalk.dim('Options:')}
-h, --help output usage information
-d, --debug Debug mode [off]
-d, --debug debug mode [off]
${chalk.dim('Examples:')}
${chalk.gray('–')} List all deployments
${chalk.cyan('$ now ls')}
${chalk.gray('–')} List all deployments for the app ${chalk.dim('`my-app`')}
${chalk.cyan('$ now ls my-app')}
${chalk.dim('Alias:')} ls
`);
};
if (argv.h || argv.help) {
if (argv.help) {
help();
process.exit(0);
}
@ -32,7 +49,7 @@ if (argv.h || argv.help) {
const app = argv._[0];
// options
const debug = argv.debug || argv.d;
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
const config = cfg.read();
@ -75,8 +92,9 @@ async function list (token) {
const current = Date.now();
const text = sorted.map(([name, deps]) => {
const t = table(deps.map(({ uid, url, created }) => {
const _url = chalk.underline(`https://${url}`);
const time = chalk.gray(ms(current - created) + ' ago');
return [ uid, time, `https://${url}` ];
return [uid, _url, time];
}), { align: ['l', 'r', 'l'], hsep: ' '.repeat(6) });
return chalk.bold(name) + '\n\n' + indent(t, 2);
}).join('\n\n');

39
bin/now-remove

@ -9,36 +9,43 @@ import login from '../lib/login';
import * as cfg from '../lib/cfg';
import { handleError, error } from '../lib/error';
const argv = minimist(process.argv.slice(2));
const argv = minimist(process.argv.slice(2), {
boolean: ['help', 'debug', 'hard'],
alias: {
help: 'h',
debug: 'd'
}
});
const deploymentId = argv._[0];
// options
const help = () => {
console.log(`
𝚫 now remove [deploymentId]
${chalk.bold('𝚫 now remove')} [deploymentId]
Alias: rm
Options:
${chalk.dim('Options:')}
-h, --help output usage information
-d, --debug Debug mode [off]
-d, --debug debug mode [off]
${chalk.dim('Examples:')}
${chalk.gray('–')} Remove a deployment identified by ${chalk.dim('`deploymentId`')}:
${chalk.cyan('$ now rm deploymentId')}
${chalk.dim('Alias:')} rm
`);
};
if (argv.h || argv.help) {
if (argv.help || !deploymentId) {
help();
process.exit(0);
}
if (!deploymentId) {
error('No deployment id specified. You can see active deployments with `now ls`.');
help();
process.exit(1);
}
// options
const debug = argv.debug || argv.d;
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
const hard = argv.hard || false;
@ -48,13 +55,13 @@ function readConfirmation (app) {
return new Promise((resolve, reject) => {
const time = chalk.gray(ms(new Date() - app.created) + ' ago');
const tbl = table(
[[app.uid, time, `https://${app.url}`]],
[[app.uid, chalk.underline(`https://${app.url}`), time]],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
);
process.stdout.write('> The following deployment 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('[yN] ')}`);
process.stdin.on('data', (d) => {
process.stdin.pause();

26
gulpfile.js

@ -2,7 +2,6 @@ const gulp = require('gulp');
const del = require('del');
const ext = require('gulp-ext');
const babel = require('gulp-babel');
const eslint = require('gulp-eslint');
const uglify = require('gulp-uglify');
const help = require('gulp-task-listing');
@ -13,7 +12,7 @@ gulp.task('compile', [
'compile-bin'
]);
gulp.task('compile-lib', function () {
gulp.task('compile-lib', () => {
return gulp.src('lib/**/*.js')
.pipe(babel({
presets: ['es2015'],
@ -27,7 +26,7 @@ gulp.task('compile-lib', function () {
.pipe(gulp.dest('build/lib'));
});
gulp.task('compile-bin', function () {
gulp.task('compile-bin', () => {
return gulp.src('bin/*')
.pipe(babel({
presets: ['es2015'],
@ -42,19 +41,18 @@ gulp.task('compile-bin', function () {
.pipe(gulp.dest('build/bin'));
});
gulp.task('lint', function () {
return gulp.src([
'gulpfile.js',
'lib/**/*.js',
'bin/*'
])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
gulp.task('watch', ['watch-lib', 'watch-bin']);
gulp.task('watch-lib', () => {
return gulp.watch('lib/*.js', ['compile-lib']);
});
gulp.task('watch-bin', () => {
return gulp.watch('bin/*', ['compile-bin']);
});
gulp.task('clean', function () {
gulp.task('clean', () => {
return del(['build']);
});
gulp.task('default', ['lint', 'compile']);
gulp.task('default', ['compile', 'watch']);

11
lib/agent.js

@ -1,4 +1,6 @@
import http2 from 'spdy';
import fetch from 'node-fetch';
import { parse } from 'url';
/**
* Returns a `fetch` version with a similar
@ -14,17 +16,16 @@ import fetch from 'node-fetch';
export default class Agent {
constructor (url, { debug } = {}) {
this._url = url;
this._host = parse(url).host;
this._debug = debug;
this._initAgent();
}
_initAgent () {
/*
this._agent = https.createAgent({
this._agent = http2.createAgent({
host: this._host,
port: 443
}).once('error', (err) => this._onError(err));
*/
}
_onError (err) {
@ -43,7 +44,7 @@ export default class Agent {
}
const { body } = opts;
// opts.agent = this._agent;
opts.agent = this._agent;
if (body && 'object' === typeof body && 'function' !== typeof body.pipe) {
opts.headers['Content-Type'] = 'application/json';
@ -59,6 +60,6 @@ export default class Agent {
close () {
if (this._debug) console.log('> [debug] closing agent');
// return this._agent.close();
return this._agent.close();
}
}

246
lib/alias.js

@ -0,0 +1,246 @@
import retry from 'async-retry';
import dns from 'dns';
import Now from '../lib';
import toHost from './to-host';
import chalk from 'chalk';
export default 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;
throw err;
}
return this.retry(async (bail, attempt) => {
const res = await this._fetch(`/now/deployments/${target.uid}/aliases`);
const body = await res.json();
return body.aliases;
});
}
return this.retry(async (bail, attempt) => {
const res = await this._fetch('/now/aliases');
const body = await res.json();
return body.aliases;
});
}
async rm (_alias) {
return this.retry(async (bail, attempt) => {
const res = await this._fetch(`/now/aliases/${_alias.uid}`, {
method: 'DELETE'
});
if (403 === res.status) {
return bail(new Error('Unauthorized'));
}
if (res.status !== 200) {
const err = new Error('Deletion failed. Try again later.');
throw err;
}
});
}
async findDeployment (deployment) {
const list = await this.list();
let key, val;
if (/\./.test(deployment)) {
val = toHost(deployment);
key = 'url';
} else {
val = deployment;
key = 'uid';
}
const depl = list.find((d) => {
if (d[key] === val) {
if (this._debug) console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`);
return true;
}
// match prefix
if (`${val}.now.sh` === d.url) {
if (this._debug) console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`);
return true;
}
return false;
});
return depl;
}
async set (deployment, alias) {
alias = alias.toLowerCase();
const depl = await this.findDeployment(deployment);
if (!depl) {
const err = new Error(`Deployment not found by "${deployment}". Run ${chalk.dim('`now ls`')} to see your deployments.`);
err.userError = true;
throw err;
}
// evaluate the alias
if (!/\./.test(alias)) {
if (this._debug) console.log(`> [debug] suffixing \`.now.sh\` to alias ${alias}`);
alias = `${alias}.now.sh`;
} else {
alias = toHost(alias);
}
if (!/\.now\.sh$/.test(alias)) {
console.log(`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`);
console.log(`> Verifying that ${chalk.bold(chalk.underline(alias))} has a ${chalk.cyan('`CNAME`')} or ${chalk.cyan('`ALIAS`')} record pointing to ${chalk.bold(chalk.underline('alias.zeit.co'))}.`);
await this.verifyOwnership(alias);
console.log(`> Verification ${chalk.bold('OK')}!`);
}
const { created, uid } = await this.createAlias(depl, alias);
if (created) {
console.log(`${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}: ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)} now points to ${chalk.bold(chalk.underline(`https://${alias}`))}`);
} else {
console.log(`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`);
}
}
createAlias (depl, alias) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`);
const res = await this._fetch(`/now/deployments/${depl.uid}/aliases`, {
method: 'POST',
body: { alias }
});
const body = await res.json();
if (this._debug) console.timeEnd(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`);
// 409 conflict is returned if it already exists
if (409 === res.status) return { uid: body.error.uid };
// no retry on authorization problems
if (403 === res.status) {
const code = body.error.code;
if ('custom_domain_needs_upgrade' === code) {
const err = new Error(`Custom domains are only enabled for premium accounts. Please upgrade at ${chalk.underline('https://zeit.co/account')}.`);
err.userError = true;
return bail(err);
}
if ('alias_in_use' === code) {
const err = new Error(`The alias you are trying to configure (${chalk.underline(chalk.bold(alias))}) is already in use by a different account.`);
err.userError = true;
return bail(err);
}
return bail(new Error('Authorization error'));
}
// all other errors
if (body.error) {
const code = body.error.code;
if ('deployment_not_found' === code) {
return bail(new Error('Deployment not found'));
}
if ('cert_missing' === code) {
console.log(`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`);
await this.createCert(alias);
// try again, but now having provisioned the certificate
return this.createAlias(depl, alias);
}
return bail(new Error(body.error.message));
}
// the two expected succesful cods are 200 and 304
if (200 !== res.status && 304 !== res.status) {
throw new Error('Unhandled error');
}
return body;
});
}
verifyOwnership (domain) {
return this.retry(async (bail, attempt) => {
const targets = await resolve4('alias.zeit.co');
if (!targets.length) {
return bail(new Error('Unable to resolve alias.zeit.co'));
}
const ips = await resolve4(domain);
if (!ips.length) {
const err = new Error('The domain ${domain} A record in the DNS configuration is not returning any IPs.');
err.userError = true;
return bail(err);
}
for (const ip of ips) {
if (!~targets.indexOf(ip)) {
const err = new Error(`The domain ${domain} has an A record ${chalk.bold(ip)} that doesn\'t resolve to ${chalk.bold(chalk.underline('alias.zeit.co'))}. Please check your DNS settings.`);
err.userError = true;
return bail(err);
}
}
});
}
createCert (domain) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] /now/certs #${attempt}`);
const res = await this._fetch('/now/certs', {
method: 'POST',
body: {
domains: [domain]
}
});
if (304 === res.status) {
console.log('> Certificate already issued.');
return;
}
const body = await res.json();
if (this._debug) console.timeEnd(`> [debug] /now/certs #${attempt}`);
if (body.error) {
const { code } = body.error;
if ('verification_failed' === code) {
const err = new Error(`We couldn't verify ownership of the domain ${domain}. Make sure the appropriate \`ALIAS\` or \`CNAME\` records are configured and pointing to ${chalk.bold('alias.zeit.co')}.`);
err.userError = true;
return bail(err);
}
throw new Error(body.message);
}
if (200 !== res.status && 304 !== res.status) {
throw new Error('Unhandled error');
}
});
}
retry (fn) {
return retry(fn, { retries: 5, randomize: true, onRetry: this._onRetry });
}
}
function resolve4 (host) {
return new Promise((resolve, reject) => {
return dns.resolve4(host, (err, answer) => {
if (err) return reject(err);
resolve(answer);
});
});
}

11
lib/index.js

@ -1,8 +1,9 @@
import bytes from 'bytes';
import chalk from 'chalk';
import getFiles from './get-files';
import ua from './ua';
import hash from './hash';
import retry from './retry';
import retry from 'async-retry';
import Agent from './agent';
import EventEmitter from 'events';
import { basename, resolve } from 'path';
@ -158,12 +159,11 @@ export default class Now extends EventEmitter {
}
const uploadChunk = () => {
Promise.all(parts.shift().map((sha) => retry(async (bail) => {
Promise.all(parts.shift().map((sha) => retry(async (bail, attempt) => {
const file = this._files.get(sha);
const { data, names } = file;
if (this._debug) console.time(`> [debug] /sync ${names.join(' ')}`);
if (this._debug) console.time(`> [debug] /sync #${attempt} ${names.join(' ')}`);
const stream = resumer().queue(data).end();
const res = await this._fetch('/now/sync', {
method: 'POST',
@ -177,7 +177,7 @@ export default class Now extends EventEmitter {
},
body: stream
});
if (this._debug) console.timeEnd(`> [debug] /sync ${names.join(' ')}`);
if (this._debug) console.timeEnd(`> [debug] /sync #${attempt} ${names.join(' ')}`);
// no retry on 4xx
if (200 !== res.status && (400 <= res.status || 500 > res.status)) {
@ -281,6 +281,7 @@ export default class Now extends EventEmitter {
_fetch (_url, opts = {}) {
opts.headers = opts.headers || {};
opts.headers.authorization = `Bearer ${this._token}`;
opts.headers['user-agent'] = ua;
return this._agent.fetch(_url, opts);
}
}

29
lib/retry.js

@ -1,29 +0,0 @@
import retrier from 'retry';
export default function retry (fn, opts) {
return new Promise((resolve, reject) => {
const op = retrier.operation(opts);
const { onRetry } = opts;
// we allow the user to abort retrying
// this makes sense in the cases where
// knowledge is obtained that retrying
// would be futile (e.g.: auth errors)
const bail = (err) => reject(err);
op.attempt((num) => {
if (num > 1 && onRetry) {
const errs = op.errors();
onRetry(errs[errs.length - 1]);
}
fn(bail)
.then((val) => resolve(val))
.catch(err => {
if (!op.retry(err)) {
reject(op.mainError());
}
});
});
});
}

17
lib/to-host.js

@ -0,0 +1,17 @@
import { parse } from 'url';
/**
* Converts a valid deployment lookup parameter to a hostname.
* `http://google.com` => google.com
* google.com => google.com
*/
export default function toHost (url) {
if (/^https?:\/\//.test(url)) {
return parse(url).host;
} else {
// remove any path if present
// `a.b.c/` => `a.b.c`
return url.replace(/(\/\/)?([^\/]+)(.*)/, '$2');
}
}

4
lib/ua.js

@ -0,0 +1,4 @@
import os from 'os';
import { version } from '../../package';
export default `now ${version} node-${process.version} ${os.platform()} (${os.arch()})`;

47
package.json

@ -1,6 +1,6 @@
{
"name": "now",
"version": "0.14.3",
"version": "0.14.4",
"description": "realtime instant node.js deployment with one command",
"readme": "",
"main": "./build/lib/index",
@ -11,49 +11,50 @@
"now": "./build/bin/now"
},
"dependencies": {
"ansi-escapes": "1.3.0",
"ansi-escapes": "1.4.0",
"arr-flatten": "1.0.1",
"array-unique": "0.2.1",
"babel-runtime": "6.6.1",
"babel-runtime": "6.5.0",
"bytes": "2.3.0",
"chalk": "1.1.1",
"copy-paste": "1.1.4",
"cross-spawn-async": "2.2.2",
"fs-promise": "0.4.1",
"graceful-fs": "4.1.3",
"chalk": "1.1.3",
"copy-paste": "1.2.0",
"cross-spawn-async": "2.2.4",
"fs-promise": "0.5.0",
"graceful-fs": "4.1.4",
"minimatch": "3.0.0",
"minimist": "1.2.0",
"ms": "0.7.1",
"node-fetch": "1.3.3",
"node-fetch": "1.5.3",
"progress": "1.1.8",
"resumer": "0.0.0",
"retry": "0.9.0",
"socket.io-client": "1.4.5",
"socket.io-client": "1.4.6",
"split-array": "1.0.1",
"text-table": "0.2.0",
"spdy": "3.3.3",
"email-validator": "1.0.4",
"email-prompt": "0.1.4"
"email-prompt": "0.1.6",
"async-retry": "0.2.0"
},
"devDependencies": {
"alpha-sort": "1.0.2",
"ava": "0.12.0",
"babel-eslint": "5.0.0",
"babel-plugin-transform-runtime": "6.6.0",
"babel-plugin-syntax-async-functions": "6.5.0",
"babel-plugin-transform-async-to-generator": "6.7.0",
"babel-preset-es2015": "6.3.13",
"babel-register": "6.6.5",
"ava": "0.15.1",
"babel-eslint": "6.0.4",
"babel-plugin-transform-runtime": "6.9.0",
"babel-plugin-syntax-async-functions": "6.8.0",
"babel-plugin-transform-async-to-generator": "6.8.0",
"babel-preset-es2015": "6.9.0",
"babel-register": "6.9.0",
"del": "2.2.0",
"eslint-config-standard": "5.1.0",
"eslint-config-standard": "5.3.1",
"eslint-plugin-standard": "1.3.2",
"gulp": "3.9.0",
"gulp": "3.9.1",
"gulp-babel": "6.1.2",
"gulp-eslint": "2.0.0",
"gulp-ext": "1.0.0",
"gulp-task-listing": "1.0.1",
"gulp-uglify": "1.5.3",
"eslint": "2.3.0",
"eslint-plugin-promise": "1.1.0",
"eslint": "2.11.0",
"eslint-plugin-promise": "1.3.1",
"estraverse-fb": "1.3.1"
},
"scripts": {

BIN
test/_fixtures/hashes/duplicate/dei.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

56
test/index.js

@ -13,61 +13,55 @@ const getFiles = (dir) => _getFiles(dir, null, true);
test('`files` + README', async t => {
let files = await getFiles(fixture('files-in-package'));
t.same(files.length, 3);
t.is(files.length, 3);
files = files.sort(alpha);
t.same(base(files[0]), 'files-in-package/build/a/b/c/d.js');
t.same(base(files[1]), 'files-in-package/build/a/e.js');
t.same(base(files[2]), 'files-in-package/package.json');
t.is(base(files[0]), 'files-in-package/build/a/b/c/d.js');
t.is(base(files[1]), 'files-in-package/build/a/e.js');
t.is(base(files[2]), 'files-in-package/package.json');
});
test('`files` + README + `.*.swp` + `.npmignore`', async t => {
let files = await getFiles(fixture('files-in-package-ignore'));
t.same(files.length, 3);
t.is(files.length, 3);
files = files.sort(alpha);
t.same(base(files[0]), 'files-in-package-ignore/build/a/b/c/d.js');
t.same(base(files[1]), 'files-in-package-ignore/build/a/e.js');
t.same(base(files[2]), 'files-in-package-ignore/package.json');
t.is(base(files[0]), 'files-in-package-ignore/build/a/b/c/d.js');
t.is(base(files[1]), 'files-in-package-ignore/build/a/e.js');
t.is(base(files[2]), 'files-in-package-ignore/package.json');
});
test('simple', async t => {
let files = await getFiles(fixture('simple'));
t.same(files.length, 5);
t.is(files.length, 5);
files = files.sort(alpha);
t.same(base(files[0]), 'simple/bin/test');
t.same(base(files[1]), 'simple/index.js');
t.same(base(files[2]), 'simple/lib/woot');
t.same(base(files[3]), 'simple/lib/woot.jsx');
t.same(base(files[4]), 'simple/package.json');
t.is(base(files[0]), 'simple/bin/test');
t.is(base(files[1]), 'simple/index.js');
t.is(base(files[2]), 'simple/lib/woot');
t.is(base(files[3]), 'simple/lib/woot.jsx');
t.is(base(files[4]), 'simple/package.json');
});
test('simple with main', async t => {
let files = await getFiles(fixture('simple-main'));
t.same(files.length, 3);
t.is(files.length, 3);
files = files.sort(alpha);
t.same(base(files[0]), 'simple-main/build/a.js');
t.same(base(files[1]), 'simple-main/index.js');
t.same(base(files[2]), 'simple-main/package.json');
t.is(base(files[0]), 'simple-main/build/a.js');
t.is(base(files[1]), 'simple-main/index.js');
t.is(base(files[2]), 'simple-main/package.json');
});
test('hashes', async t => {
const files = await getFiles(fixture('hashes'));
const hashes = await hash(files);
t.same(hashes.size, 3);
t.same(hashes.get('277c55a2042910b9fe706ad00859e008c1b7d172').name, prefix + 'hashes/dei.png');
t.same(hashes.get('56c00d0466fc6bdd41b13dac5fc920cc30a63b45').name, prefix + 'hashes/index.js');
t.same(hashes.get('706214f42ae940a01d2aa60c5e32408f4d2127dd').name, prefix + 'hashes/package.json');
t.is(hashes.size, 3);
t.is(hashes.get('277c55a2042910b9fe706ad00859e008c1b7d172').names[0], prefix + 'hashes/dei.png');
t.is(hashes.get('277c55a2042910b9fe706ad00859e008c1b7d172').names[1], prefix + 'hashes/duplicate/dei.png');
t.is(hashes.get('56c00d0466fc6bdd41b13dac5fc920cc30a63b45').names[0], prefix + 'hashes/index.js');
t.is(hashes.get('706214f42ae940a01d2aa60c5e32408f4d2127dd').names[0], prefix + 'hashes/package.json');
});
test('ignore node_modules', async t => {
let files = await getFiles(fixture('no-node_modules'));
files = files.sort(alpha);
t.same(base(files[0]), 'no-node_modules/index.js');
t.same(base(files[1]), 'no-node_modules/package.json');
});
test('ignore files over limit', async t => {
let files = await _getFiles(fixture('big-file'), null, { limit: 200 });
t.same(base(files[0]), 'big-file/package.json');
t.same(base(files[1]), 'big-file/small-two.js');
t.same(base(files[2]), 'big-file/small.js');
t.is(base(files[0]), 'no-node_modules/index.js');
t.is(base(files[1]), 'no-node_modules/package.json');
});

26
test/to-host.js

@ -0,0 +1,26 @@
import test from 'ava';
import toHost from '../lib/to-host';
test('simple', async t => {
t.is(toHost('zeit.co'), 'zeit.co');
});
test('leading //', async t => {
t.is(toHost('//zeit-logos-rnemgaicnc.now.sh'), 'zeit-logos-rnemgaicnc.now.sh');
});
test('leading http://', async t => {
t.is(toHost('http://zeit-logos-rnemgaicnc.now.sh'), 'zeit-logos-rnemgaicnc.now.sh');
});
test('leading https://', async t => {
t.is(toHost('https://zeit-logos-rnemgaicnc.now.sh'), 'zeit-logos-rnemgaicnc.now.sh');
});
test('leading https:// and path', async t => {
t.is(toHost('https://zeit-logos-rnemgaicnc.now.sh/path'), 'zeit-logos-rnemgaicnc.now.sh');
});
test('simple and path', async t => {
t.is(toHost('zeit.co/test'), 'zeit.co');
});
Loading…
Cancel
Save