Browse Source

preview

master 8.0.0-beta.1
Guillermo Rauch 8 years ago
commit
4c5a0a0050
  1. 9
      .flowconfig
  2. 2
      .gitignore
  3. 191
      LICENSE
  4. 297
      Readme.md
  5. 5
      examples/nodejs/1-basic-server/server.js
  6. 9
      examples/nodejs/2-microservice/package.json
  7. 8
      examples/nodejs/2-microservice/server.js
  8. 17
      examples/static/index.html
  9. 21
      examples/static/style.css
  10. 112
      package.json
  11. 107
      src/describe-project.js
  12. 7
      src/get-default-auth-cfg.js
  13. 6
      src/get-default-cfg.js
  14. 46
      src/get-help.js
  15. 8
      src/get-now-dir.js
  16. 40
      src/get-welcome.js
  17. 397
      src/now.js
  18. 362
      src/providers/aws/deploy.js
  19. 18
      src/providers/aws/get-aws.js
  20. 61
      src/providers/aws/get-lambda-handler.js
  21. 5
      src/providers/aws/help.js
  22. 13
      src/providers/aws/index.js
  23. 162
      src/providers/aws/login.js
  24. 64
      src/providers/aws/util/external-credentials.js
  25. 285
      src/providers/gcp/deploy.js
  26. 5
      src/providers/gcp/help.js
  27. 18
      src/providers/gcp/index.js
  28. 19
      src/providers/gcp/list-projects.js
  29. 276
      src/providers/gcp/login.js
  30. 24
      src/providers/gcp/util/fetch.js
  31. 121
      src/providers/gcp/util/get-access-token.js
  32. 64
      src/providers/gcp/util/get-function-handler.js
  33. 31
      src/providers/gcp/util/save-credentials.js
  34. 6
      src/providers/index.js
  35. 853
      src/providers/sh/deploy.js
  36. 13
      src/providers/sh/index.js
  37. 83
      src/providers/sh/legacy/agent.js
  38. 138
      src/providers/sh/legacy/build-logger.js
  39. 49
      src/providers/sh/legacy/check-path.js
  40. 91
      src/providers/sh/legacy/error.js
  41. 385
      src/providers/sh/legacy/get-files.js
  42. 221
      src/providers/sh/legacy/git.js
  43. 44
      src/providers/sh/legacy/hash.js
  44. 17
      src/providers/sh/legacy/ignored.js
  45. 14
      src/providers/sh/legacy/logs.js
  46. 1031
      src/providers/sh/legacy/now.js
  47. 57
      src/providers/sh/legacy/plans.js
  48. 39
      src/providers/sh/legacy/prompt-options.js
  49. 195
      src/providers/sh/legacy/read-metadata.js
  50. 20
      src/providers/sh/legacy/to-host.js
  51. 290
      src/providers/sh/login.js
  52. 54
      src/providers/sh/util/get-user.js
  53. 11
      src/providers/sh/util/pkg.js
  54. 7
      src/providers/sh/util/ua.js
  55. 28
      src/resolve.js
  56. 13
      src/resolvers/fs.js
  57. 100
      src/resolvers/github.js
  58. 4
      src/resolvers/index.js
  59. 4
      src/serverless/README.md
  60. 7
      src/serverless/build.js
  61. 11
      src/serverless/builders/index.js
  62. 92
      src/serverless/builders/nodejs.js
  63. 29
      src/serverless/get-handler.js
  64. 110
      src/serverless/handler.js
  65. 43
      src/util/config-files.js
  66. 31
      src/util/copy-to-clipboard.js
  67. 15
      src/util/humanize-path.js
  68. 82
      src/util/input/list.js
  69. 18
      src/util/input/patch-inquirer.js
  70. 68
      src/util/input/prompt-bool.js
  71. 103
      src/util/input/text.js
  72. 5
      src/util/output/aborted.js
  73. 7
      src/util/output/chars.js
  74. 5
      src/util/output/cmd.js
  75. 5
      src/util/output/effect.js
  76. 5
      src/util/output/erase-lines.js
  77. 7
      src/util/output/error.js
  78. 5
      src/util/output/highlight.js
  79. 7
      src/util/output/info.js
  80. 5
      src/util/output/link.js
  81. 17
      src/util/output/list-item.js
  82. 3
      src/util/output/logo.js
  83. 5
      src/util/output/note.js
  84. 6
      src/util/output/ok.js
  85. 5
      src/util/output/param.js
  86. 5
      src/util/output/ready.js
  87. 5
      src/util/output/success.js
  88. 16
      src/util/output/wait.js
  89. 7
      src/util/sleep.js
  90. 7
      test.js
  91. 4255
      yarn.lock

9
.flowconfig

@ -0,0 +1,9 @@
[ignore]
[include]
[libs]
[options]
[lints]

2
.gitignore

@ -0,0 +1,2 @@
node_modules
out

191
LICENSE

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2017 ZEIT, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

297
Readme.md

@ -0,0 +1,297 @@
![now](https://github.com/zeit/art/blob/master/now-hosted/now-header.png?raw=true)
## How it works
Now enables instant immutable deployments to **any cloud provider**
with a simple API that's scalable, intuitive and optimized for collaboration.
This is all it takes to deploy, for example, a Node.js project:
```
cd my-project
cat server.js
```
```js
require('http').createServer((req, res) => {
res.end('▲ Hello World')
}).listen(process.env.PORT)
```
and deploy!
```
now
```
The output of the `now` command will be a unique url to the deployment. No need for git.
## Features
- **Single command deployment**: `now`.
- **100% OSS** and licensed under Apache 2.0
- **Serverless**. Worry about code, not servers.
- **Immutable**. Every time you write `now`, a new deployment is provisioned.
- **Pluggable**. Client can query any public and private cloud provider APIs
- **Flexible**. Interact with multiple clouds at once: `now gcp deploy && now aws deploy`
- **Single command setup**: `now [provider] login`
- **Secure**. All deployments are served over SSL
- **Dynamic and Static**. Deploy popular runtimes or static websites
- **Remote fs support**. Deploy any github project with `now project/repo`, gitlab with `gitlab://`. [PRs welcome](https://github.com/zeit/now/pulls)!
## Installation
To install the latest version:
```
npm install -g now@preview
```
Note: while the API has been in production for over a year, the different
providers are still under heavy development
Optionally, you can clone this repo and run `npm run build` to
produce the [pkg](https://github.com/zeit/pkg) binaries.
## Setup
Configuration of one or more provides is necessary via `login` commands is necessary. If no logins are active and `now`
Global configuration is stored as `~/.now/config.json`.
Your default provider will be the first one you log in to. If you are logged into multiple providers and want to set
```
now config set provider gcp
```
### Now.sh
```
now login
```
To skip the configuration steps and deploy to `https://now.sh`
execute `now login` without any parameters, defaulting to the `sh` provider (equivalent to: `now sh login`).
[Now.sh](https://zeit.co/now) is _**free** for open-source projects and static deployments_. It supports `Dockerfile`, `package.json` and static sites out of the box. All builds are reproducible and executed in the cloud.
### AWS Lambda (`aws`)
Run:
```
now aws login
```
If you have already run `aws configure` before, you will be offered
to synchronize your credentials.
Serverless deployments are provisioned by using:
- Lambda functions λ
- A proxy is automatically used to bridge the API between
HTTP and lambda functions and retain a consistent interface
- Certificate Manager
- API Gateway
### Google Cloud Platform (`gcp`)
```
$ now gcp login
```
and follow the instructions!
### Microsoft Azure (`az`)
```
$ now az login
```
and follow the instructions!
## <span id="configuration">Project Configuration</span>
<table>
<td>ℹ️</td><td>We welcome feedback from <a href="#community">the community</a>!</td>
</table>
The v1 release of `now.json` includes the following specification:
- `name` (optional, recommended) `String`
- `description` (optional, recommended) `String`
- `type` (optional, recommended). One of:
- `String` an unique identifier for the project type. The following
are recommended choices to be supported by every provider:
- `docker`
- `nodejs`
- `static`
- `Object`
when it's necessary to specify a version or multiple interacting runtimes. It's a dictionary of runtime identifier and [SemVer-compatible]() version. For example:
```
{ "type": { "docker": "1.x.x" } }
```
- `provider` (optional) indicates affinity to a certain provider
- `target` (optional) `String`
- specifies a directory or file to deploy. If relative, it's resolved
to the project directory. This is useful when a certain
deployment type (like `static`) has an output target, like an `out`
or `dist` directory.
- `env` (optional). One of
- `Object` a dictionary mapping the name of the environmental variable
to expose to the deployment and its value.
If the value begins with `@`, it's considered a
- `Array` a list of suggested environmental variables that the project
_might_ require to be deployed and function correctly
- `regions` - `Array` of `String`
- specifies one or more regition identifiers to deploy to. A wildcard
can be used to signify deployment to all supported regions by the
provider
- `files` - `Array` of `String`
- specifies a whitelist of what files have to be deployed
To supply provider-specific configuration, you can include an arbitrary `Object` and use the provider identifier as the key.
## <span id="global-config">Global Configuration</span>
The client will initialize a `.now` directory in the user's home
directory upon first running.
There, two files can be found:
- `config.json`
- `credentials.json`
## Implementation notes
Now is directly modeled after UNIX. It's useful to think of the primary subcommands `deploy`, `alias` and `rm` as being the "cloud equivalents" of `cp`, `ln` and `rm`.
The minimal set of commands that providers must supply are:
<table>
<tr>
<td><code>[]</code> / <code>deploy</code></td>
<td>the default command to launch a deployment</td>
</tr>
<tr>
<td><code>remove</code> / <code>rm</code></td>
<td>remove a deployment identified by its unique URL</td>
</tr>
</table>
Recommended, but not required, commands are:
<table>
<tr>
<td><code>logs</code> | <code>ln</code></td>
<td>associates a URL with a permanent domain name</td>
</tr>
<tr>
<td><code>secrets</code> <code>ls</code> <code>rm</code> <code>add</code></td>
<td>associates a URL with a permanent domain name</td>
</tr>
<tr>
<td><code>domains</code> <code>ls</code> / <code>add</code> / <code>rm</code></td>
<td>manage domains</td>
</tr>
<tr>
<td><code>dns</code> <code>ls</code> / <code>add</code> / <code>rm</code></td>
<td>manage dns records</td>
</tr>
<tr>
<td><code>certs</code> <code>ls</code> / <code>add</code> / <code>rm</code></td>
<td>manage certificates</td>
</tr>
</table>
The `build` step for serverless deployments is implemented locally and is compatible with projects configured with the `type`:
- `nodejs`
- `go`
- `static`
## Philosophy
### Immutability
Each time you write `now` a new deployment is provisioned. Whenever
possible, providers should strive to make deployments idempotent in the
absence of changes to:
- Originating source code
- Configuration
- Environment variables
### Standards compliance
All projects expose a HTTP/1.1-compatible interface. A port is provided
via the standard `process.env.PORT`.
### Secure
Whenever possible, deployments are strongly encouraged to be served over SSL. The process of provisioning certificates should be transparent to the user.
### Projects should require minimal JSON configuration
Whenever possible, projects should be deployable with minimal or no configuration.
### Avoid manifest duplication
If the configuration or conventions imposed by a programming language
or framework are present, attempt to provide sane defaults.
Examples of this is the presence of `Dockerfile` or `package.json`. When
publishing a project it's recommended that the [`type`](#type) is strictly
configured in [`now.json`](#now-json) to avoid
## Contributions and Roadmap
#### Community
All feedback and suggestions are welcome!
- 💬 Chat: Join us on [zeit.chat](https://zeit.chat) `#now-client`.
- 📣 Stay up to date on new features and announcments on [@zeithq](https://twitter.com/zeithq).
- 🔐 Subscribe to our [security](http://zeit.us12.list-manage1.com/subscribe?u=3c9e9e13d7e6dae8faf375bed&id=110e586914) mailing list to stay up-to-date on urgent security disclosures.
Please note: we adhere to the [contributor coventant](http://contributor-covenant.org/) for
all interactions in our community.
#### Contributions
To get started contributing, make sure you're running `node` `8.x.x`. Clone this repository:
```
git clone https://github.com/zeit/now
```
To test the [`pkg`](https://github.com/zeit/pkg) binary distribution, run:
```
npm run build
```
#### Ongoing development
- Support for `now <file>`, with support for:
- Binaries as a first-class deployment type
- Static deployments as a fallback
- We are working on built-in support for provisioning [Kubernetes](https://kubernetes.io/)
replication controllers and pods, in a similar vein as the [Draft](https://github.com/azure/draft) project.
- A simple API to register custom providers and pluggable build systems externally, such as Travis, Circle CI, etc.
- A companion desktop app [Now Desktop](https://github.com/zeit/now-desktop)
is available, released under the MIT license.
Work is ongoing for pluggable providers to enable:
- Team collaboration
- One-click context switch
- Drag and drop deployments
- Adding interoperabity between objects that live in different providers
- Providing a Next.js and React powered dashboard that can be deployed anywhere
## License
Now is licensed under the Apache License, Version 2.0.
See LICENSE for the full license text.

5
examples/nodejs/1-basic-server/server.js

@ -0,0 +1,5 @@
require('http')
.createServer((req, res) => {
res.end('Hello world!')
})
.listen(process.env.PORT)

9
examples/nodejs/2-microservice/package.json

@ -0,0 +1,9 @@
{
"name": "micro-example",
"dependencies": {
"micro": "latest"
},
"scripts": {
"start": "micro server"
}
}

8
examples/nodejs/2-microservice/server.js

@ -0,0 +1,8 @@
module.exports = () => {
return {
project: {
type: 'microservice',
poweredBy: '▲'
}
}
}

17
examples/static/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>My first now deployment</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div>
<p style="font: 50px Helvetica">
</p>
</div>
</body>
</html>

21
examples/static/style.css

@ -0,0 +1,21 @@
body {
background: #000;
color: #fff;
}
div {
display: flex;
align-items: center;
height: 100%;
width: 100%;
position: absolute;
}
p {
font-size: 200px;
margin: 0;
padding: 0;
width: 100%;
text-align: center;
}

112
package.json

@ -0,0 +1,112 @@
{
"name": "now",
"version": "8.0.0-beta.1",
"dependencies": {
"ansi-escapes": "^2.0.0",
"archiver": "^2.0.0",
"array-unique": "^0.3.2",
"async-retry": "^1.1.3",
"aws-sdk": "^2.82.0",
"bytes": "^2.5.0",
"chalk": "^2.0.1",
"clipboardy": "^1.1.4",
"convert-stream": "^1.0.2",
"debug": "^2.6.8",
"deployment-type": "^1.0.1",
"docker-file-parser": "^1.0.2",
"dotenv": "^4.0.0",
"download": "^6.2.5",
"email-prompt": "^0.3.1",
"email-validator": "^1.0.7",
"fs-extra": "^3.0.1",
"fs.promised": "^3.0.0",
"glob": "^7.1.2",
"ignore": "^3.3.3",
"ini": "^1.3.4",
"inquirer": "^3.2.0",
"is-url": "^1.2.2",
"minimist": "^1.2.0",
"mkdirp-promise": "^5.0.1",
"ms": "^2.0.0",
"node-fetch": "^1.7.1",
"opn": "^5.1.0",
"ora": "^1.3.0",
"pipe-streams-to-promise": "^0.2.0",
"resumer": "^0.0.0",
"socket.io-client": "^2.0.3",
"split-array": "^1.0.1",
"strip-ansi": "^4.0.0",
"tar-fs": "^1.15.3",
"then-sleep": "^1.0.1",
"tmp-promise": "^1.0.3",
"uid-promise": "^1.0.0"
},
"main": "./out/now.js",
"files": [
"out"
],
"bin": {
"now": "./out/now.js"
},
"devDependencies": {
"ava": "^0.20.0",
"babel-cli": "^6.24.1",
"babel-eslint": "^7.2.3",
"babel-preset-flow": "^6.23.0",
"babel-register": "^6.24.1",
"eslint": "^4.1.1",
"flow-bin": "^0.49.1",
"flow-remove-types": "^1.2.1",
"lint-staged": "^4.0.1",
"pre-commit": "^1.2.2",
"prettier": "^1.5.2"
},
"scripts": {
"build": "babel src/ -d out/ && chmod +x out/now.js",
"test": "eslint . && flow",
"prepublish": "yarn run test && yarn run build",
"lint:staged": "lint-staged",
"dev": "yarn run build && ./out/now.js"
},
"pre-commit": "lint:staged",
"lint-staged": {
"*.js": [
"eslint",
"prettier --write --single-quote --no-semi",
"git add"
]
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "script"
},
"parser": "babel-eslint",
"extends": [
"eslint:recommended"
],
"env": {
"es6": true,
"node": true
},
"rules": {
"func-names": [
"error",
"as-needed"
],
"no-console": 0,
"no-shadow": "error",
"no-extra-semi": 0
}
},
"babel": {
"presets": [
"flow"
]
},
"ava": {
"require": [
"babel-register"
]
}
}

107
src/describe-project.js

@ -0,0 +1,107 @@
const { join, basename } = require('path')
const { exists, stat, readFile } = require('fs.promised')
const describeProject = async path => {
let nowJSON = null
let packageJSON = null
const s = await stat(path)
if (s.isFile()) {
throw new Error(
'Deploying files directly is coming! Please supply a directory'
)
}
const nowJSONPath = join(path, 'now.json')
if (await exists(nowJSONPath)) {
nowJSON = JSON.parse(await readFile(nowJSONPath))
}
const packageJSONPath = join(path, 'package.json')
if (await exists(packageJSONPath)) {
packageJSON = JSON.parse(await readFile(packageJSONPath))
}
if (packageJSON && packageJSON.now && nowJSON) {
const err = new Error(
'Ambigous config: package.json (with `now` field) and now.json'
)
err.code = 'AMBIGOUS_CONFIG'
err.files = ['package.json', 'now.json']
throw err
}
if (nowJSON && (nowJSON.type === 'npm' || nowJSON.type === 'node')) {
console.log(
'DEPRECATED: `npm` and `node` types should be `nodejs` in `now.json`'
)
nowJSON.type = 'nodejs'
}
// npm has a convention that `npm start`, if not defined,
// will invoke `node server.js`
const hasServerJSFile = await exists(join(path, 'server.js'))
// we support explicit definition of nodejs as a type, or we
// guess it based on `package.json` or
if (
(nowJSON && nowJSON.type === 'nodejs') ||
((!nowJSON || !nowJSON.type) && (packageJSON || hasServerJSFile))
) {
return {
name: getName(path, nowJSON, packageJSON),
description: getDescription(nowJSON, packageJSON),
type: 'nodejs',
nowJSON,
packageJSON,
hasServerJSFile
}
}
if (nowJSON && nowJSON.type) {
return {
name: getName(path, nowJSON),
description: getDescription(nowJSON),
type: nowJSON.type,
nowJSON
}
} else if (await exists(join(path, 'main.go'))) {
return {
name: getName(path, nowJSON),
description: getDescription(nowJSON),
type: 'go'
}
} else {
return {
type: 'static'
}
}
}
const getName = (path, nowJSON = null, packageJSON = null) => {
if (nowJSON && nowJSON.name != null) {
return nowJSON.name.toLowerCase()
}
if (packageJSON && packageJSON.name != null) {
return packageJSON.name.toLowerCase()
}
return basename(path).replace(/[^\w]+/g, '-').toLowerCase()
}
const getDescription = (nowJSON = null, packageJSON = null) => {
if (nowJSON && nowJSON.description != null) {
return nowJSON.description
}
if (packageJSON && packageJSON.name != null) {
return packageJSON.description
}
return null
}
module.exports = describeProject

7
src/get-default-auth-cfg.js

@ -0,0 +1,7 @@
const getDefaultAuthCfg = () => ({
_:
'This is your now credentials file. DONT SHARE! More: https://git.io/now-global-config',
credentials: []
})
module.exports = getDefaultAuthCfg

6
src/get-default-cfg.js

@ -0,0 +1,6 @@
const getDefaultCfg = () => ({
_:
'This is your now credentials file. DONT SHARE! More: https://git.io/now-global-config'
})
module.exports = getDefaultCfg

46
src/get-help.js

@ -0,0 +1,46 @@
const cmd = require('./util/output/cmd')
const li = require('./util/output/list-item')
const link = require('./util/output/link')
const { gray, bold } = require('chalk')
// prettier-disable
const getHelp = (currentProvider, providers) =>
`
${bold('Now')}: universal serverless deployments.
To deploy, run in any directory of your choosing:
${cmd('now')}
The deployment backend provider is fully configurable.
The following are supported:
${Object.keys(providers)
.map(name =>
li(
`${bold(name)}\t ${providers[name]
.title}\t\t\t\t\t${currentProvider === name
? gray('(default)')
: ' '}`
)
)
.join('\n ')}
For example, to setup AWS Lambda functions run:
${cmd('now aws login')}
Some useful subcommands:
${li(cmd('now ls'))}
${li(cmd('now rm'))}
${li(cmd('now alias'))}
To read more in-depth documentation, run:
${cmd('now [provider] [subcommand] help')}
For more information: ${link('https://github.com/zeit/now')}.
`
module.exports = getHelp

8
src/get-now-dir.js

@ -0,0 +1,8 @@
const { homedir } = require('os')
const { join } = require('path')
const getNowDir = () => {
return process.env.NOW_HOME || join(homedir(), '.now')
}
module.exports = getNowDir

40
src/get-welcome.js

@ -0,0 +1,40 @@
const cmd = require('./util/output/cmd')
const li = require('./util/output/list-item')
const link = require('./util/output/link')
const { gray, bold } = require('chalk')
// prettier-disable
const getWelcome = (currentProvider, providers) =>
`
Welcome to ${bold('Now')}!
Our tool makes serverless deployment universal and instant,
with just one command: ${cmd('now')}.
To setup deployments with ${link('https://now.sh')} run:
${cmd('now login')}
The following providers are also supported
${Object.keys(providers)
.map(name =>
li(
`${bold(name)}\t ${providers[name]
.title}\t\t\t\t\t${currentProvider === name
? gray('(default)')
: ' '}`
)
)
.join('\n ')}
To set up AWS, for example, run ${cmd('now aws login')}.
Many can be configured simultaneously!
Hope you enjoy Now! Check out these other resources:
${li(`Run ${cmd('now help')} for more info and examples`)}
${li(link('https://github.com/zeit/now'))}
`
module.exports = getWelcome

397
src/now.js

@ -0,0 +1,397 @@
#!/usr/bin/env node
//@flow
const start = Date.now()
// theirs
const debug = require('debug')('now:main')
const { exists } = require('fs.promised')
const { join } = require('path')
const mkdirp = require('mkdirp-promise')
const minimist = require('minimist')
// ours
const error = require('./util/output/error')
const effect = require('./util/output/effect')
const param = require('./util/output/param')
const getHelp = require('./get-help')
const getWelcome = require('./get-welcome')
const getNowDir = require('./get-now-dir')
const getDefaultCfg = require('./get-default-cfg')
const getDefaultAuthCfg = require('./get-default-auth-cfg')
const hp = require('./util/humanize-path')
const providers = require('./providers')
const configFiles = require('./util/config-files')
const NOW_DIR = getNowDir()
const NOW_CONFIG_PATH = configFiles.getConfigFilePath()
const NOW_AUTH_CONFIG_PATH = configFiles.getAuthConfigFilePath()
const GLOBAL_COMMANDS = new Set(['help'])
const exit = code => {
debug('finished in', Date.now() - start)
process.exit(code)
}
const main = async argv_ => {
const argv = minimist(argv_, {
boolean: ['help', 'version'],
alias: {
help: 'h',
version: 'v'
}
})
// the second argument to the command can be a path
// (as in: `now path/`) or a subcommand / provider
// (as in: `now ls` or `now aws help`)
let targetOrSubcommand: ?string = argv._[2]
// we want to handle version or help directly only
if (!targetOrSubcommand) {
if (argv.version) {
console.log(require('../package').version)
return 0
}
}
let nowDirExists
try {
nowDirExists = await exists(NOW_DIR)
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to find the ' +
'now global directory: ' +
err.message
)
)
return 1
}
if (!nowDirExists) {
try {
await mkdirp(NOW_DIR)
} catch (err) {
error(
'An unexpected error occurred while trying to create the ' +
`now global directory "${hp(NOW_DIR)}" ` +
err.message
)
}
}
let initConfig = false
let initAuthConfig = false
let configExists
try {
configExists = await exists(NOW_CONFIG_PATH)
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to find the ' +
`now config file "${hp(NOW_CONFIG_PATH)}" ` +
err.message
)
)
return 1
}
let config
if (configExists) {
try {
config = configFiles.readConfigFile()
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to read the ' +
`now config in "${hp(NOW_CONFIG_PATH)}" ` +
err.message
)
)
return 1
}
try {
config = JSON.parse(config)
} catch (err) {
console.error(
error(
`An error occurred while trying to parse "${hp(NOW_CONFIG_PATH)}": ` +
err.message
)
)
return 1
}
} else {
config = getDefaultCfg()
try {
configFiles.writeToConfigFile(config)
initConfig = true
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to write the ' +
`default now config to "${hp(NOW_CONFIG_PATH)}" ` +
err.message
)
)
return 1
}
}
let authConfigExists
try {
authConfigExists = await exists(NOW_AUTH_CONFIG_PATH)
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to find the ' +
`now auth file "${hp(NOW_AUTH_CONFIG_PATH)}" ` +
err.message
)
)
return 1
}
let authConfig = null
if (authConfigExists) {
try {
authConfig = configFiles.readAuthConfigFile()
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to read the ' +
`now auth config in "${hp(NOW_AUTH_CONFIG_PATH)}" ` +
err.message
)
)
return 1
}
try {
authConfig = JSON.parse(authConfig)
if (!Array.isArray(authConfig.credentials)) {
console.error(
error(
`The content of "${hp(NOW_AUTH_CONFIG_PATH)}" is invalid. ` +
'No `credentials` list found inside'
)
)
return 1
}
for (const [i, { provider }] of authConfig.credentials.entries()) {
if (null == provider) {
console.error(
error(
`Invalid credential found in "${hp(NOW_AUTH_CONFIG_PATH)}". ` +
`Missing \`provider\` key in entry with index ${i}`
)
)
return 1
}
if (!(provider in providers)) {
console.error(
error(
`Invalid credential found in "${hp(NOW_AUTH_CONFIG_PATH)}". ` +
`Unknown provider "${provider}"`
)
)
return 1
}
}
} catch (err) {
console.error(
error(
`An error occurred while trying to parse "${hp(
NOW_AUTH_CONFIG_PATH
)}": ` + err.message
)
)
return 1
}
} else {
authConfig = getDefaultAuthCfg()
try {
configFiles.writeToAuthConfigFile(authConfig)
initAuthConfig = true
} catch (err) {
console.error(
error(
'An unexpected error occurred while trying to write the ' +
`default now config to "${hp(NOW_CONFIG_PATH)}" ` +
err.message
)
)
return 1
}
}
if (initConfig || initAuthConfig) {
console.log(
effect(
`Initialized default config in "${initConfig && initAuthConfig
? hp(NOW_DIR)
: hp(initConfig ? NOW_CONFIG_PATH : NOW_AUTH_CONFIG_PATH)}"`
)
)
}
let suppliedProvider = null
// if the target is something like `aws`
if (targetOrSubcommand in providers) {
debug('user supplied a known provider')
const targetPath = join(process.cwd(), targetOrSubcommand)
const targetPathExists = await exists(targetPath)
if (targetPathExists) {
console.error(
error(
`The supplied argument ${param(targetOrSubcommand)} is ambiguous. ` +
'Both a directory and a provider are known'
)
)
return 1
}
suppliedProvider = targetOrSubcommand
targetOrSubcommand = argv._[3]
}
let { defaultProvider = null }: { defaultProvider: ?string } = config
if (null === suppliedProvider) {
if (null === defaultProvider) {
// the first provider the user ever logged in to is
// the default provider
if (authConfig && authConfig.credentials.length) {
debug('using first credential as default provider')
defaultProvider = authConfig.credentials[0].provider
} else {
debug(`fallbacking to default now provider 'sh'`)
defaultProvider = 'sh'
}
} else {
debug('using provider supplied by user', defaultProvider)
if (!(defaultProvider in providers)) {
console.error(
error(
`The \`defaultProvider\` "${defaultProvider}" supplied in ` +
`"${NOW_CONFIG_PATH}" is not a valid provider`
)
)
return 1
}
}
}
// we special case help because it's a generic command with
// information about all providers
if (!suppliedProvider && argv.help) {
console.log(getHelp(defaultProvider, providers))
return 0
}
const provider: Object = providers[suppliedProvider || defaultProvider]
// the context object to supply to the providers
const ctx = {
config,
authConfig,
argv: argv_
}
let subcommand
// we check if we are deploying something
if (targetOrSubcommand) {
const targetPath = join(process.cwd(), targetOrSubcommand)
const targetPathExists = await exists(targetPath)
const subcommandExists =
GLOBAL_COMMANDS.has(targetOrSubcommand) ||
provider.subcommands.has(targetOrSubcommand)
if (targetPathExists && subcommandExists) {
console.error(
error(
`The supplied argument ${param(targetOrSubcommand)} is ambiguous. ` +
'Both a directory and a subcommand are known'
)
)
return 1
}
if (subcommandExists) {
debug('user supplied known subcommand', targetOrSubcommand)
subcommand = targetOrSubcommand
} else {
debug('user supplied a possible target for deployment')
// our default command is deployment
// at this point we're
subcommand = 'deploy'
}
} else {
debug('user supplied no target, defaulting to deploy')
subcommand = 'deploy'
}
if (subcommand === 'deploy' && !authConfig.credentials.length) {
debug('subcommand is deploy, but user has no credentials')
console.log(getWelcome(provider, providers))
return 0
}
if (subcommand === 'help') {
console.log(getHelp(defaultProvider, providers))
return 0
}
try {
return provider[subcommand](ctx)
} catch (err) {
console.error(
error(
`An unexpected error occurred in provider ${subcommand}: ${err.stack}`
)
)
}
}
debug('start')
const handleRejection = err => {
debug('handling rejection')
if (err) {
if (err instanceof Error) {
handleUnexpected(err)
} else {
console.error(error(`An unexpected rejection occurred\n ${err}`))
}
} else {
console.error(error('An unexpected empty rejection occurred'))
}
return 1
}
const handleUnexpected = err => {
debug('handling unexpected error')
console.error(
error(`An unexpected error occurred!\n ${err.stack} ${err.stack}`)
)
return 1
}
process.on('uncaughtRejection', handleRejection)
process.on('uncaughtException', handleUnexpected)
main(process.argv).then((code: number) => exit(code)).catch(handleUnexpected)

362
src/providers/aws/deploy.js

@ -0,0 +1,362 @@
// @flow
// theirs
const ms = require('ms')
const minimist = require('minimist')
const { gray, bold } = require('chalk')
const bytes = require('bytes')
const uid = require('uid-promise')
const debug = require('debug')('now:aws:deploy')
// ours
const resolve = require('../../resolve')
const ok = require('../../util/output/ok')
const wait = require('../../util/output/wait')
const info = require('../../util/output/info')
const error = require('../../util/output/error')
const link = require('../../util/output/link')
const success = require('../../util/output/success')
const param = require('../../util/output/param')
const humanPath = require('../../util/humanize-path')
const build = require('../../serverless/build')
const getLambdaHandler = require('./get-lambda-handler')
const getAWS = require('./get-aws')
const describeProject = require('../../describe-project')
const copyToClipboard = require('../../util/copy-to-clipboard')
const NOW_DEFAULT_IAM_ROLE = 'now-default-role'
const IAM_POLICY_DOCUMENT = {
Version: '2012-10-17',
Statement: [
{
Sid: '',
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com'
},
Action: 'sts:AssumeRole'
}
]
}
const deploy = async ({ config, authConfig, argv: argv_ }) => {
const argv = minimist(argv_, {
boolean: ['help'],
alias: {
help: 'h'
}
})
// `now [provider] [deploy] [target]`
const [cmdOrTarget = null, target_ = null] = argv._.slice(2).slice(-2)
let target
if (cmdOrTarget === 'aws' || cmdOrTarget === 'deploy') {
target = target_ === null ? process.cwd() : target_
} else {
if (target_) {
console.error(error('Unexpected number of arguments for deploy command'))
return 1
} else {
target = cmdOrTarget === null ? process.cwd() : cmdOrTarget
}
}
const start = Date.now()
const resolved = await resolve(target)
if (resolved === null) {
console.error(error(`Could not resolve deployment target ${param(target)}`))
return 1
}
let desc = null
try {
desc = await describeProject(resolved)
} catch (err) {
if (err.code === 'AMBIGOUS_CONFIG') {
console.error(
error(`There is more than one source of \`now\` config: ${err.files}`)
)
return 1
} else {
throw err
}
}
// a set of files that we personalize for this build
const overrides = {
'__now_handler.js': getLambdaHandler(desc)
}
// initialize aws client
const aws = getAWS(authConfig)
const region = aws.config.region || 'us-west-1'
console.log(
info(
`Deploying ${param(humanPath(resolved))} ${gray('(aws)')} ${gray(
`(${region})`
)}`
)
)
const buildStart = Date.now()
const stopBuildSpinner = wait('Building and bundling your app…')
const zipFile = await build(resolved, desc, { overrides })
stopBuildSpinner()
// lambda limits to 50mb
if (zipFile.length > 50 * 1024 * 1024) {
console.error(error('The build exceeds the 50mb AWS Lambda limit'))
return 1
}
console.log(
ok(
`Build generated a ${bold(bytes(zipFile.length))} zip ${gray(
`[${ms(Date.now() - buildStart)}]`
)}`
)
)
const iam = new aws.IAM({ apiVersion: '2010-05-08' })
const gateway = new aws.APIGateway({
apiVersion: '2015-07-09',
region
})
const lambda = new aws.Lambda({
apiVersion: '2015-03-31',
region
})
let role
try {
role = await getRole(iam, { RoleName: NOW_DEFAULT_IAM_ROLE })
} catch (err) {
if ('NoSuchEntity' === err.code) {
const iamStart = Date.now()
role = await createRole(iam, {
AssumeRolePolicyDocument: JSON.stringify(IAM_POLICY_DOCUMENT),
RoleName: NOW_DEFAULT_IAM_ROLE
})
console.log(
ok(
`Initialized IAM role ${param(NOW_DEFAULT_IAM_ROLE)} ${gray(
`[${ms(iamStart - Date.now())}]`
)}`
)
)
} else {
throw err
}
}
const deploymentId = 'now-' + desc.name + '-' + (await uid(10))
const resourcesStart = Date.now()
const stopResourcesSpinner = wait('Creating API resources')
debug('initializing lambda function')
const λ = await createFunction(lambda, {
Code: {
ZipFile: zipFile
},
Runtime: 'nodejs6.10',
Description: desc.description,
FunctionName: deploymentId,
Handler: '__now_handler.handler',
Role: role.Role.Arn,
Timeout: 15,
MemorySize: 512
})
debug('initializing api gateway')
const api = await createAPI(gateway, {
name: deploymentId,
description: desc.description
})
debug('retrieving root resource id')
const resources = await getResources(gateway, {
restApiId: api.id
})
const rootResourceId = resources.items[0].id
debug('initializing gateway method for /')
await putMethod(gateway, {
restApiId: api.id,
authorizationType: 'NONE',
httpMethod: 'ANY',
resourceId: rootResourceId
})
debug('initializing gateway integration for /')
await putIntegration(gateway, {
restApiId: api.id,
resourceId: rootResourceId,
httpMethod: 'ANY',
type: 'AWS_PROXY',
integrationHttpMethod: 'POST',
uri: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${λ.FunctionArn}/invocations`
})
debug('initializing gateway resource')
const resource = await createResource(gateway, {
restApiId: api.id,
parentId: rootResourceId,
pathPart: '{proxy+}'
})
debug('initializing gateway method for {proxy+}')
await putMethod(gateway, {
restApiId: api.id,
authorizationType: 'NONE',
httpMethod: 'ANY',
resourceId: resource.id
})
debug('initializing gateway integration for {proxy+}')
await putIntegration(gateway, {
restApiId: api.id,
resourceId: resource.id,
httpMethod: 'ANY',
type: 'AWS_PROXY',
integrationHttpMethod: 'POST',
uri: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${λ.FunctionArn}/invocations`
})
debug('creating deployment')
await createDeployment(gateway, {
restApiId: api.id,
stageName: 'now'
})
const [, accountId] = role.Role.Arn.match(/^arn:aws:iam::(\d+):/)
await addPermission(lambda, {
FunctionName: deploymentId,
StatementId: deploymentId,
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
SourceArn: `arn:aws:execute-api:${region}:${accountId}:${api.id}/now/ANY/*`
})
stopResourcesSpinner()
console.log(
ok(
`API resources created (id: ${param(deploymentId)}) ${gray(
`[${ms(Date.now() - resourcesStart)}]`
)}`
)
)
const url = `https://${api.id}.execute-api.${region}.amazonaws.com/now`
const copied = copyToClipboard(url, config.copyToClipboard)
console.log(
success(
`${link(url)} ${copied ? gray('(in clipboard)') : ''} ${gray(
`[${ms(Date.now() - start)}]`
)}`
)
)
return 0
}
const getRole = (iam, params) => {
return new Promise((res, reject) => {
iam.getRole(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const createRole = (iam, params) => {
return new Promise((res, reject) => {
iam.createRole(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const createFunction = (lambda, params) => {
return new Promise((res, reject) => {
lambda.createFunction(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const addPermission = (lambda, params) => {
return new Promise((res, reject) => {
lambda.addPermission(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const createAPI = (gateway, params) => {
return new Promise((res, reject) => {
gateway.createRestApi(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const getResources = (gateway, params) => {
return new Promise((res, reject) => {
gateway.getResources(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const createResource = (gateway, params) => {
return new Promise((res, reject) => {
gateway.createResource(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const putMethod = (gateway, params) => {
return new Promise((res, reject) => {
gateway.putMethod(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const putIntegration = (gateway, params) => {
return new Promise((res, reject) => {
gateway.putIntegration(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
const createDeployment = (gateway, params) => {
return new Promise((res, reject) => {
gateway.createDeployment(params, (err, data) => {
if (err) return reject(err)
res(data)
})
})
}
module.exports = deploy

18
src/providers/aws/get-aws.js

@ -0,0 +1,18 @@
const aws = require('aws-sdk')
const getAWS = authConfig => {
const { credentials } = authConfig
const awsCredentials: Object = credentials.find(c => c.provider === 'aws')
if (awsCredentials.useVendorConfig) {
aws.config.credentials = new aws.SharedIniFileCredentials()
} else {
aws.config = new aws.Config()
aws.config.accessKeyId = awsCredentials.accessKeyId
aws.config.secretAccessKey = awsCredentials.secretAccessKey
}
return aws
}
module.exports = getAWS

61
src/providers/aws/get-lambda-handler.js

@ -0,0 +1,61 @@
const getHandler = require('../../serverless/get-handler')
// generate the handler that we'll use as the ƛ function
const getLambdaHandler = desc => {
// the command that our handler will invoke to fire up
// the user-suppled HTTP server
let cmd = null
let script = null
if (desc.packageJSON) {
if (desc.packageJSON.scripts && desc.packageJSON.scripts.start) {
cmd = desc.packageJSON.scripts.start
} else {
// `node .` will use `main` or fallback to `index.js`
script = './'
}
} else {
if (desc.hasServerJSFile) {
script = 'server.js'
} else {
script = 'index.js'
}
}
return getHandler({ script, cmd }, (makeRequest, getPort, req, ctx, fn) => {
const url =
req.path +
'?' +
require('querystring').stringify(req.queryStringParameters)
const proxy = makeRequest(
{
port: getPort(),
hostname: '127.0.0.1',
path: url,
method: req.httpMethod,
headers: req.headers
},
proxyRes => {
let body = ''
proxyRes.on('data', data => {
body += data
})
proxyRes.on('error', err => {
fn(err)
body = ''
})
proxyRes.on('end', () => {
fn(null, {
statusCode: proxyRes.statusCode,
headers: proxyRes.headers,
body
})
})
}
)
proxy.on('error', fn)
proxy.end(req.body)
})
}
module.exports = getLambdaHandler

5
src/providers/aws/help.js

@ -0,0 +1,5 @@
const help = () => {
console.log('halp')
}
module.exports = help

13
src/providers/aws/index.js

@ -0,0 +1,13 @@
module.exports = {
title: 'AWS Lambda',
subcommands: new Set(['help', 'login', 'deploy', 'ls']),
get deploy() {
return require('./deploy')
},
get help() {
return require('./help')
},
get login() {
return require('./login')
}
}

162
src/providers/aws/login.js

@ -0,0 +1,162 @@
// @flow
// theirs
const { green, italic } = require('chalk')
// ours
const info = require('../../util/output/info')
const note = require('../../util/output/note')
const aborted = require('../../util/output/aborted')
const cmd = require('../../util/output/cmd')
const param = require('../../util/output/param')
const ready = require('../../util/output/ready')
const highlight = require('../../util/output/highlight')
const listItem = require('../../util/output/list-item')
const link = require('../../util/output/link')
const textInput = require('../../util/input/text')
const eraseLines = require('../../util/output/erase-lines')
const chars = require('../../util/output/chars')
const {
hasExternalCredentials,
getExternalCredentials,
AWS_CREDENTIALS_FILE_PATH
} = require('./util/external-credentials')
const promptBool = require('../../util/input/prompt-bool')
const {
writeToAuthConfigFile,
getAuthConfigFilePath
} = require('../../util/config-files')
const humanize = require('../../util/humanize-path')
const accessKeyIdLabel = 'Access Key ID '
const secretAccessKeyLabel = 'Secret Access Key '
function saveCredentials({
ctx,
accessKeyId,
secretAccessKey,
useExternal,
credentialsIndex
}) {
const obj = {
provider: 'aws'
}
if (useExternal) {
obj.useVendorConfig = true
} else {
obj.accessKeyId = accessKeyId
obj.secretAccessKey = secretAccessKey
}
if (credentialsIndex === -1) {
// the user is not logged in
ctx.authConfig.credentials.push(obj)
} else {
// the user is already logged in - let's replace the credentials we have
ctx.authConfig.credentials[credentialsIndex] = obj
}
writeToAuthConfigFile(ctx.authConfig)
return ctx
}
async function login(ctx) {
const credentialsIndex = ctx.authConfig.credentials.findIndex(
cred => cred.provider === 'aws'
)
if (credentialsIndex !== -1) {
// the user is already logged in on aws
console.log(
note(`You already have AWS credentials – this will replace them.`)
)
}
if (await hasExternalCredentials()) {
// if the user has ~/.aws/credentials, let's ask if they want to use them
const credentials = await getExternalCredentials()
if (credentials.accessKeyId && credentials.secretAccessKey) {
let yes
try {
yes = await promptBool(
info(
`AWS credentials found in ${param(AWS_CREDENTIALS_FILE_PATH)}.`,
` Would you like to use them?`
),
{
defaultValue: true
}
)
} catch (err) {
if (err.code === 'USER_ABORT') {
console.log(aborted('No changes made.'))
return 1
}
throw err
}
if (yes) {
ctx = saveCredentials({ ctx, useExternal: true, credentialsIndex })
console.log(
ready(`Credentials will be read from your AWS config when needed`)
)
return
} else {
console.log(info(`Ignoring ${param(AWS_CREDENTIALS_FILE_PATH)}`))
}
}
}
// prettier-ignore
console.log(info(
`We'll need your ${highlight('AWS credentials')} in order to comunicate with their API.`,
` To provision a dedicated set of tokens for ${cmd('now')}, do the following:`,
``,
` ${listItem(1, `Go to ${link('https://console.aws.amazon.com/iam')}`)}`,
` ${listItem(2, `Click on ${param('Users')} in the left menubar`)}`,
` ${listItem(3, `Click on ${param('Add user')}`)}`,
` ${listItem(4, `Give your user a name and select ${param('Programmatic access')}`)}`,
` ${listItem(5, `In the ${param('Permissions')} step, select\n` +
` ${param('Attach existing policies directly')}\n` +
` and then\n` +
` ${param('AdministratorAccess')}`)} ${italic('(or pick your own)')}`,
` ${listItem(6, `After the ${param('Review')} step, grab your keys and paste them below:`)}`,
``
))
try {
const accessKeyId = await textInput({ label: listItem(accessKeyIdLabel) })
console.log(
`${eraseLines(1)}${green(chars.tick)} ${accessKeyIdLabel}${accessKeyId}`
)
const secretAccessKey = await textInput({
label: listItem(secretAccessKeyLabel)
})
console.log(
`${eraseLines(1)}${green(
chars.tick
)} ${secretAccessKeyLabel}${secretAccessKey}`
)
ctx = saveCredentials({
ctx,
accessKeyId,
secretAccessKey,
credentialsIndex
})
console.log(
ready(`Credentials saved in ${param(humanize(getAuthConfigFilePath()))}`)
)
} catch (err) {
if (err.code === 'USER_ABORT') {
console.log(aborted('No changes made.'))
return 1
}
throw err
}
}
module.exports = login

64
src/providers/aws/util/external-credentials.js

@ -0,0 +1,64 @@
// node
const { join: joinPath } = require('path')
const { homedir } = require('os')
// theirs
const { readFile, exists: fileExists } = require('fs.promised')
const debug = require('debug')('now:aws:util:external-credentials')
const AWS_CREDENTIALS_FILE_PATH = joinPath(homedir(), '.aws', 'credentials')
// matches `aws_access_key_id=aaaaa`
// and `aws_access_key_id = aaaaa` with any number of spaces
const ACCESS_KEY_ID_REGEX = /^aws_access_key_id(\s+)?=(\s+)?(.*)$/m
const SECRET_ACCESS_KEY_REGEX = /^aws_secret_access_key(\s+)?=(\s+)?(.*)$/m
// checks if there's a ~/.aws/credentials
async function hasExternalCredentials() {
let found = false
try {
found = await fileExists(AWS_CREDENTIALS_FILE_PATH)
} catch (err) {
// if this happens, we're fine:
// 1. if the user is trying to login, let's just fallback to the manual
// steps
// 2. if it's the Nth time the user is using `now aws`, we know we depend
// on this file and we'll let him know that we couldn't find the file
// anymore upon `hasExternalCredentials() === false`
debug(`Couldn't read ${AWS_CREDENTIALS_FILE_PATH} because of ${err}`)
}
return found
}
// gets the two aws tokens from ~/.aws/credentials
// assumes the file exist – `hasExternalCredentials` should always be called
// first
async function getExternalCredentials() {
let contents
try {
contents = await readFile(AWS_CREDENTIALS_FILE_PATH, 'utf8')
} catch (err) {
// Here we should error because the file is there but we can't read it
throw new Error(
`Couldn't read ${AWS_CREDENTIALS_FILE_PATH} beause of ${err.message}`
)
}
const matchesAccessKeyId = ACCESS_KEY_ID_REGEX.exec(contents)
const matchesSecretAccessKey = SECRET_ACCESS_KEY_REGEX.exec(contents)
return {
accessKeyId: (matchesAccessKeyId && matchesAccessKeyId[3]) || undefined,
secretAccessKey:
(matchesSecretAccessKey && matchesSecretAccessKey[3]) || undefined
}
}
module.exports = {
hasExternalCredentials,
getExternalCredentials,
AWS_CREDENTIALS_FILE_PATH:
process.platform === 'win32'
? AWS_CREDENTIALS_FILE_PATH
: AWS_CREDENTIALS_FILE_PATH.replace(homedir(), '~')
}

285
src/providers/gcp/deploy.js

@ -0,0 +1,285 @@
// @flow
// theirs
const ms = require('ms')
const fetch = require('node-fetch')
const minimist = require('minimist')
const { gray, bold } = require('chalk')
const uid = require('uid-promise')
const bytes = require('bytes')
const sleep = require('then-sleep')
const debug = require('debug')('now:gcp:deploy')
// ours
const ok = require('../../util/output/ok')
const info = require('../../util/output/info')
const wait = require('../../util/output/wait')
const link = require('../../util/output/link')
const success = require('../../util/output/success')
const humanPath = require('../../util/humanize-path')
const resolve = require('../../resolve')
const error = require('../../util/output/error')
const param = require('../../util/output/param')
const build = require('../../serverless/build')
const getToken = require('./util/get-access-token')
const describeProject = require('../../describe-project')
const copyToClipboard = require('../../util/copy-to-clipboard')
const getFunctionHandler = require('./util/get-function-handler')
const BUCKET_NAME = 'now-deployments'
const deploy = async ctx => {
const { argv: argv_ } = ctx
const argv = minimist(argv_, {
boolean: ['help'],
alias: {
help: 'h'
}
})
const token = await getToken(ctx)
// `now [provider] [deploy] [target]`
const [cmdOrTarget = null, target_ = null] = argv._.slice(2).slice(-2)
let target
if (cmdOrTarget === 'gcp' || cmdOrTarget === 'deploy') {
target = target_ === null ? process.cwd() : target_
} else {
if (target_) {
console.error(error('Unexpected number of arguments for deploy command'))
return 1
} else {
target = cmdOrTarget === null ? process.cwd() : cmdOrTarget
}
}
const start = Date.now()
const resolved = await resolve(target)
if (resolved === null) {
console.error(error(`Could not resolve deployment target ${param(target)}`))
return 1
}
let desc = null
try {
desc = await describeProject(resolved)
} catch (err) {
if (err.code === 'AMBIGOUS_CONFIG') {
console.error(
error(`There is more than one source of \`now\` config: ${err.files}`)
)
return 1
} else {
throw err
}
}
const overrides = {
'function.js': getFunctionHandler(desc)
}
const region = 'us-central1'
console.log(
info(
`Deploying ${param(humanPath(resolved))} ${gray('(gcp)')} ${gray(
`(${region})`
)}`
)
)
const buildStart = Date.now()
const stopBuildSpinner = wait('Building and bundling your app…')
const zipFile = await build(resolved, desc, { overrides })
stopBuildSpinner()
if (zipFile.length > 100 * 1024 * 1024) {
console.error(error('The build exceeds the 100mb GCP Functions limit'))
return 1
}
console.log(
ok(
`Build generated a ${bold(bytes(zipFile.length))} zip ${gray(
`[${ms(Date.now() - buildStart)}]`
)}`
)
)
const deploymentId = 'now-' + desc.name + '-' + (await uid(10))
const zipFileName = `${deploymentId}.zip`
const { project } = ctx.authConfig.credentials.find(p => p.provider === 'gcp')
const resourcesStart = Date.now()
const stopResourcesSpinner = wait('Creating API resources')
debug('creating gcp storage bucket')
const bucketRes = await fetch(
`https://www.googleapis.com/storage/v1/b?project=${project.id}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: BUCKET_NAME
})
}
)
if (
bucketRes.status !== 200 &&
bucketRes.status !== 409 /* already exists */
) {
console.error(
error(
`Error while creating GCP Storage bucket: ${await bucketRes.text()}`
)
)
return 1
}
debug('creating gcp storage file')
const fileRes = await fetch(
`https://www.googleapis.com/upload/storage/v1/b/${BUCKET_NAME}/o?uploadType=media&name=${encodeURIComponent(
zipFileName
)}&project=${encodeURIComponent(project.id)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/zip',
'Content-Length': zipFile.length,
Authorization: `Bearer ${token}`
},
body: zipFile
}
)
try {
await assertSuccessfulResponse(fileRes)
} catch (err) {
console.error(error(err.message))
return 1
}
debug('creating gcp function create')
const fnCreateRes = await fetch(
`https://cloudfunctions.googleapis.com/v1beta2/projects/${project.id}/locations/${region}/functions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: `projects/${project.id}/locations/${region}/functions/${deploymentId}`,
timeout: '15s',
availableMemoryMb: 512,
sourceArchiveUrl: `gs://${BUCKET_NAME}/${zipFileName}`,
entryPoint: 'handler',
httpsTrigger: {
url: null
}
})
}
)
if (403 === fnCreateRes.status) {
const url = `https://console.cloud.google.com/apis/api/cloudfunctions.googleapis.com/overview?project=${project.id}`
console.error(
error(
'GCP Permission Denied error. Make sure the "Google Cloud Functions API" ' +
`is enabled in the API Manager\n ${bold('API Manager URL')}: ${link(
url
)}`
)
)
return 1
}
try {
await assertSuccessfulResponse(fnCreateRes)
} catch (err) {
console.error(error(err.message))
return 1
}
let retriesLeft = 10
let status
let url
do {
if (!--retriesLeft) {
console.error(
error('Could not determine status of the deployment: ' + url)
)
return 1
} else {
await sleep(5000)
}
const fnRes = await fetch(
`https://cloudfunctions.googleapis.com/v1beta2/projects/${project.id}/locations/${region}/functions/${deploymentId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`
}
}
)
try {
await assertSuccessfulResponse(fnRes)
} catch (err) {
console.error(error(err.message))
return 1
}
;({ status, httpsTrigger: { url } } = await fnRes.json())
} while (status !== 'READY')
stopResourcesSpinner()
console.log(
ok(
`API resources created (id: ${param(deploymentId)}) ${gray(
`[${ms(Date.now() - resourcesStart)}]`
)}`
)
)
const copied = copyToClipboard(url, ctx.config.copyToClipboard)
console.log(
success(
`${link(url)} ${copied ? gray('(in clipboard)') : ''} ${gray(
`[${ms(Date.now() - start)}]`
)}`
)
)
return 0
}
const assertSuccessfulResponse = async res => {
if (!res.ok) {
let msg
let body
try {
body = await res.json()
} catch (err) {
msg = `An API error was returned (${res.status}), but the error code could not be diagnosed`
}
msg = body.error.message
throw new Error(msg)
}
}
module.exports = deploy

5
src/providers/gcp/help.js

@ -0,0 +1,5 @@
const help = () => {
console.log('gcp help')
}
module.exports = help

18
src/providers/gcp/index.js

@ -0,0 +1,18 @@
module.exports = {
title: 'Google Cloud Platform',
subcommands: new Set(['help', 'login', 'deploy', 'ls']),
// we use getters for commands to lazily get code
// and not bog down initialization
get help() {
return require('./help')
},
get deploy() {
return require('./deploy')
},
get login() {
return require('./login')
}
}

19
src/providers/gcp/list-projects.js

@ -0,0 +1,19 @@
// ours
const fetch = require('./util/fetch')
const getToken = require('./util/get-access-token')
const URL = 'https://cloudresourcemanager.googleapis.com/v1/projects'
const projectsLs = async ctx => {
const token = await getToken(ctx)
if (!token) {
return 1
}
const { projects } = await fetch({ url: URL, token })
return projects
}
module.exports = projectsLs

276
src/providers/gcp/login.js

@ -0,0 +1,276 @@
// node
const { parse: parseUrl } = require('url')
const { encode: encodeQuery, stringify: formUrlEncode } = require('querystring')
const { createServer } = require('http')
// theirs
const opn = require('opn')
const fetch = require('node-fetch')
const debug = require('debug')('now:gcp:login')
// ours
const error = require('../../util/output/error')
const aborted = require('../../util/output/aborted')
const info = require('../../util/output/info')
const ready = require('../../util/output/ready')
const param = require('../../util/output/param')
const promptBool = require('../../util/input/prompt-bool')
const getNowDir = require('../../get-now-dir')
const humanize = require('../../util/humanize-path')
const saveCredentials = require('./util/save-credentials')
const promptList = require('../../util/input/list')
const listProjects = require('./list-projects')
const { writeToAuthConfigFile } = require('../../util/config-files')
// ports that are authorized in the GCP app
const PORTS = [8085, 8086, 8087, 8088]
const CLIENT_ID =
'258013614557-0qulvq65vqk8pi9akn7igqsquejjffil.apps.googleusercontent.com'
const CLIENT_SECRET = 'SvmeeRFmKQkIe_ZQHSe1UJ-O'
// instructs gcp to send the response in the query string
const RESPONSE_TYPE = 'code'
const SCOPES = [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/appengine.admin',
'https://www.googleapis.com/auth/compute',
'https://www.googleapis.com/auth/accounts.reauth'
]
// instructs gcp to return a `refresh_token` that we'll use to seamlessly
// get a new auth token every time the current one expires
const ACCESS_TYPE = 'offline'
// url we'll send the user to
const USER_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
// url we'll get the access tokens from and refresh the token when needed
const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
// required by oauth2's spec
const GRANT_TYPE = 'authorization_code'
// this ensures google *always* asks the user for permission
// we enfore this to make sure we *always* receive a `refresh_token` (if the
// is already authorized by the user, a `refresh_token` will *not*
// be returned) since we need it
const PROMPT_CONSENT = 'consent'
const serverListen = ({ server, port }) => {
return new Promise((resolve, reject) => {
server.on('error', reject) // will happen if the port is already in use
server.listen(port, resolve)
})
}
function login(ctx) {
return new Promise(async resolve => {
let credentialsIndex = ctx.authConfig.credentials.findIndex(
cred => cred.provider === 'gcp'
)
if (credentialsIndex !== -1) {
// the user is already logged into gcp
let yes
try {
yes = await promptBool(
info(
`You already have GCP credentials – this will replace them.`,
` Do you want to continue?`
)
)
} catch (err) {
// promptBool only `reject`s upon user abort
// let's set it to false just to make it clear
yes = false
}
if (!yes) {
console.log(aborted('No changes made.'))
resolve(0)
}
}
const ports = [...PORTS]
const server = createServer(async function handleRequest(req, res) {
const { query: { error: _error, code } } = parseUrl(req.url, true)
if (!_error && !code) {
// the browser requesting the favicon etc
res.end('')
return
}
res.setHeader('content-type', 'text/html')
res.end(
`<meta charset="UTF-8">` +
`<h2>That's it – you can now return to your terminal!</h2>`
)
if (_error) {
// the user didn't give us permission
console.log(aborted(`No changes made.`))
return resolve(1)
}
if (code) {
// that's right after the user gave us permission
// let's exchange the authorization code for an access + refresh codes
const body = formUrlEncode({
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: `http://${req.headers.host}`,
grant_type: GRANT_TYPE
})
const opts = {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'content-length': body.length // just in case
},
body: body
}
let accessToken
let expiresIn
let refreshToken
let response
try {
response = await fetch(TOKEN_URL, opts)
if (response.status !== 200) {
debug(
`HTTP ${response.status} when trying to exchange the authorization code`,
await response.text()
)
console.log(
error(
`Got unexpected status code from Google: ${response.status}`
)
)
return resolve(1)
}
} catch (err) {
debug(
'unexpected error occurred while making the request to exchange the authorization code',
err.message
)
console.log(
error(
'Unexpected error occurred while authenthing with Google',
err.stack
)
)
return resolve(1)
}
try {
const json = await response.json()
accessToken = json.access_token
expiresIn = json.expires_in
refreshToken = json.refresh_token
} catch (err) {
debug(
'unexpected error occurred while parsing the JSON from the exchange request',
err.stack,
'got',
await response.text()
)
console.log(
error(
'Unexpected error occurred while parsing the JSON response from Google',
err.message
)
)
resolve(1)
}
const now = new Date()
// `expires_in` is 3600 seconds
const expiresAt = now.setSeconds(now.getSeconds() + expiresIn)
ctx = saveCredentials({
ctx,
accessToken,
expiresAt,
refreshToken,
credentialsIndex
})
const projects = await listProjects(ctx)
const message = 'Select a project:'
const choices = projects.map(project => {
return {
name: `${project.name} (${project.projectId})`,
value: project.projectId,
short: project.name
}
})
const projectId = await promptList({
message,
choices,
separator: false
})
const { projectId: id, name } = projects.find(
p => p.projectId === projectId
)
credentialsIndex = ctx.authConfig.credentials.findIndex(
cred => cred.provider === 'gcp'
)
ctx.authConfig.credentials[credentialsIndex].project = {
id,
name
}
writeToAuthConfigFile(ctx.authConfig)
console.log(
ready(
`Credentials and project saved in ${param(humanize(getNowDir()))}.`
)
)
resolve(1)
}
})
let shouldRetry = true
let portToTry = ports.shift()
while (shouldRetry) {
try {
await serverListen({ server, port: portToTry })
shouldRetry = false // done, listening
} catch (err) {
if (ports.length) {
// let's try again
portToTry = ports.shift()
} else {
// we're out of ports to try
shouldRetry = false
}
}
}
if (!server.listening) {
console.log(
error(
`Make sure you have one of the following TCP ports available:`,
` ${PORTS.join(', ').replace()}`
)
)
return resolve(1)
}
const query = {
client_id: CLIENT_ID,
redirect_uri: `http://localhost:${portToTry}`,
response_type: RESPONSE_TYPE,
scope: SCOPES.join(' '),
access_type: ACCESS_TYPE,
prompt: PROMPT_CONSENT
}
opn(USER_URL + '?' + encodeQuery(query))
})
}
module.exports = login

24
src/providers/gcp/util/fetch.js

@ -0,0 +1,24 @@
// node
const { encode: encodeQuery } = require('querystring')
// theirs
const _fetch = require('node-fetch')
const fetch = async ({ url, method = 'GET', token, query }) => {
url = query ? url + '?' + encodeQuery(query) : url
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
const res = await _fetch(url, {
method,
headers
})
const json = await res.json()
return json
}
module.exports = fetch

121
src/providers/gcp/util/get-access-token.js

@ -0,0 +1,121 @@
// node
const { stringify: formUrlEncode } = require('querystring')
// theirs
const fetch = require('node-fetch')
const debug = require('debug')('now:gcp:get_token')
// ours
const saveCredentials = require('./save-credentials')
const error = require('../../../util/output/error')
const cmd = require('../../../util/output/cmd')
const CLIENT_ID =
'258013614557-0qulvq65vqk8pi9akn7igqsquejjffil.apps.googleusercontent.com'
const CLIENT_SECRET = 'SvmeeRFmKQkIe_ZQHSe1UJ-O'
// required by oauth2's spec
const GRANT_TYPE = 'refresh_token'
const URL = 'https://www.googleapis.com/oauth2/v4/token'
// note that this function treats the errors it can produce, printing them
// to the user and then returns `undefined`
const getAccessToken = async ctx => {
const credentialsIndex = ctx.authConfig.credentials.findIndex(
c => c.provider === 'gcp'
)
if (credentialsIndex === -1) {
console.log(error(`You're not logged in! Run ${cmd('now gcp login')}.`))
return
}
const { accessToken, expiresAt, refreshToken } = ctx.authConfig.credentials[
credentialsIndex
]
if (Date.now() < expiresAt) {
// the token is still valid
return accessToken
}
// we need to refresh the token
const body = formUrlEncode({
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: GRANT_TYPE
})
const opts = {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'content-length': body.length // just in case
},
body: body
}
let newAccessToken
let newExpiresIn
let response
try {
response = await fetch(URL, opts)
if (response.status !== 200) {
debug(
`HTTP ${response.status} when trying to exchange the authorization code`,
await response.text()
)
console.log(
error(`Got unexpected status code from Google: ${response.status}`)
)
return
}
} catch (err) {
debug(
'unexpected error occurred while making the request to exchange the authorization code',
err.message
)
console.log(
error(
'Unexpected error occurred while authenthing with Google',
err.stack
)
)
return
}
try {
const json = await response.json()
newAccessToken = json.access_token
newExpiresIn = json.expires_in
} catch (err) {
debug(
'unexpected error occurred while parsing the JSON from the exchange request',
err.stack,
'got',
await response.text()
)
console.log(
error(
'Unexpected error occurred while parsing the JSON response from Google',
err.message
)
)
return
}
const now = new Date()
// `expires_in` is 3600 seconds
const newExpiresAt = now.setSeconds(now.getSeconds() + newExpiresIn)
saveCredentials({
ctx,
accessToken: newAccessToken,
expiresAt: newExpiresAt,
refreshToken,
credentialsIndex
})
return newAccessToken
}
module.exports = getAccessToken

64
src/providers/gcp/util/get-function-handler.js

@ -0,0 +1,64 @@
const getHandler = require('../../../serverless/get-handler')
const getFunctionHandler = desc => {
// the command that our handler will invoke to fire up
// the user-suppled HTTP server
let cmd = null
let script = null
if (desc.packageJSON) {
if (desc.packageJSON.scripts && desc.packageJSON.scripts.start) {
cmd = desc.packageJSON.scripts.start
} else {
// `node .` will use `main` or fallback to `index.js`
script = './'
}
} else {
if (desc.hasServerJSFile) {
script = 'server.js'
} else {
script = 'index.js'
}
}
return getHandler({ cmd, script }, (makeRequest, getPort, req, res) => {
let body
if ('object' === typeof req.body && !(body instanceof Buffer)) {
body = JSON.stringify(req.body)
} else {
body = req.body
}
console.log('got request', req.url, req.method, req.headers)
const proxyRequest = makeRequest(
{
port: getPort(),
hostname: '127.0.0.1',
// TODO: figure out how to get the path?
path: req.url,
method: req.method,
headers: req.headers
},
proxyRes => {
proxyRes.on('data', data => {
res.write(data)
})
proxyRes.on('error', err => {
console.error(err)
res.status(500).end()
})
proxyRes.on('end', () => {
res.end()
})
}
)
proxyRequest.on('error', err => {
console.error(err)
res.status(500).end()
})
proxyRequest.end(body)
})
}
module.exports = getFunctionHandler

31
src/providers/gcp/util/save-credentials.js

@ -0,0 +1,31 @@
const { writeToAuthConfigFile } = require('../../../util/config-files')
const saveCredentials = ({
ctx,
accessToken,
expiresAt,
refreshToken,
credentialsIndex
}) => {
const current = ctx.authConfig.credentials[credentialsIndex] || {}
const obj = Object.assign({}, current, {
provider: 'gcp',
accessToken,
expiresAt,
refreshToken
})
if (credentialsIndex === -1) {
// the user is not logged in
ctx.authConfig.credentials.push(obj)
} else {
// the user is already logged in - let's replace the credentials we have
ctx.authConfig.credentials[credentialsIndex] = obj
}
writeToAuthConfigFile(ctx.authConfig)
return ctx
}
module.exports = saveCredentials

6
src/providers/index.js

@ -0,0 +1,6 @@
// @flow
module.exports = {
sh: require('./sh'),
aws: require('./aws'),
gcp: require('./gcp')
}

853
src/providers/sh/deploy.js

@ -0,0 +1,853 @@
#!/usr/bin/env node
// Native
const { resolve, basename } = require('path')
// Packages
const Progress = require('progress')
const fs = require('fs-extra')
const bytes = require('bytes')
const chalk = require('chalk')
const minimist = require('minimist')
const ms = require('ms')
const dotenv = require('dotenv')
const { eraseLines } = require('ansi-escapes')
const { write: copy } = require('clipboardy')
const inquirer = require('inquirer')
// Ours
const Logger = require('./legacy/build-logger')
const Now = require('./legacy/now.js')
const toHumanPath = require('../../util/humanize-path')
const { handleError, error } = require('./legacy/error')
const { fromGit, isRepoPath, gitPathParts } = require('./legacy/git')
const readMetaData = require('./legacy/read-metadata')
const checkPath = require('./legacy/check-path')
const logo = require('../../util/output/logo')
const cmd = require('../../util/output/cmd')
const info = require('../../util/output/info')
const wait = require('../../util/output/wait')
const NowPlans = require('./legacy/plans')
const promptBool = require('../../util/input/prompt-bool')
const promptOptions = require('./legacy/prompt-options')
const note = require('../../util/output/note')
const minimistOpts = {
string: ['config', 'token', 'name', 'alias', 'session-affinity'],
boolean: [
'help',
'version',
'debug',
'force',
'links',
'login',
'no-clipboard',
'forward-npm',
'docker',
'npm',
'static'
],
alias: {
env: 'e',
dotenv: 'E',
help: 'h',
config: 'c',
debug: 'd',
version: 'v',
force: 'f',
token: 't',
forceSync: 'F',
links: 'l',
login: 'L',
public: 'p',
'no-clipboard': 'C',
'forward-npm': 'N',
'session-affinity': 'S',
name: 'n',
alias: 'a'
}
}
const help = () => {
console.log(`
${chalk.bold(`${logo()} now`)} [options] <command | path>
${chalk.dim('Commands:')}
${chalk.dim('Cloud')}
deploy [path] Performs a deployment ${chalk.bold(
'(default)'
)}
ls | list [app] List deployments
rm | remove [id] Remove a deployment
ln | alias [id] [url] Configures aliases for deployments
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
logs [url] Displays the logs for a deployment
scale [args] Scales the instance count of a deployment
help [cmd] Displays complete help for [cmd]
${chalk.dim('Administrative')}
billing | cc [cmd] Manages your credit cards and billing methods
upgrade | downgrade [plan] Upgrades or downgrades your plan
teams [team] Manages your teams
switch Switches between teams and your account
login Login into your account or creates a new one
logout Logout from your account
${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.
-E ${chalk.underline('FILE')}, --dotenv=${chalk.underline(
'FILE'
)} Include env vars from .env file. Defaults to '.env'
-C, --no-clipboard Do not attempt to copy URL to clipboard
-N, --forward-npm Forward login information to install private npm modules
--session-affinity Session affinity, \`ip\` (default) or \`random\` to control session affinity.
${chalk.dim(
'Enforcable Types (when both package.json and Dockerfile exist):'
)}
--npm Node.js application
--docker Docker container
--static Static file hosting
${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('–')} Deploys a GitHub repository
${chalk.cyan('$ now user/repo#ref')}
${chalk.gray('–')} Deploys a GitHub, GitLab or Bitbucket repo using its URL
${chalk.cyan('$ now https://gitlab.com/user/repo')}
${chalk.gray('–')} Deploys with ENV vars
${chalk.cyan(
'$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password'
)}
${chalk.gray('–')} Displays comprehensive help for the subcommand ${chalk.dim(
'`list`'
)}
${chalk.cyan('$ now help list')}
`)
}
let argv
let path
// Options
let forceNew
let deploymentName
let sessionAffinity
let debug
let clipboard
let forwardNpm
let forceSync
let followSymlinks
let wantsPublic
let apiUrl
let isTTY
let quiet
let alwaysForwardNpm
// If the current deployment is a repo
const gitRepo = {}
const stopDeployment = msg => {
handleError(msg)
process.exit(1)
}
const envFields = async list => {
const questions = []
for (const field of list) {
questions.push({
name: field,
message: field
})
}
// eslint-disable-next-line import/no-unassigned-import
require('../../lib/util/input/patch-inquirer')
console.log(
info('Please enter the values for the following environment variables:')
)
const answers = await inquirer.prompt(questions)
for (const answer in answers) {
if (!{}.hasOwnProperty.call(answers, answer)) {
continue
}
const content = answers[answer]
if (content === '') {
stopDeployment(`Enter a value for ${answer}`)
}
}
return answers
}
async function main(ctx) {
argv = minimist(ctx.argv.slice(2), minimistOpts)
// very ugly hack – this (now-cli's code) expects that `argv._[0]` is the path
// we should fix this ASAP
if (argv._[0] === 'sh') {
argv._.shift()
}
if (argv._[0] === 'deploy') {
argv._.shift()
}
if (path) {
// If path is relative: resolve
// if path is absolute: clear up strange `/` etc
path = resolve(process.cwd(), path)
} else {
path = process.cwd()
}
// Options
forceNew = argv.force
deploymentName = argv.name
sessionAffinity = argv['session-affinity']
debug = argv.debug
clipboard = !argv['no-clipboard']
forwardNpm = argv['forward-npm']
forceSync = argv.forceSync
followSymlinks = !argv.links
wantsPublic = argv.public
apiUrl = argv.url || 'https://api.zeit.co'
isTTY = process.stdout.isTTY
quiet = !isTTY
if (argv.h || argv.help) {
help()
return 0
}
let { token } =
ctx.authConfig.credentials.find(c => c.provider === 'sh') || {}
if (!token) {
// node file sh [...]
const sh = argv[2] === 'sh'
const _cmd = `now ${sh ? 'sh ' : ''}login`
console.log(error(`You're not logged in! Please run ${cmd(_cmd)}`))
return 1
}
const config = ctx.config.sh
alwaysForwardNpm = config.forwardNpm
if (argv.config) {
// TODO enable this
// cfg.setConfigFile(argv.config)
}
try {
return sync({ token, config })
} catch (err) {
return stopDeployment(err)
}
}
async function sync({ token, config: { currentTeam, user } }) {
return new Promise(async (_resolve, reject) => {
const start = Date.now()
const rawPath = argv._[0]
const planPromise = new NowPlans({
apiUrl,
token,
debug,
currentTeam
}).getCurrent()
try {
await fs.stat(path)
} catch (err) {
let repo
let isValidRepo = false
try {
isValidRepo = isRepoPath(rawPath)
} catch (_err) {
if (err.code === 'INVALID_URL') {
stopDeployment(_err)
} else {
reject(_err)
}
}
if (isValidRepo) {
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) {
// why is this ignored?
}
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) {
const gitRef = gitRepo.ref ? `with "${chalk.bold(gitRepo.ref)}" ` : ''
stopDeployment(
`There's no repository named "${chalk.bold(
gitRepo.main
)}" ${gitRef}on ${gitRepo.type}`
)
} else {
error(`The specified directory "${basename(path)}" doesn't exist.`)
process.exit(1)
}
}
// Make sure that directory is deployable
try {
await checkPath(path)
} catch (err) {
error(err)
return
}
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} under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
)
} else {
console.log(
`> Deploying ${chalk.bold(toHumanPath(path))} under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
)
}
}
let deploymentType
// CLI deployment type explicit overrides
if (argv.docker) {
if (debug) {
console.log(`> [debug] Forcing \`deploymentType\` = \`docker\``)
}
deploymentType = 'docker'
} else if (argv.npm) {
if (debug) {
console.log(`> [debug] Forcing \`deploymentType\` = \`npm\``)
}
deploymentType = 'npm'
} else if (argv.static) {
if (debug) {
console.log(`> [debug] Forcing \`deploymentType\` = \`static\``)
}
deploymentType = 'static'
}
let meta
;({
meta,
deploymentName,
deploymentType,
sessionAffinity
} = await readMeta(path, deploymentName, deploymentType, sessionAffinity))
const nowConfig = meta.nowConfig
const now = new Now({ apiUrl, token, debug, currentTeam })
let dotenvConfig
let dotenvOption
if (argv.dotenv) {
dotenvOption = argv.dotenv
} else if (nowConfig && nowConfig.dotenv) {
dotenvOption = nowConfig.dotenv
}
if (dotenvOption) {
const dotenvFileName =
typeof dotenvOption === 'string' ? dotenvOption : '.env'
if (!fs.existsSync(dotenvFileName)) {
error(`--dotenv flag is set but ${dotenvFileName} file is missing`)
return process.exit(1)
}
const dotenvFile = await fs.readFile(dotenvFileName)
dotenvConfig = dotenv.parse(dotenvFile)
}
let pkgEnv = nowConfig && nowConfig.env
const argEnv = [].concat(argv.env || [])
if (pkgEnv && Array.isArray(nowConfig.env)) {
const defined = argEnv.join()
const askFor = nowConfig.env.filter(item => !defined.includes(`${item}=`))
pkgEnv = await envFields(askFor)
}
// Merge `now.env` from package.json with `-e` arguments
const envs = [
...Object.keys(dotenvConfig || {}).map(k => `${k}=${dotenvConfig[k]}`),
...Object.keys(pkgEnv || {}).map(k => `${k}=${pkgEnv[k]}`),
...argEnv
]
let secrets
const findSecret = async uidOrName => {
if (!secrets) {
secrets = await now.listSecrets()
}
return secrets.filter(secret => {
return secret.name === uidOrName || secret.uid === uidOrName
})
}
const env_ = await Promise.all(
envs.map(async kv => {
if (typeof kv !== 'string') {
error('Env key and value missing')
return process.exit(1)
}
const [key, ...rest] = kv.split('=')
let val
if (rest.length > 0) {
val = rest.join('=')
}
if (/[^A-z0-9_]/i.test(key)) {
error(
`Invalid ${chalk.dim('-e')} key ${chalk.bold(
`"${chalk.bold(key)}"`
)}. Only letters, digits and underscores are allowed.`
)
return process.exit(1)
}
if (!key) {
error(`Invalid env option ${chalk.bold(`"${kv}"`)}`)
return process.exit(1)
}
if (val === undefined) {
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(/^@/, '\\@')
} else {
error(
`No value specified for env ${chalk.bold(
`"${chalk.bold(key)}"`
)} and it was not found in your env.`
)
return process.exit(1)
}
}
if (val[0] === '@') {
const uidOrName = val.substr(1)
const _secrets = await findSecret(uidOrName)
if (_secrets.length === 0) {
if (uidOrName === '') {
error(
`Empty reference provided for env key ${chalk.bold(
`"${chalk.bold(key)}"`
)}`
)
} else {
error(
`No secret found by uid or name ${chalk.bold(`"${uidOrName}"`)}`
)
}
return process.exit(1)
} else if (_secrets.length > 1) {
error(
`Ambiguous secret ${chalk.bold(
`"${uidOrName}"`
)} (matches ${chalk.bold(_secrets.length)} secrets)`
)
return process.exit(1)
}
val = { uid: _secrets[0].uid }
}
return [key, typeof val === 'string' ? val.replace(/^\\@/, '@') : val]
})
)
const env = {}
env_.filter(v => Boolean(v)).forEach(([key, val]) => {
if (key in env) {
console.log(
note(`Overriding duplicate env key ${chalk.bold(`"${key}"`)}`)
)
}
env[key] = val
})
try {
await now.create(
path,
Object.assign(
{
env,
followSymlinks,
forceNew,
forceSync,
forwardNpm: alwaysForwardNpm || forwardNpm,
quiet,
wantsPublic,
sessionAffinity
},
meta
)
)
} catch (err) {
if (debug) {
console.log(`> [debug] error: ${err}\n${err.stack}`)
}
return stopDeployment(err)
}
const { url } = now
const elapsed = ms(new Date() - start)
if (isTTY) {
if (clipboard) {
try {
await copy(url)
console.log(
`${chalk.cyan('> Ready!')} ${chalk.bold(
url
)} (copied to clipboard) [${elapsed}]`
)
} catch (err) {
console.log(
`${chalk.cyan('> Ready!')} ${chalk.bold(url)} [${elapsed}]`
)
}
} else {
console.log(`> ${url} [${elapsed}]`)
}
} else {
process.stdout.write(url)
}
const startU = new Date()
const complete = ({ syncCount }) => {
if (!quiet) {
const elapsedU = ms(new Date() - startU)
console.log(
`> Synced ${syncCount} (${bytes(now.syncAmount)}) [${elapsedU}] `
)
console.log('> Initializing…')
}
// Close http2 agent
now.close()
// Show build logs
if (!quiet) {
if (deploymentType === 'static') {
console.log(`${chalk.cyan('> Deployment complete!')}`)
} else {
printLogs(now.host, token, currentTeam, user)
}
}
}
const plan = await planPromise
if (plan.id === 'oss' && !wantsPublic) {
if (isTTY) {
console.log(
info(
`${chalk.bold(
(currentTeam && `${currentTeam.slug} is`) ||
`You (${user.username || user.email}) are`
)} on the OSS plan. Your code and logs will be made ${chalk.bold(
'public'
)}.`
)
)
const proceed = await promptBool(
'Are you sure you want to proceed with the deployment?',
{ trailing: eraseLines(1) }
)
if (proceed) {
console.log(
note(`You can use ${cmd('now --public')} to skip this prompt`)
)
} else {
const stopSpinner = wait('Canceling deployment')
await now.remove(now.id, { hard: true })
stopSpinner()
console.log(
info(
'Deployment aborted. No files were synced.',
` You can upgrade by running ${cmd('now upgrade')}.`
)
)
return 0
}
} else if (!wantsPublic) {
const msg =
'\nYou are on the OSS plan. Your code and logs will be made public.' +
' If you agree with that, please run again with --public.'
return stopDeployment(msg)
}
}
if (now.syncAmount) {
if (debug && now.syncFileCount !== now.fileCount) {
console.log(
`> [debug] total files ${now.fileCount}, ${now.syncFileCount} changed. `
)
}
const size = bytes(now.syncAmount)
const syncCount = `${now.syncFileCount} file${now.syncFileCount > 1
? 's'
: ''}`
const bar = new Progress(
`> Upload [:bar] :percent :etas (${size}) [${syncCount}]`,
{
width: 20,
complete: '=',
incomplete: '',
total: now.syncAmount,
clear: true
}
)
now.upload()
now.on('upload', ({ names, data }) => {
const amount = data.length
if (debug) {
console.log(
`> [debug] Uploaded: ${names.join(' ')} (${bytes(data.length)})`
)
}
bar.tick(amount)
})
now.on('complete', () => complete({ syncCount }))
now.on('error', err => {
error('Upload failed')
return stopDeployment(err)
})
} else {
if (!quiet) {
console.log(`> Initializing…`)
}
// Close http2 agent
now.close()
// Show build logs
if (!quiet) {
if (deploymentType === 'static') {
console.log(`${chalk.cyan('> Deployment complete!')}`)
} else {
printLogs(now.host, token, currentTeam, user)
}
}
}
})
}
async function readMeta(
_path,
_deploymentName,
deploymentType,
_sessionAffinity
) {
try {
const meta = await readMetaData(_path, {
deploymentType,
deploymentName: _deploymentName,
quiet: true,
sessionAffinity: _sessionAffinity
})
if (!deploymentType) {
deploymentType = meta.type
if (debug) {
console.log(
`> [debug] Detected \`deploymentType\` = \`${deploymentType}\``
)
}
}
if (!_deploymentName) {
_deploymentName = meta.name
if (debug) {
console.log(
`> [debug] Detected \`deploymentName\` = "${_deploymentName}"`
)
}
}
return {
meta,
deploymentName: _deploymentName,
deploymentType,
sessionAffinity: _sessionAffinity
}
} catch (err) {
if (isTTY && err.code === 'MULTIPLE_MANIFESTS') {
if (debug) {
console.log('> [debug] Multiple manifests found, disambiguating')
}
console.log(
`> Two manifests found. Press [${chalk.bold(
'n'
)}] to deploy or re-run with --flag`
)
deploymentType = await promptOptions([
['npm', `${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `],
['docker', `${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `]
])
if (debug) {
console.log(
`> [debug] Selected \`deploymentType\` = "${deploymentType}"`
)
}
return readMeta(_path, _deploymentName, deploymentType)
}
throw err
}
}
function printLogs(host, token) {
// Log build
const logger = new Logger(host, token, { debug, quiet })
logger.on('error', async err => {
if (!quiet) {
if (err && err.type === 'BUILD_ERROR') {
error(
`The build step of your project failed. To retry, run ${cmd(
'now --force'
)}.`
)
} else {
error('Deployment failed')
}
}
if (gitRepo && gitRepo.cleanup) {
// Delete temporary directory that contains repository
gitRepo.cleanup()
if (debug) {
console.log(`> [debug] Removed temporary repo directory`)
}
}
process.exit(1)
})
logger.on('close', async () => {
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)
})
}
module.exports = main

13
src/providers/sh/index.js

@ -0,0 +1,13 @@
module.exports = {
title: 'now.sh',
subcommands: new Set(['help', 'login', 'deploy', 'ls']),
get deploy() {
return require('./deploy')
},
get help() {
return require('./help')
},
get login() {
return require('./login')
}
}

83
src/providers/sh/legacy/agent.js

@ -0,0 +1,83 @@
// Native
const { parse } = require('url')
const http = require('http')
const https = require('https')
// Packages
const fetch = require('node-fetch')
/**
* Returns a `fetch` version with a similar
* API to the browser's configured with a
* HTTP2 agent.
*
* It encodes `body` automatically as JSON.
*
* @param {String} host
* @return {Function} fetch
*/
module.exports = class Agent {
constructor(url, { tls = true, debug } = {}) {
this._url = url
const parsed = parse(url)
this._protocol = parsed.protocol
this._debug = debug
if (tls) {
this._initAgent()
}
}
_initAgent() {
const module = this._protocol === 'https:' ? https : http
this._agent = new module.Agent({
keepAlive: true,
keepAliveMsecs: 10000,
maxSockets: 8
}).on('error', err => this._onError(err, this._agent))
}
_onError(err, agent) {
if (this._debug) {
console.log(`> [debug] agent connection error ${err}\n${err.stack}`)
}
if (this._agent === agent) {
this._agent = null
}
}
fetch(path, opts = {}) {
if (!this._agent) {
if (this._debug) {
console.log('> [debug] re-initializing agent')
}
this._initAgent()
}
const { body } = opts
if (this._agent) {
opts.agent = this._agent
}
if (body && typeof body === 'object' && typeof body.pipe !== 'function') {
opts.headers['Content-Type'] = 'application/json'
opts.body = JSON.stringify(body)
}
if (opts.body && typeof body.pipe !== 'function') {
opts.headers['Content-Length'] = Buffer.byteLength(opts.body)
}
return fetch(this._url + path, opts)
}
close() {
if (this._debug) {
console.log('> [debug] closing agent')
}
if (this._agent) {
this._agent.destroy()
}
}
}

138
src/providers/sh/legacy/build-logger.js

@ -0,0 +1,138 @@
// Native
const EventEmitter = require('events')
// Packages
const io = require('socket.io-client')
const chalk = require('chalk')
const { compare, deserialize } = require('./logs')
module.exports = class Logger extends EventEmitter {
constructor(host, token, { debug = false, quiet = false } = {}) {
super()
this.host = host
this.token = token
this.debug = debug
this.quiet = quiet
// ReadyState
this.building = false
this.socket = io(`https://io.now.sh/states?host=${host}&v=2`)
this.socket.once('error', this.onSocketError.bind(this))
this.socket.on('auth', this.onAuth.bind(this))
this.socket.on('state', this.onState.bind(this))
this.socket.on('logs', this.onLog.bind(this))
this.socket.on('backend', this.onComplete.bind(this))
// Log buffer
this.buf = []
this.printed = new Set()
}
onAuth(callback) {
if (this.debug) {
console.log('> [debug] authenticate')
}
callback(this.token)
}
onState(state) {
// Console.log(state)
if (!state.id) {
console.error('> Deployment not found')
this.emit('error')
return
}
if (state.error) {
this.emit('error', state)
return
}
if (state.backend) {
this.onComplete()
return
}
if (state.logs) {
state.logs.forEach(this.onLog, this)
}
}
onLog(log) {
if (!this.building) {
if (!this.quiet) {
console.log('> Building')
}
this.building = true
}
if (this.quiet) {
return
}
log = deserialize(log)
const timer = setTimeout(() => {
this.buf.sort((a, b) => compare(a.log, b.log))
const idx = this.buf.findIndex(b => b.log.id === log.id) + 1
for (const b of this.buf.slice(0, idx)) {
clearTimeout(b.timer)
this.printLog(b.log)
}
this.buf = this.buf.slice(idx)
}, 500)
this.buf.push({ log, timer })
}
onComplete() {
this.socket.disconnect()
if (this.building) {
this.building = false
}
this.buf.sort((a, b) => compare(a.log, b.log))
// Flush all buffer
for (const b of this.buf) {
clearTimeout(b.timer)
this.printLog(b.log)
}
this.buf = []
this.emit('close')
}
onSocketError(err) {
if (this.debug) {
console.log(`> [debug] Socket error ${err}\n${err.stack}`)
}
}
printLog(log) {
if (this.printed.has(log.id)) return
this.printed.add(log.id)
const data = log.object ? JSON.stringify(log.object) : log.text
if (log.type === 'command') {
console.log(`${chalk.gray('>')}${data}`)
} else if (log.type === 'stderr') {
data.split('\n').forEach(v => {
if (v.length > 0) {
console.error(chalk.gray(`> ${v}`))
}
})
} else if (log.type === 'stdout') {
data.split('\n').forEach(v => {
if (v.length > 0) {
console.log(`${chalk.gray('>')} ${v}`)
}
})
}
}
}

49
src/providers/sh/legacy/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

91
src/providers/sh/legacy/error.js

@ -0,0 +1,91 @@
// Packages
const ms = require('ms')
const chalk = require('chalk')
const error = require('../../../util/output/error')
const info = require('../../../util/output/info')
function handleError(err, { debug = false } = {}) {
// Coerce Strings to Error instances
if (typeof err === 'string') {
err = new Error(err)
}
if (debug) {
console.log(`> [debug] handling error: ${err.stack}`)
}
if (err.status === 403) {
console.log(
error(
'Authentication error. Run `now -L` or `now --login` to log-in again.'
)
)
} else if (err.status === 429) {
if (err.retryAfter === 'never') {
console.log(error(err.message))
} else if (err.retryAfter === null) {
console.log(error('Rate limit exceeded error. Please try later.'))
} else {
console.log(
error(
'Rate limit exceeded error. Try again in ' +
ms(err.retryAfter * 1000, { long: true }) +
', or upgrade your account by running ' +
`${chalk.gray('`')}${chalk.cyan('now upgrade')}${chalk.gray('`')}`
)
)
}
} else if (err.userError) {
console.log(error(err.message))
} else if (err.status === 500) {
console.log(error('Unexpected server error. Please retry.'))
} else if (err.code === 'USER_ABORT') {
console.log(info('Aborted'))
} else {
console.log(
error(`Unexpected error. Please try again later. (${err.message})`)
)
}
}
async function responseError(res) {
let message
let userError
if (res.status >= 400 && res.status < 500) {
let body
try {
body = await res.json()
} catch (err) {
body = {}
}
// Some APIs wrongly return `err` instead of `error`
message = (body.error || body.err || {}).message
userError = true
} else {
userError = false
}
const err = new Error(message || 'Response error')
err.status = res.status
err.userError = userError
if (res.status === 429) {
const retryAfter = res.headers.get('Retry-After')
if (retryAfter) {
err.retryAfter = parseInt(retryAfter, 10)
}
}
return err
}
module.exports = {
handleError,
responseError,
error
}

385
src/providers/sh/legacy/get-files.js

@ -0,0 +1,385 @@
// Native
const { resolve } = require('path')
// Packages
const flatten = require('arr-flatten')
const unique = require('array-unique')
const ignore = require('ignore')
const _glob = require('glob')
const { stat, readdir, readFile } = require('fs-extra')
// Ours
const IGNORED = require('./ignored')
const glob = async function(pattern, options) {
return new Promise((resolve, reject) => {
_glob(pattern, options, (error, files) => {
if (error) {
reject(error)
} else {
resolve(files)
}
})
})
}
/**
* Remove leading `./` from the beginning of ignores
* because our parser doesn't like them :|
*/
const clearRelative = function(str) {
return str.replace(/(\n|^)\.\//g, '$1')
}
/**
* Returns the contents of a file if it exists.
*
* @return {String} results or `''`
*/
const maybeRead = async function(path, default_ = '') {
try {
return await readFile(path, 'utf8')
} catch (err) {
return default_
}
}
/**
* Transform relative paths into absolutes,
* and maintains absolutes as such.
*
* @param {String} maybe relative path
* @param {String} parent full path
*/
const asAbsolute = function(path, parent) {
if (path[0] === '/') {
return path
}
return resolve(parent, path)
}
/**
* Returns a list of files in the given
* directory that are subject to be
* synchronized for static deployments.
*
* @param {String} full path to directory
* @param {Object} options:
* - `limit` {Number|null} byte limit
* - `debug` {Boolean} warn upon ignore
* @return {Array} comprehensive list of paths to sync
*/
async function staticFiles(
path,
nowConfig = {},
{ limit = null, hasNowJson = false, debug = false } = {}
) {
const whitelist = nowConfig.files
// The package.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
const search_ = whitelist || ['.']
// Convert all filenames into absolute paths
const search = Array.prototype.concat.apply(
[],
await Promise.all(
search_.map(file => glob(file, { cwd: path, absolute: true, dot: true }))
)
)
// Compile list of ignored patterns and files
const gitIgnore = await maybeRead(resolve(path, '.gitignore'))
const filter = ignore()
.add(IGNORED + '\n' + clearRelative(gitIgnore))
.createFilter()
const prefixLength = path.length + 1
// 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 accepts = file => {
const relativePath = file.substr(prefixLength)
if (relativePath === '') {
return true
}
const accepted = filter(relativePath)
if (!accepted && debug) {
console.log('> [debug] ignoring "%s"', file)
}
return accepted
}
// Locate files
if (debug) {
console.time(`> [debug] locating files ${path}`)
}
const files = await explode(search, {
accepts,
limit,
debug
})
if (debug) {
console.timeEnd(`> [debug] locating files ${path}`)
}
if (hasNowJson) {
files.push(asAbsolute('now.json', path))
}
// Get files
return unique(files)
}
/**
* Returns a list of files in the given
* directory that are subject to be
* synchronized for npm.
*
* @param {String} full path to directory
* @param {String} contents of `package.json` to avoid lookup
* @param {Object} options:
* - `limit` {Number|null} byte limit
* - `debug` {Boolean} warn upon ignore
* @return {Array} comprehensive list of paths to sync
*/
async function npm(
path,
pkg = {},
nowConfig = {},
{ limit = null, hasNowJson = false, debug = false } = {}
) {
const whitelist = nowConfig.files || pkg.files || (pkg.now && pkg.now.files)
// The package.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
const search_ = whitelist || ['.']
// Convert all filenames into absolute paths
const search = Array.prototype.concat.apply(
[],
await Promise.all(
search_.map(file => glob(file, { cwd: path, absolute: true, dot: true }))
)
)
// 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 ? gitIgnore : npmIgnore)
)
.createFilter()
const prefixLength = path.length + 1
// 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) ||
nowConfig.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)
}
return accepted
}
// Locate files
if (debug) {
console.time(`> [debug] locating files ${path}`)
}
const files = await explode(search, {
accepts,
limit,
debug
})
if (debug) {
console.timeEnd(`> [debug] locating files ${path}`)
}
// Always include manifest as npm does not allow ignoring it
// source: https://docs.npmjs.com/files/package.json#files
files.push(asAbsolute('package.json', path))
if (hasNowJson) {
files.push(asAbsolute('now.json', path))
}
// Get files
return unique(files)
}
/**
* Returns a list of files in the given
* directory that are subject to be
* sent to docker as build context.
*
* @param {String} full path to directory
* @param {String} contents of `Dockerfile`
* @param {Object} options:
* - `limit` {Number|null} byte limit
* - `debug` {Boolean} warn upon ignore
* @return {Array} comprehensive list of paths to sync
*/
async function docker(
path,
nowConfig = {},
{ limit = null, hasNowJson = false, debug = false } = {}
) {
const whitelist = nowConfig.files
// Base search path
// the now.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
const search_ = whitelist || ['.']
// Convert all filenames into absolute paths
const search = search_.map(file => asAbsolute(file, path))
// Compile list of ignored patterns and files
const dockerIgnore = await maybeRead(resolve(path, '.dockerignore'), null)
const filter = ignore()
.add(
IGNORED +
'\n' +
clearRelative(
dockerIgnore === null
? await maybeRead(resolve(path, '.gitignore'))
: dockerIgnore
)
)
.createFilter()
const prefixLength = path.length + 1
const accepts = function(file) {
const relativePath = file.substr(prefixLength)
if (relativePath === '') {
return true
}
const accepted = filter(relativePath)
if (!accepted && debug) {
console.log('> [debug] ignoring "%s"', file)
}
return accepted
}
// Locate files
if (debug) {
console.time(`> [debug] locating files ${path}`)
}
const files = await explode(search, { accepts, limit, debug })
if (debug) {
console.timeEnd(`> [debug] locating files ${path}`)
}
// Always include manifest as npm does not allow ignoring it
// source: https://docs.npmjs.com/files/package.json#files
files.push(asAbsolute('Dockerfile', path))
if (hasNowJson) {
files.push(asAbsolute('now.json', path))
}
// Get files
return unique(files)
}
/**
* Explodes directories into a full list of files.
* Eg:
* in: ['/a.js', '/b']
* out: ['/a.js', '/b/c.js', '/b/d.js']
*
* @param {Array} of {String}s representing paths
* @param {Array} of ignored {String}s.
* @param {Object} options:
* - `limit` {Number|null} byte limit
* - `debug` {Boolean} warn upon ignore
* @return {Array} of {String}s of full paths
*/
async function explode(paths, { accepts, debug }) {
const list = async file => {
let path = file
let s
if (!accepts(file)) {
return null
}
try {
s = await stat(path)
} catch (e) {
// In case the file comes from `files`
// and it wasn't specified with `.js` by the user
path = file + '.js'
try {
s = await stat(path)
} catch (e2) {
if (debug) {
console.log('> [debug] ignoring invalid file "%s"', file)
}
return null
}
}
if (s.isDirectory()) {
const all = await readdir(file)
/* eslint-disable no-use-before-define */
return many(all.map(subdir => asAbsolute(subdir, file)))
/* eslint-enable no-use-before-define */
} else if (!s.isFile()) {
if (debug) {
console.log('> [debug] ignoring special file "%s"', file)
}
return null
}
return path
}
const many = all => Promise.all(all.map(file => list(file)))
return flatten(await many(paths)).filter(v => v !== null)
}
module.exports = {
npm,
docker,
staticFiles
}

221
src/providers/sh/legacy/git.js

@ -0,0 +1,221 @@
// Native
const path = require('path')
const url = require('url')
const childProcess = require('child_process')
// Packages
const fs = require('fs-extra')
const download = require('download')
const tmp = require('tmp-promise')
const isURL = require('is-url')
const cloneRepo = (parts, tmpDir, { ssh }) =>
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 = ssh
? `git@${host}:${parts.main}`
: `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 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 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) {
try {
await cloneRepo(pathParts, tmpDir, { ssh: true })
} catch (err) {
gitInstalled = false
}
}
if (gitInstalled) {
const renaming = await renameRepoDir(pathParts, tmpDir)
return renaming
}
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
}
const renaming = await renameRepoDir(pathParts, tmpDir)
return renaming
}
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
}
const err = new Error(`Host "${urlParts.host}" is unsupported.`)
err.code = 'INVALID_URL'
err.userError = true
throw err
}
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
}

44
src/providers/sh/legacy/hash.js

@ -0,0 +1,44 @@
// Native
const { createHash } = require('crypto')
// Packages
const { readFile } = require('fs-extra')
/**
* Computes hashes for the contents of each file given.
*
* @param {Array} of {String} full paths
* @return {Map}
*/
async function hashes(files) {
const map = new Map()
await Promise.all(
files.map(async name => {
const data = await readFile(name)
const h = hash(data)
const entry = map.get(h)
if (entry) {
entry.names.push(name)
} else {
map.set(hash(data), { names: [name], data })
}
})
)
return map
}
/**
* Computes a hash for the given buf.
*
* @param {Buffer} file data
* @return {String} hex digest
*/
function hash(buf) {
return createHash('sha1').update(buf).digest('hex')
}
module.exports = hashes

17
src/providers/sh/legacy/ignored.js

@ -0,0 +1,17 @@
// Base `.gitignore` to which we add entries
// supplied by the user
module.exports = `.hg
.git
.gitmodules
.svn
.npmignore
.dockerignore
.gitignore
.*.swp
.DS_Store
.wafpicke-*
.lock-wscript
npm-debug.log
config.gypi
node_modules
CVS`

14
src/providers/sh/legacy/logs.js

@ -0,0 +1,14 @@
exports.compare = function(a, b) {
return (
a.serial.localeCompare(b.serial) ||
// For the case serials are a same value on old logs
a.created.getTime() - b.created.getTime()
)
}
exports.deserialize = function(log) {
return Object.assign({}, log, {
date: new Date(log.date),
created: new Date(log.created)
})
}

1031
src/providers/sh/legacy/now.js

File diff suppressed because it is too large

57
src/providers/sh/legacy/plans.js

@ -0,0 +1,57 @@
const ms = require('ms')
const Now = require('./now')
async function parsePlan(json) {
const { subscription } = json
let id
let until
let name
if (subscription) {
const planItems = subscription.items.data
const mainPlan = planItems.find(d => d.plan.metadata.is_main_plan === '1')
if (mainPlan) {
id = mainPlan.plan.id
name = mainPlan.plan.name
if (subscription.cancel_at_period_end) {
until = ms(
new Date(subscription.current_period_end * 1000) - new Date(),
{ long: true }
)
}
} else {
id = 'oss'
}
} else {
id = 'oss'
}
return { id, name, until }
}
module.exports = class Plans extends Now {
async getCurrent() {
const res = await this._fetch('/plan')
const json = await res.json()
return parsePlan(json)
}
async set(plan) {
const res = await this._fetch('/plan', {
method: 'PUT',
body: { plan }
})
const json = await res.json()
if (res.ok) {
return parsePlan(json)
}
const err = new Error(json.error.message)
err.code = json.error.code
throw err
}
}

39
src/providers/sh/legacy/prompt-options.js

@ -0,0 +1,39 @@
// Packages
const chalk = require('chalk')
module.exports = promptOptions
function promptOptions(opts) {
return new Promise((resolve, reject) => {
opts.forEach(([, text], i) => {
console.log(`${chalk.gray('>')} [${chalk.bold(i + 1)}] ${text}`)
})
const ondata = v => {
const s = v.toString()
const cleanup = () => {
process.stdin.setRawMode(false)
process.stdin.removeListener('data', ondata)
}
// Ctrl + C
if (s === '\u0003') {
cleanup()
const err = new Error('Aborted')
err.code = 'USER_ABORT'
return reject(err)
}
const n = Number(s)
if (opts[n - 1]) {
cleanup()
resolve(opts[n - 1][0])
}
}
process.stdin.setRawMode(true)
process.stdin.resume()
process.stdin.on('data', ondata)
})
}

195
src/providers/sh/legacy/read-metadata.js

@ -0,0 +1,195 @@
// Native
const { basename, resolve: resolvePath } = require('path')
// Packages
const chalk = require('chalk')
const { readFile } = require('fs-extra')
const { parse: parseDockerfile } = require('docker-file-parser')
const determineType = require('deployment-type')
module.exports = readMetaData
async function readMetaData(
path,
{
deploymentType,
deploymentName,
sessionAffinity,
quiet = false,
strict = true
}
) {
let description
let type = deploymentType
let name = deploymentName
let affinity = sessionAffinity
const pkg = await readJSON(path, 'package.json')
let nowConfig = await readJSON(path, 'now.json')
const dockerfile = await readDockerfile(path)
const hasNowJson = Boolean(nowConfig)
if (pkg && pkg.now) {
// If the project has both a `now.json` and `now` Object in the `package.json`
// file, then fail hard and let the user know that they need to pick one or the
// other
if (nowConfig) {
const err = new Error(
'You have a `now` configuration field inside `package.json` ' +
'but configuration is also present in `now.json`! ' +
"Please ensure there's a single source of configuration by removing one."
)
err.userError = true
throw err
} else {
nowConfig = pkg.now
}
}
// We can remove this once the prompt for choosing `--npm` or `--docker` is gone
if (pkg && pkg.now && pkg.now.type) {
type = nowConfig.type
}
// The same goes for this
if (nowConfig && nowConfig.type) {
type = nowConfig.type
}
if (!type) {
type = await determineType(path)
// Both `package.json` and `Dockerfile` exist! Prompt the user to pick one.
// We can remove this soon (details are internal) - also read the comment paragraph above
if (type === 'docker' && (pkg && dockerfile)) {
const err = new Error(
'Ambiguous deployment (`package.json` and `Dockerfile` found). ' +
'Please supply `--npm` or `--docker` to disambiguate.'
)
err.userError = true
err.code = 'MULTIPLE_MANIFESTS'
throw err
}
}
if (!name && nowConfig) {
name = nowConfig.name
}
if (!affinity && nowConfig) {
affinity = nowConfig.sessionAffinity
}
if (type === 'npm') {
if (pkg) {
if (!name && pkg.now && pkg.now.name) {
name = String(pkg.now.name)
}
if (!name && pkg.name) {
name = String(pkg.name)
}
description = pkg.description
}
} else if (type === 'docker') {
if (strict && dockerfile.length <= 0) {
const err = new Error('No commands found in `Dockerfile`')
err.userError = true
throw err
}
const labels = {}
dockerfile.filter(cmd => cmd.name === 'LABEL').forEach(({ args }) => {
for (const key in args) {
if (!{}.hasOwnProperty.call(args, key)) {
continue
}
// Unescape and convert into string
try {
labels[key] = args[key]
} catch (err) {
const e = new Error(
`Error parsing value for LABEL ${key} in \`Dockerfile\``
)
e.userError = true
throw e
}
}
})
if (!name) {
name = labels.name
}
description = labels.description
} else if (type === 'static') {
// Do nothing
} else {
throw new TypeError(`Unsupported "deploymentType": ${type}`)
}
// No name in `package.json` / `now.json`, or "name" label in Dockerfile.
// Default to the basename of the root dir
if (!name) {
name = basename(path)
if (!quiet && type !== 'static') {
if (type === 'docker') {
console.log(
`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`
)
} else {
console.log(
`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`
)
}
}
}
return {
name,
description,
type,
pkg,
nowConfig,
hasNowJson,
// XXX: legacy
deploymentType: type,
sessionAffinity: affinity
}
}
async function readJSON(path, name) {
try {
const contents = await readFile(resolvePath(path, name), 'utf8')
return JSON.parse(contents)
} catch (err) {
// If the file doesn't exist then that's fine; any other error bubbles up
if (err.code !== 'ENOENT') {
err.userError = true
throw err
}
}
}
async function readDockerfile(path, name = 'Dockerfile') {
try {
const contents = await readFile(resolvePath(path, name), 'utf8')
return parseDockerfile(contents, { includeComments: true })
} catch (err) {
// If the file doesn't exist then that's fine; any other error bubbles up
if (err.code !== 'ENOENT') {
err.userError = true
throw err
}
}
}

20
src/providers/sh/legacy/to-host.js

@ -0,0 +1,20 @@
// Native
const { parse } = require('url')
/**
* Converts a valid deployment lookup parameter to a hostname.
* `http://google.com` => google.com
* google.com => google.com
*/
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')
}
module.exports = toHost

290
src/providers/sh/login.js

@ -0,0 +1,290 @@
// node
const { stringify: stringifyQuery } = require('querystring')
const { platform, arch, hostname } = require('os')
// theirs
const fetch = require('node-fetch')
const debug = require('debug')('now:sh:login')
const promptEmail = require('email-prompt')
const ms = require('ms')
const { validate: validateEmail } = require('email-validator')
// ours
const { version } = require('./util/pkg')
const ua = require('./util/ua')
const error = require('../../util/output/error')
const aborted = require('../../util/output/aborted')
const wait = require('../../util/output/wait')
const highlight = require('../../util/output/highlight')
const info = require('../../util/output/info')
const ok = require('../../util/output/ok')
const cmd = require('../../util/output/cmd')
const ready = require('../../util/output/ready')
const param = require('../../util/output/param')
const eraseLines = require('../../util/output/erase-lines')
const sleep = require('../../util/sleep')
const getUser = require('./util/get-user')
const {
writeToAuthConfigFile,
writeToConfigFile
} = require('../../util/config-files')
const getNowDir = require('../../get-now-dir')
const hp = require('../../util/humanize-path')
// POSTs to /now/registration – either creates an account or performs a login
// returns {token, securityCode}
// token: should be used to verify the status of the login process
// securityCode: will be sent to the user in the email body
const getVerificationData = async ({ apiUrl, email }) => {
const tokenName = `Now CLI ${version}${platform()}-${arch()} (${hostname()})`
const data = JSON.stringify({ email, tokenName })
debug('POST /now/registration')
let res
try {
res = await fetch(`${apiUrl}/now/registration`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
'User-Agent': ua
},
body: data
})
} catch (err) {
debug('error fetching /now/registration: %O', err.stack)
throw new Error(
error(
`An unexpected error occurred while trying to login: ${err.message}`
)
)
}
debug('parsing response from POST /now/registration')
let body
try {
body = await res.json()
} catch (err) {
debug(
`error parsing the response from /now/registration as JSON – got %O`,
err.stack
)
throw new Error(
error(
`An unexpected error occurred while trying to log in: ${err.message}`
)
)
}
return body
}
const verify = async ({ apiUrl, email, verificationToken }) => {
const query = {
email,
token: verificationToken
}
debug('GET /now/registration/verify')
let res
try {
res = await fetch(
`${apiUrl}/now/registration/verify?${stringifyQuery(query)}`,
{
headers: { 'User-Agent': ua }
}
)
} catch (err) {
debug(`error fetching /now/registration/verify: $O`, err.stack)
throw new Error(
error(
`An unexpected error occurred while trying to verify your login: ${err.message}`
)
)
}
debug('parsing response from GET /now/registration/verify')
let body
try {
body = await res.json()
} catch (err) {
debug(
`error parsing the response from /now/registration/verify: $O`,
err.stack
)
throw new Error(
error(
`An unexpected error occurred while trying to verify your login: ${err.message}`
)
)
}
return body.token
}
const readEmail = async () => {
let email
try {
email = await promptEmail({ start: info('Enter your email: ') })
} catch (err) {
console.log() // \n
if (err.message === 'User abort') {
throw new Error(aborted('No changes made.'))
}
if (err.message === 'stdin lacks setRawMode support') {
throw new Error(
error(
`Interactive mode not supported – please run ${cmd(
'now login you@domain.com'
)}`
)
)
}
}
console.log() // \n
return email
}
// TODO open issues: .co, error messages
const login = async ctx => {
const { argv } = ctx
const apiUrl =
(ctx.config.sh && ctx.config.sh.apiUrl) || 'https://api.zeit.co'
let email
let emailIsValid = false
let stopSpinner
// node file sh login [email|help]
const argvHasSh = argv[2] === 'sh'
const allowedNumberOfArgs = argvHasSh ? 5 : 4
if (argv.length > allowedNumberOfArgs) {
const _cmd = argvHasSh ? 'now sh login' : 'now login'
console.log(error(`Invalid number of arguments for ${cmd(_cmd)}`))
console.log(info(`See ${cmd(_cmd + ' help')}`))
return 1
}
const maybeEmail = argv[argv.length - 1]
// if the last arg is not the command itself, then maybe it's an email
if (maybeEmail !== 'login') {
if (!validateEmail(maybeEmail)) {
// if it's not a valid email, let's just error
console.log(error(`Invalid email: ${param(maybeEmail)}.`))
return 1
}
// valid email, no need to prompt the user
email = maybeEmail
} else {
do {
try {
email = await readEmail()
} catch (err) {
let erase = ''
if (err.message.includes('Aborted')) {
// no need to keep the prompt if the user `ctrl+c`ed
erase = eraseLines(2)
}
console.log(erase + err.message)
return 1
}
emailIsValid = validateEmail(email)
if (!emailIsValid) {
// let's erase the `> Enter email [...]`
// we can't use `console.log()` because it appends a `\n`
// we need this check because `email-prompt` doesn't print
// anything if there's no TTY
process.stdout.write(eraseLines(2))
}
} while (!emailIsValid)
}
let verificationToken
let securityCode
stopSpinner = wait('Sending you an email')
try {
const data = await getVerificationData({ apiUrl, email })
verificationToken = data.token
securityCode = data.securityCode
} catch (err) {
stopSpinner()
console.log(err.message)
return 1
}
stopSpinner()
// prettier-ignore
console.log(info(
`We sent an email to ${highlight(email)}. Please follow the steps provided`,
` in it and make sure the security code matches ${highlight(securityCode)}.`
))
stopSpinner = wait('Waiting for your confirmation')
let token
while (!token) {
try {
await sleep(ms('1s'))
token = await verify({ apiUrl, email, verificationToken })
} catch (err) {
if (/invalid json response body/.test(err.message)) {
// /now/registraton is currently returning plain text in that case
// we just wait for the user to click on the link
} else {
stopSpinner()
console.log(err.message)
return 1
}
}
}
stopSpinner()
console.log(ok('Email confirmed.'))
stopSpinner = wait('Feching your personal details')
let user
try {
user = await getUser({ apiUrl, token })
} catch (err) {
stopSpinner()
console.log(err)
return 1
}
const index = ctx.authConfig.credentials.findIndex(c => c.provider === 'sh')
const obj = { provider: 'sh', token }
if (index === -1) {
// wasn't logged in before
ctx.authConfig.credentials.push(obj)
} else {
// let's just replace the existing object
ctx.authConfig.credentials[index] = obj
}
// NOTE: this will override any existing config for `sh`
ctx.config.sh = { user }
writeToAuthConfigFile(ctx.authConfig)
writeToConfigFile(ctx.config)
stopSpinner()
console.log(ok('Fetched your personal details.'))
console.log(
ready(
`Authentication token and personal details saved in ${param(
hp(getNowDir())
)}`
)
)
return ctx
}
module.exports = login

54
src/providers/sh/util/get-user.js

@ -0,0 +1,54 @@
// theirs
const fetch = require('node-fetch')
const debug = require('debug')('now:sh:get-user')
// ours
const error = require('../../../util/output/error')
const getUser = async ({ apiUrl, token }) => {
debug('start')
const url = apiUrl + '/www/user'
const headers = {
Authorization: `Bearer ${token}`
}
debug('GET /www/user')
let res
try {
res = await fetch(url, { headers })
} catch (err) {
debug(`error fetching /www/user: $O`, err.stack)
throw new Error(
error(
`An unexpected error occurred while trying to fetch your personal details: ${err.message}`
)
)
}
debug('parsing response from GET /www/user')
let body
try {
body = await res.json()
} catch (err) {
debug(
`error parsing the response from /www/user as JSON – got %O`,
err.stack
)
throw new Error(
error(
`An unexpected error occurred while trying to fetch your personal details: ${err.message}`
)
)
}
const { user } = body
// this is pretty much useless
delete user.billingChecked
return user
}
module.exports = getUser

11
src/providers/sh/util/pkg.js

@ -0,0 +1,11 @@
const path = require('path')
const pkg = require('../../../../package.json')
try {
const distDir = path.dirname(process.execPath)
pkg._npmPkg = require(path.join(distDir, '../../package.json'))
} catch (err) {
pkg._npmPkg = null
}
module.exports = pkg

7
src/providers/sh/util/ua.js

@ -0,0 +1,7 @@
// node
const os = require('os')
// ours
const { version } = require('./pkg')
module.exports = `now ${version} node-${process.version} ${os.platform()} (${os.arch()})`

28
src/resolve.js

@ -0,0 +1,28 @@
const resolvers = require('./resolvers')
const resolverNames = Object.keys(resolvers)
const resolve = async (param, opts) => {
for (const name of resolverNames) {
const resolver = resolvers[name]
let resolved
// give the caller the ability to create
// nicer errors by attaching the resolver name
try {
resolved = await resolver(param, opts)
} catch (err) {
err.resolverName = name
throw err
}
if (resolved !== null) {
return resolved
}
// otherwise continue onto the next resolver
// note: if a resolver throws, we consider that
// unexpected. a resolver should return `null`
// when the parameter is unresolvable instead
}
return null
}
module.exports = resolve

13
src/resolvers/fs.js

@ -0,0 +1,13 @@
const { exists } = require('fs.promised')
const { resolve } = require('path')
const fsResolver = async (param, { cwd = process.cwd() } = {}) => {
const resolved = resolve(cwd, param)
if (await exists(resolved)) {
return resolved
} else {
return null
}
}
module.exports = fsResolver

100
src/resolvers/github.js

@ -0,0 +1,100 @@
//@flow
const { tmpdir } = require('os')
const { parse, format } = require('url')
const fetch = require('node-fetch')
const tar = require('tar-fs')
const pipeStreams = require('pipe-streams-to-promise')
const { mkdir } = require('fs.promised')
const uid = require('uid-promise')
const { createGunzip } = require('zlib')
const { join } = require('path')
const debug = require('debug')('now:resolvers:github')
// matches a parameter that can be `now`d like zeit/now#master
const DEPLOY_PARAM_REGEX = /^([\w-]+)\/([\w-]+)(#\w+)?$/
// matches whether the parameter could be a github url
const GITHUB_TEST_REGEX = /^(https?:\/\/)(www\.)?github\.com/
// matches a github url pathname like: zeit/now/tree/master
const URL_PATHNAME_REGEX = /^\/([\w-]+)\/([\w-]+)(\/tree\/(\w+))?$/
const resolveGitHub = param => {
// support simple `user/repo` syntax
const match = param.match(DEPLOY_PARAM_REGEX)
if (match) {
const [, user, repo, tree = 'master'] = match
return resolveGitHubByURL(`https://github.com/${user}/${repo}/tree/${tree}`)
} else if (GITHUB_TEST_REGEX.test(param)) {
return resolveGitHubByURL(param)
} else {
return null
}
}
const resolveGitHubByURL = async (url: string) => {
debug('resolving %s by github url', url)
if (/^https?/.test(url)) {
const parsed = parse(url)
if (parsed.hostname === 'github.com') {
const httpsUrl =
'https:' === parsed.protocol ? url : format(Object.assign({}, parsed))
const res = await fetch(httpsUrl)
if (res.ok) {
debug('attempting github clone')
const { pathname } = parsed
const match = pathname.match(URL_PATHNAME_REGEX)
if (match) {
const [, user, repo, , tree] = match
const downloadURL = format({
protocol: 'https:',
hostname: 'codeload.github.com',
pathname: `/${user}/${repo}/tar.gz/${tree}`
})
debug('fetching download url', downloadURL)
const downloadRes = await fetch(downloadURL, { compress: false })
if (downloadRes.ok) {
const tmpDir = join(tmpdir(), `now-gh-${await uid(20)}`)
debug('creating tmp dir to extract', tmpDir)
try {
await mkdir(tmpDir)
} catch (err) {
throw new Error(
'Error occurred while trying to extract ' +
`GH tarball to tmp directory ${tmpDir}: ${err.stack}`
)
}
debug('unzipping and untarring stream')
await pipeStreams([
downloadRes.body,
createGunzip(),
tar.extract(tmpDir)
])
// instead of stripping a directory upon untar,
// we return the full path to the extracted project,
// so that now can take advantage of the name
return join(tmpDir, `${repo}-${tree}`)
} else {
throw new Error(
'An HTTP error ${res.status} was returned ' +
`by "${downloadURL}"`
)
}
} else {
debug('invalid github project url')
return null
}
} else {
debug('non-200 from github (%d)', res.status)
return null
}
} else {
debug('skipping non-github hostname')
return null
}
} else {
return null
}
}
module.exports = resolveGitHub

4
src/resolvers/index.js

@ -0,0 +1,4 @@
module.exports = {
fs: require('./fs'),
github: require('./github')
}

4
src/serverless/README.md

@ -0,0 +1,4 @@
# serverless utilities
This directory contains a utilities that are useful and reusable
across different FaaS providers.

7
src/serverless/build.js

@ -0,0 +1,7 @@
const builders = require('./builders')
const build = (dir, desc, opts) => {
return builders[desc.type](dir, desc, opts)
}
module.exports = build

11
src/serverless/builders/index.js

@ -0,0 +1,11 @@
module.exports = {
get nodejs() {
return require('./nodejs')
},
get static() {
return require('./static')
},
get go() {
return require('./go')
}
}

92
src/serverless/builders/nodejs.js

@ -0,0 +1,92 @@
const { tmpdir } = require('os')
const { join } = require('path')
const { mkdir, stat, link, exists, readdir } = require('fs.promised')
const uid = require('uid-promise')
const { exec: exec_ } = require('child_process')
const { toBuffer } = require('convert-stream')
const archiver = require('archiver')
const debug = require('debug')('now:serverless:builders:nodejs')
const exec = require('util').promisify(exec_)
const nodejsBuilder = async (dir, desc, { overrides = {} } = {}) => {
const files = await readdir(dir)
const tmpDirName = `now-nodejs-build-${await uid(20)}`
const targetPath = join(tmpdir(), tmpDirName)
debug('init nodejs project build stage in', targetPath)
await mkdir(targetPath)
// produce hard links of the source files in the target dir
await Promise.all(
files
.filter(name => name !== 'node_modules' && !(name in overrides))
.map(file => {
debug('making hard link for %s', file)
return link(join(dir, file), join(targetPath, file))
})
)
const archive = archiver('zip')
// trigger an install if needed
if (desc.packageJSON) {
let buildCommand = ''
if (await exists(join(targetPath, 'package-lock.json'))) {
buildCommand = 'npm install'
} else if (await exists(join(targetPath, 'yarn.lock'))) {
buildCommand = 'yarn install'
} else {
buildCommand = 'npm install'
}
try {
debug('executing %s in %s', buildCommand, targetPath)
await exec(buildCommand, {
cwd: targetPath,
env: Object.assign({}, process.env, {
// we set this so that we make the installers ignore
// dev dependencies. in the future, we can add a flag
// to ignore this behavior, or set different envs
NODE_ENV: 'production'
})
})
} catch (err) {
throw new Error(
`The build command ${buildCommand} failed for ${dir}: ${err.message}`
)
}
} else {
debug('ignoring build step, no manifests found')
}
const buffer = toBuffer(archive)
archive.on('warning', err => {
console.error('Warning while creating zip file', err)
})
for (const name in overrides) {
archive.append(overrides[name], { name })
}
// we read again to get the results of the build process
const filesToZip = await readdir(targetPath)
await Promise.all(
filesToZip.map(async file => {
const path = join(targetPath, file)
const stats = await stat(path)
debug('adding', path)
return stats.isDirectory()
? archive.directory(path, file, { stats })
: archive.file(path, { name: file, stats })
})
)
archive.finalize()
// buffer promise
return buffer
}
module.exports = nodejsBuilder

29
src/serverless/get-handler.js

@ -0,0 +1,29 @@
// @flow
const { readFileSync } = require('fs')
const { join } = require('path')
const handler = readFileSync(join(__dirname, 'handler.js')).toString()
// symbols to replace in the meta-source
const CMD_SYMBOL = '/*NOW_CMD*/'
const SCRIPT_SYMBOL = '/*NOW_SCRIPT*/'
const REQ_HANDLER_SYMBOL = '/*PROXY_REQUEST_SOURCE*/'
if (handler.indexOf(CMD_SYMBOL) < 0) {
throw new Error('Missing symbol in `handler.js`: ' + CMD_SYMBOL)
}
if (handler.indexOf(SCRIPT_SYMBOL) < 0) {
throw new Error('Missing symbol in `handler.js`: ' + SCRIPT_SYMBOL)
}
if (handler.indexOf(REQ_HANDLER_SYMBOL) < 0) {
throw new Error('Missing symbol in `handler.js`: ' + REQ_HANDLER_SYMBOL)
}
const getHandler = ({ cmd, script }, fn: Function) =>
handler
.replace(CMD_SYMBOL, JSON.stringify(cmd))
.replace(SCRIPT_SYMBOL, JSON.stringify(script))
.replace(REQ_HANDLER_SYMBOL, fn.toString())
module.exports = getHandler

110
src/serverless/handler.js

@ -0,0 +1,110 @@
// @flow
const start = new Date()
const { createServer } = require('http')
const { createConnection } = require('net')
const { spawn } = require('child_process')
const request = require('http').request
let spawned = false
let PORT = null
let retriesLeft = 20
let buffer = []
const flushBuffer = () => {
buffer.forEach(args => {
proxyRequest.apply(null, args)
})
buffer = null
}
const findFreePort = () =>
new Promise((resolve, reject) => {
const srv = createServer(() => {}).listen(err => {
if (err) return reject(err)
const { port } = srv.address()
srv.close()
resolve(port)
})
})
findFreePort().then(
port => {
PORT = port
const env = Object.assign({}, process.env, {
// we need to add `/nodejs/bin` for GCP functions to
// work correctly
PATH: `/nodejs/bin:/usr/local/bin:/usr/bin`,
PORT
})
const NOW_CMD = [
/*NOW_CMD*/
][0]
const NOW_SCRIPT = [
/*NOW_SCRIPT*/
][0]
if (NOW_CMD) {
const cmd = spawn('/usr/bin/env', ['sh', '-c', NOW_CMD], { env: env })
cmd.on('error', err => {
throw err
})
} else {
process.env.PORT = PORT
require(`./${NOW_SCRIPT}`)
}
const attemptConnect = () => {
const socket = createConnection(PORT)
socket.setTimeout(1000)
socket.on('error', retry)
socket.on('connect', () => {
socket.end()
spawned = true
flushBuffer()
console.log('spawn took', new Date() - start)
})
socket.on('timeout', () => {
socket.end()
retry()
})
}
const retry = () => {
if (--retriesLeft < 0) {
throw new Error('Could not establish a connection to the http server')
}
// this is close to the bootup time of the most minimal
// node server that could be created
setTimeout(attemptConnect, 80)
}
retry()
},
err => {
throw err
}
)
exports.handler = (...args) => {
// hack for lambda. we will refactor the handler injection
// per-provider later
if (args[1] && args[1].callbackWaitsForEmptyEventLoop) {
args[1].callbackWaitsForEmptyEventLoop = false
}
if (spawned) {
proxyRequest.apply(null, args)
} else {
buffer.push(args)
}
}
// we will replace the comment with the function with the logic
// to proxy the request for every provider
const proxyRequest = [
/*PROXY_REQUEST_SOURCE*/
][0].bind(null, request, () => PORT)

43
src/util/config-files.js

@ -0,0 +1,43 @@
// node
const { readFileSync, writeFileSync } = require('fs')
const { join: joinPath } = require('path')
// ours
const getNowDir = require('../get-now-dir')
const NOW_DIR = getNowDir()
const CONFIG_FILE_PATH = joinPath(NOW_DIR, 'config.json')
const AUTH_CONFIG_FILE_PATH = joinPath(NOW_DIR, 'auth.json')
const prettify = obj => JSON.stringify(obj, null, 2)
// reads `CONFIG_FILE_PATH` atomically
const readConfigFile = () => readFileSync(CONFIG_FILE_PATH, 'utf8')
// writes whatever's in `stuff` to `CONFIG_FILE_PATH`, atomically
const writeToConfigFile = stuff =>
writeFileSync(CONFIG_FILE_PATH, prettify(stuff))
// reads `AUTH_CONFIG_FILE_PATH` atomically
const readAuthConfigFile = () => readFileSync(AUTH_CONFIG_FILE_PATH, 'utf8')
// writes whatever's in `stuff` to `AUTH_CONFIG_FILE_PATH`, atomically
const writeToAuthConfigFile = stuff =>
writeFileSync(AUTH_CONFIG_FILE_PATH, prettify(stuff))
function getConfigFilePath() {
return CONFIG_FILE_PATH
}
function getAuthConfigFilePath() {
return AUTH_CONFIG_FILE_PATH
}
module.exports = {
readConfigFile,
writeToConfigFile,
readAuthConfigFile,
writeToAuthConfigFile,
getConfigFilePath,
getAuthConfigFilePath
}

31
src/util/copy-to-clipboard.js

@ -0,0 +1,31 @@
const { write } = require('clipboardy')
const copyToClipboard = async (
str: string,
shouldCopy = 'auto',
isTTY = process.stdout.isTTY
): boolean => {
if (shouldCopy === false) {
return false
}
if (shouldCopy === 'auto') {
if (isTTY) {
await write(str)
return true
} else {
return false
}
}
if (shouldCopy === true) {
await write(str)
return true
}
throw new TypeError(
'The `copyToClipbard` value in now config has an invalid type'
)
}
module.exports = copyToClipboard

15
src/util/humanize-path.js

@ -0,0 +1,15 @@
// @flow
const { homedir } = require('os')
const { resolve } = require('path')
const humanizePath = (path: string) => {
const resolved: string = resolve(path)
const _homedir = homedir()
if (resolved.indexOf(_homedir) === 0) {
return `~` + resolved.substr(_homedir.length)
} else {
return resolved
}
}
module.exports = humanizePath

82
src/util/input/list.js

@ -0,0 +1,82 @@
const inquirer = require('inquirer')
const stripAnsi = require('strip-ansi')
// eslint-disable-next-line import/no-unassigned-import
require('./patch-inquirer')
function getLength(string) {
let biggestLength = 0
string.split('\n').map(str => {
str = stripAnsi(str)
if (str.length > biggestLength) {
biggestLength = str.length
}
return undefined
})
return biggestLength
}
module.exports = async function promptList({
message = 'the question',
// eslint-disable-line no-unused-vars
choices = [
{
name: 'something\ndescription\ndetails\netc',
value: 'something unique',
short: 'generally the first line of `name`'
}
],
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
separator = true, // Puts a blank separator between each choice
// Wether the `abort` option will be at the `start` or the `end`
// can be `false`
abort = 'end'
}) {
let biggestLength = 0
choices = choices.map(choice => {
if (choice.name) {
const length = getLength(choice.name)
if (length > biggestLength) {
biggestLength = length
}
return choice
}
throw new Error('Invalid choice')
})
if (separator === true) {
choices = choices.reduce(
(prev, curr) => prev.concat(new inquirer.Separator(' '), curr),
[]
)
}
if (abort) {
const abortSeparator = new inquirer.Separator('─'.repeat(biggestLength))
const _abort = {
name: 'Abort',
value: undefined
}
if (abort === 'start') {
const blankSep = choices.shift()
choices.unshift(abortSeparator)
choices.unshift(_abort)
choices.unshift(blankSep)
} else {
choices.push(abortSeparator)
choices.push(_abort)
}
}
const nonce = Date.now()
const answer = await inquirer.prompt({
name: nonce,
type: 'list',
message,
choices,
pageSize
})
return answer[nonce]
}

18
src/util/input/patch-inquirer.js

@ -0,0 +1,18 @@
const inquirer = require('inquirer')
const chalk = require('chalk')
// Here we patch inquirer to use a `>` instead of the ugly green `?`
const getQuestion = function() {
var message = chalk.bold('> ' + this.opt.message) + ' '
// Append the default if available, and if question isn't answered
if (this.opt.default != null && this.status !== 'answered') {
message += chalk.dim('(' + this.opt.default + ') ')
}
return message
}
/* eslint-enable */
inquirer.prompt.prompts.input.prototype.getQuestion = getQuestion
inquirer.prompt.prompts.list.prototype.getQuestion = getQuestion

68
src/util/input/prompt-bool.js

@ -0,0 +1,68 @@
// theirs
const chalk = require('chalk')
// ours
const eraseLines = require('../output/erase-lines')
module.exports = (
label,
{
defaultValue = false,
abortSequences = new Set(['\u0003', '\u001b']), // ctrl+c, esc
resolveChars = new Set(['\r']), // enter
yesChar = 'y',
noChar = 'n',
stdin = process.stdin,
stdout = process.stdout,
// if `true`, `eraseLines(1)` will be `stdout.write`d before
// `resolve`ing or `reject`ing
clearWhenDone = true
} = {}
) => {
return new Promise((resolve, reject) => {
const isRaw = stdin.isRaw
stdin.setRawMode(true)
stdin.resume()
function restore() {
if (clearWhenDone) {
stdout.write(eraseLines(1))
}
stdin.setRawMode(isRaw)
stdin.pause()
stdin.removeListener('data', onData)
}
function onData(buffer) {
const data = buffer.toString()
if (data[0].toLowerCase() === yesChar) {
restore()
resolve(true)
} else if (data[0].toLowerCase() === noChar) {
restore()
resolve(false)
} else if (abortSequences.has(data)) {
restore()
const e = new Error('User abort')
e.code = 'USER_ABORT'
reject(e)
} else if (resolveChars.has(data[0])) {
restore()
resolve(defaultValue)
} else {
// ignore extraneous input
}
}
const defaultText =
defaultValue === null
? `[${yesChar}|${noChar}]`
: defaultValue
? `[${chalk.bold(yesChar.toUpperCase())}|${noChar}]`
: `[${yesChar}|${chalk.bold(noChar.toUpperCase())}]`
stdout.write(`${label} ${chalk.gray(defaultText)} `)
stdin.on('data', onData)
})
}

103
src/util/input/text.js

@ -0,0 +1,103 @@
// inspired by https://github.com/zeit/email-prompt
// theirs
const ansiEscapes = require('ansi-escapes')
const stripAnsi = require('strip-ansi')
// ours
const eraseLines = require('../output/erase-lines')
const ESCAPES = {
LEFT: '\u001B[D',
RIGHT: '\u001B[C',
CTRL_C: '\x03',
BACKSPACE: '\u0008',
CTRL_H: '\u007F',
CARRIAGE: '\r'
}
const textInput = ({
label = 'Enter some text: ',
resolveChars = new Set([ESCAPES.CARRIAGE]),
abortChars = new Set([ESCAPES.CTRL_C]),
// if `true`, `eraseLines(1)` will be `stdout.write`d before
// `resolve`ing or `reject`ing
clearWhenDone = true
}) => {
return new Promise((resolve, reject) => {
if (!process.stdin.setRawMode) {
// Some environments (e.g., cygwin) don't provide a tty
const e = new Error('stdin lacks setRawMode support')
e.userError = true
restore()
reject(e)
}
const isRaw = process.stdin.isRaw
process.stdin.setRawMode(true)
process.stdin.resume()
process.stdout.write(label)
let input = '' // Whatever the user types
let caretOffset = 0 // Left/right keys
const onData = buffer => {
let data = buffer.toString()
if (abortChars.has(data)) {
const e = new Error('User abort')
e.code = 'USER_ABORT'
restore()
return reject(e)
}
if (data === ESCAPES.LEFT) {
if (input.length > Math.abs(caretOffset)) {
caretOffset--
}
} else if (data === ESCAPES.RIGHT) {
if (caretOffset < 0) {
caretOffset++
}
} else if (data === '\x08' || data === '\x7f') {
// Delete key needs splicing according to caret position
input =
input.substr(0, input.length + caretOffset - 1) +
input.substr(input.length + caretOffset)
} else {
if (resolveChars.has(data)) {
restore()
resolve(input)
}
if (stripAnsi(data).length !== data.length) {
data = ''
}
input =
input.substr(0, input.length + caretOffset) +
stripAnsi(data) +
input.substr(input.length + caretOffset)
}
process.stdout.write(eraseLines(1) + label + input)
if (caretOffset) {
process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset)))
}
}
const restore = () => {
if (clearWhenDone) {
process.stdout.write(eraseLines(1))
}
process.stdin.setRawMode(isRaw)
process.stdin.pause()
process.stdin.removeListener('data', onData)
}
process.stdin.on('data', onData)
})
}
module.exports = textInput

5
src/util/output/aborted.js

@ -0,0 +1,5 @@
const { red } = require('chalk')
const error = msg => `${red('> Aborted!')} ${msg}`
module.exports = error

7
src/util/output/chars.js

@ -0,0 +1,7 @@
const chars = {
// in some setups now.exe crashes if we use
// the normal tick unicode character :|
tick: process.platform === 'win32' ? '√' : '✔'
}
module.exports = chars

5
src/util/output/cmd.js

@ -0,0 +1,5 @@
const { gray, cyan } = require('chalk')
const cmd = text => `${gray('`')}${cyan(text)}${gray('`')}`
module.exports = cmd

5
src/util/output/effect.js

@ -0,0 +1,5 @@
const { gray } = require('chalk')
const effect = msg => `${gray(`+ ${msg}`)}`
module.exports = effect

5
src/util/output/erase-lines.js

@ -0,0 +1,5 @@
const ansiEscapes = require('ansi-escapes')
const eraseLines = n => ansiEscapes.eraseLines(n)
module.exports = eraseLines

7
src/util/output/error.js

@ -0,0 +1,7 @@
const { red } = require('chalk')
// error('woot') === '> woot'
// error('woot', 'yay') === 'woot\nyay'
const error = (...msgs) => `${red('> Error!')} ${msgs.join('\n')}`
module.exports = error

5
src/util/output/highlight.js

@ -0,0 +1,5 @@
const { bold } = require('chalk')
const highlight = text => bold.underline(text)
module.exports = highlight

7
src/util/output/info.js

@ -0,0 +1,7 @@
const { gray } = require('chalk')
// info('woot') === '> woot'
// info('woot', 'yay') === 'woot\nyay'
const info = (...msgs) => `${gray('>')} ${msgs.join('\n')}`
module.exports = info

5
src/util/output/link.js

@ -0,0 +1,5 @@
const { underline } = require('chalk')
const highlight = text => underline(text)
module.exports = highlight

17
src/util/output/list-item.js

@ -0,0 +1,17 @@
const { gray } = require('chalk')
// listItem('woot') === '- woot'
// listItem('->', 'woot') === '-> woot'
// listItem(1, 'woot') === '1. woot'
const listItem = (n, msg) => {
if (!msg) {
msg = n
n = '-'
}
if (!isNaN(n)) {
n += '.'
}
return `${gray(n)} ${msg}`
}
module.exports = listItem

3
src/util/output/logo.js

@ -0,0 +1,3 @@
const logo = () => (process.platform === 'win32' ? 'Δ' : '𝚫')
module.exports = logo

5
src/util/output/note.js

@ -0,0 +1,5 @@
const { yellow } = require('chalk')
const note = msg => `${yellow('> NOTE:')} ${msg}`
module.exports = note

6
src/util/output/ok.js

@ -0,0 +1,6 @@
const { cyan } = require('chalk')
const { tick } = require('./chars')
const ok = msg => `${cyan(tick)} ${msg}`
module.exports = ok

5
src/util/output/param.js

@ -0,0 +1,5 @@
const { gray, bold } = require('chalk')
const param = text => `${gray('"')}${bold(text)}${gray('"')}`
module.exports = param

5
src/util/output/ready.js

@ -0,0 +1,5 @@
const { cyan } = require('chalk')
const ready = msg => `${cyan('> Ready!')} ${msg}`
module.exports = ready

5
src/util/output/success.js

@ -0,0 +1,5 @@
const { cyan } = require('chalk')
const success = msg => `${cyan('> Success!')} ${msg}`
module.exports = success

16
src/util/output/wait.js

@ -0,0 +1,16 @@
const ora = require('ora')
const { gray } = require('chalk')
const eraseLines = require('./erase-lines')
const wait = msg => {
const spinner = ora(gray(msg))
spinner.color = 'gray'
spinner.start()
return () => {
spinner.stop()
process.stdout.write(eraseLines(1))
}
}
module.exports = wait

7
src/util/sleep.js

@ -0,0 +1,7 @@
const sleep = ms => {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
module.exports = sleep

7
test.js

@ -0,0 +1,7 @@
const resolve = require('./src/resolve')
resolve('now-examples/wordpress')
.then(dir => {
console.log(dir)
})
.catch(console.error)

4255
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save