Compare commits

...

89 Commits

Author SHA1 Message Date
Steven
500c36f5d4 Publish
- now@16.3.1
 - now-client@5.2.0
 - @now/next@1.0.3
 - @now/python@0.3.1
 - @now/static-build@0.10.1
2019-10-03 13:54:14 -04:00
Steven
69dbbeac44 [now-cli][now-client] Fix v1 files when defining a directory (#3123)
This is a follow up to #3117 which added a fix for `files` but did not observe directories.

This PR fixes the scenario where a directory is defined such that all files inside the directory should be added uploaded (recursively).

Thanks to @williamli 

[PRODUCT-350] #close

[PRODUCT-350]: https://zeit.atlassian.net/browse/PRODUCT-350
2019-10-03 13:44:21 -04:00
Sophearak Tha
69486c3adb [now-client] [now-cli] Handle notice type (#3122)
This PR handle `notice` type from API respond.
2019-10-03 12:14:02 -04:00
Steven
e6692bb79b [now-cli][now-client] Fix --local-config flag and files key (#3117)
This PR is a followup to #3110 that fixes the first deployment when using the `--local-config` flag and also fixes v1 deployments using the [`files`](https://zeit.co/docs/v1/features/configuration/#files-(array)) key.

The tests have been adjusted so we don't regress in both cases.

Fixes #3099 
Fixes #3105
Fixes #3107
Fixes #3109

[PRODUCT-350] #close

[PRODUCT-350]: https://zeit.atlassian.net/browse/PRODUCT-350
2019-10-03 09:07:01 -04:00
Chris
94fba1d7af [now-python] Encode body as utf-8 before making a request (#3093)
Fixes #3091
2019-10-03 09:06:54 -04:00
Sophearak Tha
223d8f4774 [now-cli] Rename lambda to serverless function (#3100)
This PR fix: [PRODUCT-66] #close

[PRODUCT-66]: https://zeit.atlassian.net/browse/PRODUCT-66
2019-10-03 09:06:49 -04:00
Nathan Rajlich
42e7a7e4e3 [now-static-build] Use stdio: 'inherit' for "dev" script child process (#3112)
Since `@now/static-build` is no longer sniffing the stdio streams for the bound port number in `now dev`, there's no need to have separate stdio streams for the "dev" script. Instead, inherit stdio from the parent process, which will allow for ANSI colors to be used when stdout is a TTY in `now dev`.

Also simplifies the `checkForPort()` function and removes the `promise-timeout` dependency.
2019-10-03 09:06:44 -04:00
Steven
6716fdd49b [now-cli][now-client] Add parameter nowConfig for custom now.json (#3110)
When now-client was implemented, it did not work with `--local-config` flag from now-cli because the only parameters it looks at are the files in a directory.

This fixes the regression in now@16.3.0 so that now-client can accept an optional `nowConfig` object or fallback to the `now.json` file.

Fixes #3099 
Fixes #3105
Fixes #3107
Fixes #3109

[PRODUCT-350] #close


[PRODUCT-350]: https://zeit.atlassian.net/browse/PRODUCT-350
2019-10-03 09:06:38 -04:00
Steven
3b69092fd8 Revert "[tests] Add test all script" since it runs on commit (#3108)
Reverts #3106 from @MAPESO 

The tests seem to be running on each commit which is going to slow down development.
2019-10-03 09:06:32 -04:00
Luis Alvarez D
aa8eaedbc8 [now-next] Upload build-time generated static artifacts (#3096)
For context, when you have a script that generates a new static file at build time (`sitemap.xml` for example), it has to be inside `.next/static`, and then you'll need a Now route for it, with this change you could generate the file inside `public`/`static` and the builder will now take care of it.

The util `includeOnlyEntryDirectory` is no longer being used after this change, should I remove it?
2019-10-03 09:06:25 -04:00
Mark
f519ed373f [tests] Add test all script (#3106)
This PR focuses on adding the `test` script to the `package.json` : )

## Main problem 

Previously there was no `test` script that includes all the tests

<img width="924" alt="Screen Shot 2019-10-01 at 8 13 13 AM" src="https://user-images.githubusercontent.com/16585386/65968681-26f6a080-e429-11e9-9f29-c6fd343fdb12.png">
2019-10-03 09:06:20 -04:00
Ana Trajkovska
851dff4b03 [now-cli] Integrate Projects API v2 (#3063)
This PR integrates v2 of Projects API that fixes an issue for projects named `list` or `remove`, because of the naming of the endpoints in v1. For listing all projects, previously in v1 it was `GET /v1/projects/list` and now it is `GET /v2/projects/`, and for removing a project it was `DELETE /v1/projects/remove`.
2019-10-03 09:06:14 -04:00
Steven
c5ebaa11ea Publish
- gatsby-plugin-now@1.2.2
 - @now/build-utils@0.10.1
 - now@16.3.0
 - now-client@5.1.4
 - @now/go@0.6.0
 - @now/next@1.0.2
 - @now/node@1.0.1
 - @now/python@0.3.0
 - @now/ruby@0.1.7
 - @now/static-build@0.10.0
2019-09-30 09:41:51 -04:00
Andy
934fbc8992 [now-cli] Fix deployment version output (#3097)
It will print `[v2]` for 1.0 deployments when logging initially:

```
• go $ now --force
> WARN! You are using an old version of the Now Platform. More: https://zeit.co/docs/v2/advanced/platform/changes-in-now-2-0
> Deploying ~/projects/zeit/now-builder-v1/examples/docker/go under andyschneider
> Using project now-v1-go-docker
> now-v1-go-docker-xxxxxxx.now.sh [v2] [1s]
> Build completed
> https://now-v1-go-docker-xxxxxxx.now.sh [v1] [in clipboard] (sfo1) [1m]
> Verifying instantiation in sfo1
> ✔ Scaled 1 instance in sfo1 [22s]
> Success! Deployment ready
```

Expected:

```
• go $ nowl --force
> WARN! You are using an old version of the Now Platform. More: https://zeit.co/docs/v2/advanced/platform/changes-in-now-2-0
> Deploying ~/projects/zeit/now-builder-v1/examples/docker/go under andyschneider
> Using project now-v1-go-docker
> now-v1-go-docker-xxxxxxx.now.sh [v1] [2s]
> Build completed
> https://now-v1-go-docker-xxxxxxx.now.sh [v1] [in clipboard] (sfo1) [1m]
> Verifying instantiation in sfo1
> ✔ Scaled 1 instance in sfo1 [23s]
> Success! Deployment ready
```

For v2 it shouldn't print anything, as it's the default.
2019-09-30 09:38:16 -04:00
Max
72cb5515fd [now-client] Use ignore module to handle file ignoring logic (#3092)
This implements ignore handling using `ignore` module, in the same way CLI stable does it
2019-09-30 09:38:06 -04:00
Steven
c7f0770d53 [now-static-build] Make curl silent (#3090)
We added a message in #3068 so this PR makes curl silent unless it errors.

Also fix typo in Gutenberg 😉 

PRODUCT-7 #close
2019-09-30 09:38:00 -04:00
Max
7ea49e8ada [now-cli] Add not_domain_owner error handling (#3045)
This adds missing error handler for `not_domain_owner` error (fixes #3042)
2019-09-30 09:37:55 -04:00
Nathan Rajlich
cae6ce96b3 [now-client] Export TypeScript types (#3087)
This makes downstream compilation with `tsc` work correctly.

Otherwise, compilation fails with errors such as:

```
../now-client/dist/src/index.d.ts:1:40 - error TS2304: Cannot find name 'CreateDeploymentFunction'.

1 export declare const createDeployment: CreateDeploymentFunction;
                                         ~~~~~~~~~~~~~~~~~~~~~~~~
````
2019-09-30 09:37:49 -04:00
Sophearak Tha
3699dfd756 [now-next] Invoke build script (#3073)
This PR make `@now/next` invoke `build` script if user defined.

PRODUCT-106 #close
2019-09-30 09:37:41 -04:00
Leo Lamprecht
6dca96d877 [now-build-utils] Make grouping prerenders optional (#3082)
As of https://github.com/zeit/now/pull/3081, we make it necessary to group `Prerenders` together for being invalidated at the same time.

However, you might not want that. In turn, we'll make it optional.
2019-09-30 09:37:36 -04:00
Leo Lamprecht
88c14b27a2 [now-build-utils] Allow prerender groups to be defined with an integer (#3081)
This pull request removes the `PrerenderGroup` type in favor of a `group` parameter for the existing `Prerender` type.

This parameter takes in an integer that defines a group of prerenders that should be invalidated at the same time:

```
interface Prerender {
  expiration: number;
  lambda: Lambda;
  fallback: FileBlob | FileFsRef | FileRef;
  group: number;
}
```

**Example:** If two `Prerender` instances exist that have `group` set to `1`, they will both be invalidated at the same time.
2019-09-30 09:37:31 -04:00
Max
0d2a9539f6 [now-cli] Add logging to execa calls in tests (#3077)
This implements #3075 for all integration tests
2019-09-30 09:37:25 -04:00
Max
bae160bd7c [now-client] Retry on network error (#3072)
This PR improves handling of occasional network errors in `now-client` which should improve benchmarking introduced in #3062
2019-09-30 09:37:19 -04:00
Max
92852ecff2 [now-cli] Move hexo test to testFixtureStdio (#3076)
This should fix the `now dev` tests that periodically hang
2019-09-30 09:37:12 -04:00
Max
ac0c841cb8 [now-client] Add debug logs (#2997)
This PR adds extensive debug logging to `now-client` and enables it in CLI based on the `--debug` flag

Debug logging works in either of the following two conditions:
- `debug: true` is provided in the `options` object of `createDeployment`/`createLegacyDeployment`
- `process.env.NOW_CLIENT_DEBUG` environment variable is set
2019-09-30 09:37:05 -04:00
Sophearak Tha
53e4b71f89 [now-cli] Render prompt when deploying home directory (#3057)
Fixes #3069 

PRODUCT-160 #close
2019-09-30 09:36:53 -04:00
Steven
017a2692ca [now-static-build] Add test for BUNDLE_WITHOUT env var (#3070)
This adds a test which confirms that `BUNDLE_WITHOUT="test:development"` works properly.

This env var is equivalent to `bundle install --without test development`.

There's no code change here because groups are defined by the user, therefore they must define which ones to ignore (if any).

- Groups Guide: https://bundler.io/v2.0/guides/groups.html
- BUNDLER_WITHOUT: https://bundler.io/v2.0/bundle_config.html#LIST-OF-AVAILABLE-KEYS
- Example Gemfile: https://github.com/thoughtbot/administrate/blob/master/Gemfile

PRODUCT-133 #close
2019-09-30 09:36:45 -04:00
Steven
311f89eecb Use PR description for merge commit body (#3071) 2019-09-30 09:36:39 -04:00
Steven
40d2bc4743 [now-static-build] Print version of static generator during build (#3068)
* [now-static-build] Print version of static generator

* Use curl progress bar
2019-09-30 09:36:33 -04:00
Steven
37160cbc8b [tests] Add benchmark script to randomly generate projects files (#3062) 2019-09-30 09:36:28 -04:00
Steven
3807a2b018 [now-node] Fix sharp test using lock file (#3064) 2019-09-30 09:36:21 -04:00
Steven
b6697dd432 [now-static-build] Add Eleventy to optimized framework list (#3060)
* [now-static-build] Add eleventy to frameworks

* Add test using eleventy-base-blog

* Fix tests with dot files

* Add now.json to 27-eleventy fixture
2019-09-30 09:36:16 -04:00
Sophearak Tha
6c33496e8a [now-cli] Add NOW_BUILDER_DEBUG to build env if --debug (#3041)
* Add `NOW_BUILDER_DEBUG` to build env if `--debug`

* Add `--debug` build env check

* Add `build-env-debug` to prepare
2019-09-30 09:36:08 -04:00
Sophearak Tha
89f32625ed [now-cli] Fix 02-angular-node test fail (#3058)
* Improve `02-angular-node` test

* Add `yarn.lock` with pin version
2019-09-30 09:35:53 -04:00
Steven
8253e76ec0 [now-python] Fix headers with multiple values (#3053)
* [now-python] Add format_headers()

* Add tests

* Fix filenames

* Fix test probes
2019-09-30 09:35:44 -04:00
Andy
e0b3e9606a [now-cli][now-static-build] Ignore output directory from now dev (#3024)
* [now-cli][now-static-build] Ignore output directory from `now dev`

* Add test

* Logging

* Fix test

* Fix test

* Adjust test

* Log failed test

* Log stderr

* Change now.json

* Change Ready check

* Dynamically create now.json

* Log error

* Log stderr on error

* Create now.json first

* Handle JSON error

* Don't use JSON output

* Join path

* Add quotes

* Use .values
2019-09-30 09:35:38 -04:00
Sophearak Tha
dc75a303f7 [now-build-utils] Remove NOW_BUILDER_ANNOTATE (#3027) 2019-09-30 09:35:33 -04:00
Steven
c1eb8ec78c [now-static-build][now-build-utils] Add python static generators (#3048)
* [now-static-build] Run pip install requirements.txt

* Add test for pelican

* Add test for mkdocs
2019-09-30 09:35:26 -04:00
Sophearak Tha
12435f25fd [builders] Consistently capitalize first letter of logs (#3039)
* Consistency capitalize logs line `@now/next`

* Consistency capitalize logs line `@now/node`

* Consistency capitalize logs line `@now/go`

* Consistency capitalize logs line `@now/python`

* Always show `Installing dependencies...`

* Consistency capitalize logs line `run-user-scripts`

* Capitalize `Running`
2019-09-30 09:35:21 -04:00
Steven
d4dc5222cf [now-static-build] Add prepareCache() function (#3047) 2019-09-30 09:35:14 -04:00
Steven
bf1e59b2d3 [now-static-build] Add support for hugo extended (#3043) 2019-09-30 09:35:06 -04:00
Nathan Rajlich
3657e4a36e [now-cli] Update @zeit/fun to v0.10.2 (#3038)
Fixes #2901.
2019-09-30 09:34:43 -04:00
Nathan Rajlich
09efc1d865 [now-cli] Render logs containing "warning" as yellow (#3035) 2019-09-30 09:34:19 -04:00
Steven
22bded50b6 [master only] Fix merge conflict 2019-09-17 18:39:06 -04:00
Steven
b5b02be3c2 [docs] Update CI badge to use master (#3037) 2019-09-17 18:36:49 -04:00
Steven
776f372eb3 [tests] Add env var FORCE_BUILD_IN_REGION (#3036)
* [tests] Add env var FORCE_BUILD_IN_REGION

* Add missing config
2019-09-17 18:35:42 -04:00
Steven
81279fd40b [now-static-build] Add hugo, zola, and gutenberg versioning (#3025)
* [now-static-build] Add hugo, zola, and gutenberg versioning

* Add tests

* Export spawnAsync

* Change spawnAsync to remove cwd
2019-09-17 18:35:34 -04:00
Leo Lamprecht
3342485d29 [now-build-utils] Add types for SPRv2 (#3021)
* Add `Prerender` type for SPRv2

* Make it a default export

* Added `PrerenderGroup` type

* Renamed interface
2019-09-17 18:35:27 -04:00
Andy
028ee848f5 [now-cli] Fix fetch body and add secrets tests (#3030) 2019-09-17 18:35:19 -04:00
Nathan Rajlich
7e64c3b8a9 [now-cli] Assign process.exitCode (#3028)
No real functional change here, but assigning to `process.exitCode`
is the more proper Node.js way to set the exit code for the process.
2019-09-17 18:35:09 -04:00
Nathan Rajlich
704031f7b2 [now-cli] Render "stderr" as red in now logs (#3026) 2019-09-17 18:35:02 -04:00
Andy
5e3c184735 [now-cli] Adjust the printed version for the deployment (#3014) 2019-09-17 18:34:54 -04:00
Steven
88a8022787 [docs] Add scripts for changelog and diff (#3017)
* [release] Add scripts for changelog and diff

* Remove publish docs since it in the wiki

* Add support for windows
2019-09-17 18:34:49 -04:00
Steven
96844dc4a5 [now-node] Change trace errors to warnings (#3016) 2019-09-17 18:34:44 -04:00
Max
a09acd6969 [now-cli][now-client] Remove fetch-h2 (#3011)
* Remove fetch-h2

* Fix package.json

* Fix migration issue

* Improve time() call and ensure consistent header names

* Remove unused agent.ts
2019-09-17 18:34:38 -04:00
Andy
4e232f78de [now-cli] Add test for zero-config and canary builders (#3009)
* [now-cli] Add test for zero-config and canary builders

* Fix test

* Fix test

* Fix url

* Fix path and export

* Make public
2019-09-17 18:34:29 -04:00
Steven
b146a04772 [now-node] Add support for AWS Gateway Event (#3010)
* [now-node] Add support for AWS Gateway Event

* Fix funcName

* Fix makeAwsLauncher export

* Add missing return
2019-09-17 18:34:20 -04:00
Steven
eaaa50e616 Add release notes script to PUBLISHING.md (#3006) 2019-09-17 18:34:14 -04:00
Andy
c893eaeb7a [now-cli] Default to empty string for undefined env var when checking (#3008) 2019-09-17 18:34:08 -04:00
Andy
5bf7d7fd07 [now-cli] Add changelog link to update message (#3001) 2019-09-17 18:34:00 -04:00
Max
ca8fc92b94 [now-cli] Fix links output during deployment (#2966)
* Fix links output during deployment

* Move "synced" log to `total-fileds` event

* Pluralize synced messages

* Update packages/now-cli/src/util/deploy/process-deployment.ts

Co-Authored-By: Steven <steven@ceriously.com>

* Fix failing tests due to stdout mismatch
2019-09-17 18:33:53 -04:00
Steven
9956e85f12 [now-ruby] Use pre-installed ruby 2.5 (#2991)
* [now-ruby] Use pre-installed ruby 2.5

* Change GEM_HOME

* Add polyfill for Node 8
2019-09-17 18:33:47 -04:00
Nathan Rajlich
7fa4739c78 [now-cli] Show a warning for invalid env vars in now dev (#3002)
* [now-cli] Show a warning for invalid env vars in `now dev`

Closes #2982.

* Add "validate env var names" integration test
2019-09-17 18:33:40 -04:00
Andy
0ef2e2a7ec [now-cli] Add tests for alias rules (#3003) 2019-09-17 18:33:34 -04:00
Sophearak Tha
8fd1752acf [now-go] Improve test for custom build flags (#2999) 2019-09-17 18:33:29 -04:00
Nathan Rajlich
14a1446faf [now-cli] Render correct command for now ls $path_alias_url (#2988)
Previously, if you ran `now ls` with a URL for a path alias, then an
error message `Cannot read property 'replace' of undefined` would occur.

Now, a message is logged saying to instead run `now alias ls $url` which
is the correct command to get path rules relevant to a path alias URL.

> Found matching path alias: rules.domain.com
> Please run `now alias ls rules.domain.com` instead

Fixes #2987.
2019-09-17 18:33:16 -04:00
Steven
0c2c8c5ae5 [now-static-build] Run bundle install when Gemfile is found (#2980)
* [now-static-build] Run `bundle install` for Gemfile

* Add logs

* Add timeout in case proc hangs

* Rename test

* Remove console.log()

* Hide warnings

* Use runBundleInstall()

* [now-build-utils] Remove --deployment flag

* Run tests for build-utils
2019-09-17 18:33:07 -04:00
Steven
511b27ad39 [now-node][now-next] Bump node-file-trace to 0.3.1 (#2990) 2019-09-17 18:33:00 -04:00
Sophearak Tha
e22ce7da0a [now-cli] Hide Init Duration and XRAY TraceId by default (#2984) 2019-09-17 18:32:54 -04:00
Steven
d9a4ce06bc [now-build-utils] Add function runBundleInstall() (#2986)
* [now-build-utils] Add function `runBundleInstall`

* Add additional flags

* Set jobs to number of cpus

* Format

* Fix formatting

* Add BUNDLE_APP_CONFIG
2019-09-17 18:32:46 -04:00
Steven
77fb14cc60 [now-node] Bump node-file-trace to 0.3.0 and print warnings (#2985)
* Bump node-file-trace to 0.3.0

* [now-node] Print warnings from node-file-trace
2019-09-17 18:32:39 -04:00
Clément ALLAIN
17c397211e [now-static-build] Fix dev server port detection (#2879)
* [now-static-build] Fix dev server detection

* Code review

* Remove unused dependency

* Fix the checking by really waiting until the port is reachable
2019-09-17 18:32:20 -04:00
Andy
6ca83644bc [now-build-utils][now-cli] Warn instead of throwing on api and pages/api (#2976)
* [now-build-utils][now-cli] Warn instead of throwing on `api` and `pages/api`

* Remove slash and adjust tests

* Remove @now/build-utils

* Hardcode builders

* Add build-utils

* Change default flag

* More logging

* Add static-build

* Remove other packages from package.json

* New file for bundled function
2019-09-17 18:31:56 -04:00
Andy
d1946ea9b6 [now-cli] Display warning when changing the secret name (#2975) 2019-09-17 18:31:44 -04:00
Nathan Rajlich
cc9eae3b71 [now-cli] Use PackageJson and Builder types from @now/build-utils (#2971)
No functionality change here, this just removes the `Package` and
`BuildConfig` types from `src/util/dev/types.ts` in favor of the
matching types from `@now/build-utils`.

Also a lot of prettier formatting…
2019-09-17 18:31:37 -04:00
Nathan Rajlich
7bbc17df4b [now-build-utils] Add env and buildEnv to Meta type (#2970)
* [now-build-utils] Add `env` and `buildEnv` to `Meta` type

`now dev` passes in these variables to the "meta" object.

* Fix build
2019-09-17 18:31:31 -04:00
Andy
df6b2be482 [now-build-utils] Throw error on Next.js pages/api and api/ (#2964) 2019-09-17 18:30:40 -04:00
Sophearak Tha
5ff6263fb7 [now-cli] Add platform in Sentry report (#2960) 2019-09-17 18:30:33 -04:00
Luc
04dc8aaf73 [gatsby-plugin-now] Add keywords in package.json (#2965)
* add keywords to gatsby-plugin-now

* add #readme in homepage url
2019-09-17 18:30:25 -04:00
Sophearak Tha
5435805e58 [now-build-utils] Use debug() on installing to and missing engines (#2954)
* Use debug() on `installing to` output

* Use debug() on `getSupportedNodeVersion` output
2019-09-17 18:30:16 -04:00
Nathan Rajlich
903f819c5d [now-cli] Fix now alias with no arguments (#2959)
Fixes #2941.
2019-09-17 18:29:41 -04:00
Nathan Rajlich
5d927b2d25 [CircleCI] Remove publish-stable and publish-canary steps (#2957)
Publishing to npm is now handled by GitHub Actions.

Aside from that, the Circle publishing was broken.
See: https://circleci.com/gh/zeit/now/7213
2019-09-17 18:29:11 -04:00
Steven
b7a260cc6d [now-python] Use system python with now dev (#2956) 2019-09-17 18:29:03 -04:00
Steven
e8ba8fb97b [now-cli] Bump @zeit/fun to 0.10.0 (#2955) 2019-09-17 18:28:49 -04:00
Max
dd1d9d856b [now-cli] Implement now-client deployments in Now CLI (#2875)
* Imlement `now-client` deployments in Now CLI

* Move now-client to dev dependencies

* Fix missing config for legacy deployments

* Restore no files warning

* Improve error handling

* Port over `--prod`

* Handle single files and warnings better

* Fix legacy deployment env config

* Handle build errors in events

* Don't use ncc for now-client

* Extract `for...await` logic into a `.ts` file

* Revert "Don't use ncc for now-client"

This reverts commit e481a04058952f7011bf5523445256f1b8882dda.

* Add `typings` field to `now-client`

* Regenerate yarn.lock

* Add bootstrap step to CircleCI

* Add bootstrap step before build

* Revert "Add bootstrap step before build"

This reverts commit db9e1113937f113cca8c7c05d5c800fd5d61e84b.

* Revert "Add bootstrap step to CircleCI"

This reverts commit 02c0006a073614814fd174ccbaf1e4e0d8dd3dbf.

* Build `now-client` before CLI

* Sort build scripts

* Tweak empty deployment detection

* Add bootstrap step before build

* Remove now-client dependency from now-client

* Use local dependencies

* Fix paths and regenerate lockfile

* Bypass broken linting rule

* Remove lint ignore

* Use `tsc` instead of `ncc` for `now-client`

Co-Authored-By: Steven <steven@ceriously.com>

* Fix output path for tsc build

* [test] Supress TS warning

* Update packages/now-cli/src/commands/deploy/latest.js

Co-Authored-By: Steven <steven@ceriously.com>

* Update packages/now-cli/src/commands/deploy/latest.js

Co-Authored-By: Steven <steven@ceriously.com>

* Update packages/now-client/package.json

Co-Authored-By: Steven <steven@ceriously.com>

* Change `now-client` output to `dist`

* Implement file events in now-client and bring back progressbar

* Update build script sorting

* Add new logic tests for `now-client`

* Remove redundant target check

* Remove now-client dependency and use local code

* Set exact dependency versions

* Revert "Set exact dependency versions"

This reverts commit e0a31eaf10e498271c9253439d4bbd650738c694.

* Revert local now-client import

* Revert `now-client` dependency to local path

* Implement feedback

* Fix formatting

* Only handle alias errors if `readyState` is `READY`

* Update packages/now-cli/src/commands/deploy/latest.js

Co-Authored-By: Andy <AndyBitz@users.noreply.github.com>

* Update packages/now-cli/src/commands/deploy/latest.js

Co-Authored-By: Andy <AndyBitz@users.noreply.github.com>
2019-09-17 18:27:13 -04:00
Steven
eef4c65e5f Publish
- @now/next@1.0.1
2019-09-17 17:44:26 -04:00
Joe Haddad
3f64594a22 [now-next] Add monorepo autosetup support (#2961)
* [now-next] Add monorepo autosetup support

* Add actual tests

* Remove invalid test

* Correct contents directory

* Update tests

* Support new Next.js canaries
2019-09-17 17:22:22 -04:00
Max Rovensky
3f5f71f8ab Publish
- now-client@5.1.3
2019-09-09 23:32:19 +08:00
Max
2a44179898 [now-client] Fix windows paths handling (#2974)
* Fix windows paths handling in now-client

* Tweak windows paths  handling
2019-09-09 23:31:16 +08:00
328 changed files with 10217 additions and 3375 deletions

View File

@@ -68,6 +68,9 @@ jobs:
command: sudo apt install -y rsync command: sudo apt install -y rsync
- attach_workspace: - attach_workspace:
at: . at: .
- run:
name: Linking dependencies
command: yarn bootstrap
- run: - run:
name: Building name: Building
command: yarn build command: yarn build
@@ -110,25 +113,6 @@ jobs:
name: Linting Code name: Linting Code
command: yarn test-lint command: yarn test-lint
# test-unit:
# docker:
# - image: circleci/node:10
# working_directory: ~/repo
# steps:
# - checkout
# - attach_workspace:
# at: .
# - run:
# name: Compiling `now dev` HTML error templates
# command: node packages/now-cli/scripts/compile-templates.js
# - run:
# name: Running Unit Tests
# command: yarn test-unit --clean false
# - persist_to_workspace:
# root: .
# paths:
# - packages/now-cli/.nyc_output
test-integration-macos-node-8: test-integration-macos-node-8:
macos: macos:
xcode: '9.2.0' xcode: '9.2.0'
@@ -387,36 +371,6 @@ jobs:
name: Finalize Sentry Release name: Finalize Sentry Release
command: sentry-cli releases finalize now-cli@`git describe --tags` command: sentry-cli releases finalize now-cli@`git describe --tags`
publish-stable:
docker:
- image: circleci/node:10
working_directory: ~/repo
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Saving Authentication Information
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- run:
name: Publishing to Stable Channel
command: npm publish --tag latest
publish-canary:
docker:
- image: circleci/node:10
working_directory: ~/repo
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Saving Authentication Information
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- run:
name: Publishing to Canary Channel
command: npm publish --tag canary
workflows: workflows:
version: 2 version: 2
unscheduled: unscheduled:
@@ -437,12 +391,6 @@ workflows:
filters: filters:
tags: tags:
only: /.*/ only: /.*/
# - test-unit:
# requires:
# - build
# filters:
# tags:
# only: /.*/
- test-integration-macos-node-8: - test-integration-macos-node-8:
requires: requires:
- build - build
@@ -523,7 +471,6 @@ workflows:
only: /.*/ only: /.*/
- coverage: - coverage:
requires: requires:
#- test-unit
- test-integration-macos-node-8 - test-integration-macos-node-8
- test-integration-macos-node-10 - test-integration-macos-node-10
- test-integration-macos-node-12 - test-integration-macos-node-12
@@ -541,19 +488,3 @@ workflows:
filters: filters:
tags: tags:
only: /.*/ only: /.*/
- publish-canary:
requires:
- coverage
filters:
tags:
only: /^.*canary.*($|\b)/
branches:
ignore: /.*/
- publish-stable:
requires:
- coverage
filters:
tags:
only: /^(\d+\.)?(\d+\.)?(\*|\d+)$/
branches:
ignore: /.*/

View File

@@ -9,7 +9,7 @@ on:
- '!*' - '!*'
jobs: jobs:
publish: Publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -12,5 +12,6 @@ optimistic_updates = true
[merge.message] [merge.message]
title = "pull_request_title" title = "pull_request_title"
body = "pull_request_body"
include_pr_number = true include_pr_number = true
body_type = "markdown" body_type = "markdown"

View File

@@ -1,40 +0,0 @@
# Publishing to npm
Always publish to the Canary Channel as soon as a PR is merged into the `canary` branch.
```
yarn publish-canary
```
Publish the Stable Channel weekly.
- Cherry pick each commit from `canary` to `master` branch
- Verify that you are _in-sync_ with canary (with the exception of the `version` line in `package.json`)
- Deploy the modified Builders
```
# View differences excluding "Publish" commits
git checkout canary && git pull
git log --pretty=format:"$ad- %s [%an]" | grep -v Publish > ~/Desktop/canary.txt
git checkout master && git pull
git log --pretty=format:"$ad- %s [%an]" | grep -v Publish > ~/Desktop/master.txt
diff ~/Desktop/canary.txt ~/Desktop/master.txt
# Cherry pick all PRs from canary into master ...
git cherry-pick <PR501_COMMIT_SHA>
git cherry-pick <PR502_COMMIT_SHA>
git cherry-pick <PR503_COMMIT_SHA>
git cherry-pick <PR504_COMMIT_SHA>
# Verify the only difference is "version" in package.json
git diff origin/canary
# Ship it
yarn publish-stable
```
After running this publish step, GitHub Actions will take care of publishing the modified Builder packages to npm.
If for some reason GitHub Actions fails to publish the npm package, you may do so
manually by running `npm publish` from the package directory. Make sure to
use `npm publish --tag canary` if you are publishing a canary release!

View File

@@ -1,6 +1,6 @@
![now](https://assets.zeit.co/image/upload/v1542240976/repositories/now-cli/now-cli-repo-banner-v3.png) ![now](https://assets.zeit.co/image/upload/v1542240976/repositories/now-cli/now-cli-repo-banner-v3.png)
[![Build Status](https://circleci.com/gh/zeit/now.svg?&style=shield)](https://circleci.com/gh/zeit/workflows/now) [![Build Status](https://badgen.net/circleci/github/zeit/now/master)](https://circleci.com/gh/zeit/workflows/now/tree/master)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit)
**Note**: The [canary](https://github.com/zeit/now/tree/canary) branch is under heavy development the stable release branch is [master](https://github.com/zeit/now/tree/master). **Note**: The [canary](https://github.com/zeit/now/tree/canary) branch is under heavy development the stable release branch is [master](https://github.com/zeit/now/tree/master).

18
changelog.js Normal file
View File

@@ -0,0 +1,18 @@
const { execSync } = require('child_process');
const commit = execSync('git log --pretty=format:"%s %H"')
.toString()
.trim()
.split('\n')
.find(line => line.startsWith('Publish '))
.split(' ')
.pop();
if (!commit) {
throw new Error('Unable to find last publish commit');
}
const log = execSync(`git log --pretty=format:"- %s [%an]" ${commit}...HEAD`).toString().trim();
console.log(`Changes since the last publish commit ${commit}:`);
console.log(`\n${log}\n`);

36
diff.js Normal file
View File

@@ -0,0 +1,36 @@
const { execSync } = require('child_process');
const { join } = require('path');
const { tmpdir } = require('os');
const { mkdirSync, writeFileSync } = require('fs');
function getCommits(count) {
return execSync('git log --pretty=format:"%s [%an]"')
.toString()
.trim()
.split('\n')
.slice(0, count)
.filter(line => !line.startsWith('Publish '))
.join('\n');
}
function main(count = '100') {
console.log(`Generating diff using last ${count} commits...`);
const randomTmpId = Math.random().toString().slice(2);
const dir = join(tmpdir(), 'now-diff' + randomTmpId);
mkdirSync(dir);
execSync('git checkout canary && git pull');
const canary = getCommits(count);
execSync('git checkout master && git pull');
const master = getCommits(count);
writeFileSync(join(dir, 'log.txt'), '# canary\n' + canary);
execSync('git init && git add -A && git commit -m "init"', { cwd: dir });
writeFileSync(join(dir, 'log.txt'), '# master\n' + master);
console.log(`Done generating diff. Run the following:`);
console.log(`cd ${dir}`);
console.log('Then use `git diff` or `git difftool` to view the differences.');
}
main(process.argv[2]);

View File

@@ -33,6 +33,8 @@
"publish-stable": "git checkout master && git pull && lerna version --exact", "publish-stable": "git checkout master && git pull && lerna version --exact",
"publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --exact", "publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --exact",
"publish-from-github": "./.circleci/publish.sh", "publish-from-github": "./.circleci/publish.sh",
"diff": "node diff.js",
"changelog": "node changelog.js",
"build": "node run.js build all", "build": "node run.js build all",
"test-lint": "node run.js test-lint", "test-lint": "node run.js test-lint",
"test-unit": "node run.js test-unit", "test-unit": "node run.js test-unit",

View File

@@ -1,14 +1,18 @@
{ {
"name": "gatsby-plugin-now", "name": "gatsby-plugin-now",
"version": "1.2.1", "version": "1.2.2",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/zeit/now/tree/canary/packages/gatsby-plugin-now", "homepage": "https://github.com/zeit/now/tree/canary/packages/gatsby-plugin-now#readme",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/zeit/now.git", "url": "https://github.com/zeit/now.git",
"directory": "packages/gatsby-plugin-now" "directory": "packages/gatsby-plugin-now"
}, },
"keywords": [
"gatsby",
"gatsby-plugin"
],
"scripts": { "scripts": {
"build": "./build.sh", "build": "./build.sh",
"test-integration-once": "jest --verbose" "test-integration-once": "jest --verbose"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@now/build-utils", "name": "@now/build-utils",
"version": "0.10.0", "version": "0.10.1",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",

View File

@@ -1,7 +1,5 @@
export default function debug(message: string, ...additional: any[]) { export default function debug(message: string, ...additional: any[]) {
if (process.env.NOW_BUILDER_DEBUG) { if (process.env.NOW_BUILDER_DEBUG) {
console.log(message, ...additional); console.log(message, ...additional);
} else if (process.env.NOW_BUILDER_ANNOTATE) {
console.log(`[now-builder-debug] ${message}`, ...additional);
} }
} }

View File

@@ -17,13 +17,13 @@ const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
code: 'missing_build_script', code: 'missing_build_script',
message: message:
'Your `package.json` file is missing a `build` property inside the `script` property.' + 'Your `package.json` file is missing a `build` property inside the `script` property.' +
'\nMore details: https://zeit.co/docs/v2/advanced/platform/frequently-asked-questions#missing-build-script' '\nMore details: https://zeit.co/docs/v2/advanced/platform/frequently-asked-questions#missing-build-script',
}; };
// Static builders are special cased in `@now/static-build` // Static builders are special cased in `@now/static-build`
function getBuilders(): Map<string, Builder> { function getBuilders(): Map<string, Builder> {
return new Map<string, Builder>([ return new Map<string, Builder>([
['next', { src, use: '@now/next', config }] ['next', { src, use: '@now/next', config }],
]); ]);
} }
@@ -35,7 +35,7 @@ function getApiBuilders(): Builder[] {
{ src: 'api/**/*.ts', use: '@now/node', config }, { src: 'api/**/*.ts', use: '@now/node', config },
{ src: 'api/**/*.go', use: '@now/go', config }, { src: 'api/**/*.go', use: '@now/go', config },
{ src: 'api/**/*.py', use: '@now/python', config }, { src: 'api/**/*.py', use: '@now/python', config },
{ src: 'api/**/*.rb', use: '@now/ruby', config } { src: 'api/**/*.rb', use: '@now/ruby', config },
]; ];
} }
@@ -107,6 +107,31 @@ async function detectApiBuilders(files: string[]): Promise<Builder[]> {
return finishedBuilds as Builder[]; return finishedBuilds as Builder[];
} }
// When a package has files that conflict with `/api` routes
// e.g. Next.js pages/api we'll check it here and return an error.
async function checkConflictingFiles(
files: string[],
builders: Builder[]
): Promise<ErrorResponse | null> {
// For Next.js
if (builders.some(builder => builder.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
const hasApiBuilders = builders.some(builder =>
builder.src.startsWith('api/')
);
if (hasApiPages && hasApiBuilders) {
return {
code: 'conflicting_files',
message:
'It is not possible to use `api` and `pages/api` at the same time, please only use one option',
};
}
}
return null;
}
// When zero config is used we can call this function // When zero config is used we can call this function
// to determine what builders to use // to determine what builders to use
export async function detectBuilders( export async function detectBuilders(
@@ -116,20 +141,28 @@ export async function detectBuilders(
): Promise<{ ): Promise<{
builders: Builder[] | null; builders: Builder[] | null;
errors: ErrorResponse[] | null; errors: ErrorResponse[] | null;
warnings: ErrorResponse[];
}> { }> {
const errors: ErrorResponse[] = []; const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = [];
// Detect all builders for the `api` directory before anything else // Detect all builders for the `api` directory before anything else
let builders = await detectApiBuilders(files); let builders = await detectApiBuilders(files);
if (pkg && hasBuildScript(pkg)) { if (pkg && hasBuildScript(pkg)) {
builders.push(await detectBuilder(pkg)); builders.push(await detectBuilder(pkg));
const conflictError = await checkConflictingFiles(files, builders);
if (conflictError) {
warnings.push(conflictError);
}
} else { } else {
if (pkg && builders.length === 0) { if (pkg && builders.length === 0) {
// We only show this error when there are no api builders // We only show this error when there are no api builders
// since the dependencies of the pkg could be used for those // since the dependencies of the pkg could be used for those
errors.push(MISSING_BUILD_SCRIPT_ERROR); errors.push(MISSING_BUILD_SCRIPT_ERROR);
return { errors, builders: null }; return { errors, warnings, builders: null };
} }
// We allow a `public` directory // We allow a `public` directory
@@ -138,7 +171,7 @@ export async function detectBuilders(
builders.push({ builders.push({
use: '@now/static', use: '@now/static',
src: 'public/**/*', src: 'public/**/*',
config config,
}); });
} else if (builders.length > 0) { } else if (builders.length > 0) {
// We can't use pattern matching, since `!(api)` and `!(api)/**/*` // We can't use pattern matching, since `!(api)` and `!(api)/**/*`
@@ -150,7 +183,7 @@ export async function detectBuilders(
.map(name => ({ .map(name => ({
use: '@now/static', use: '@now/static',
src: name, src: name,
config config,
})) }))
); );
} }
@@ -177,6 +210,7 @@ export async function detectBuilders(
return { return {
builders: builders.length ? builders : null, builders: builders.length ? builders : null,
errors: errors.length ? errors : null errors: errors.length ? errors : null,
warnings,
}; };
} }

View File

@@ -1,5 +1,6 @@
import { intersects } from 'semver'; import { intersects } from 'semver';
import { NodeVersion } from '../types'; import { NodeVersion } from '../types';
import debug from '../debug';
const supportedOptions: NodeVersion[] = [ const supportedOptions: NodeVersion[] = [
{ major: 10, range: '10.x', runtime: 'nodejs10.x' }, { major: 10, range: '10.x', runtime: 'nodejs10.x' },
@@ -20,7 +21,7 @@ export async function getSupportedNodeVersion(
if (!engineRange) { if (!engineRange) {
if (!silent) { if (!silent) {
console.log( debug(
'missing `engines` in `package.json`, using default range: ' + 'missing `engines` in `package.json`, using default range: ' +
selection.range selection.range
); );
@@ -34,7 +35,7 @@ export async function getSupportedNodeVersion(
}); });
if (found) { if (found) {
if (!silent) { if (!silent) {
console.log( debug(
'Found `engines` in `package.json`, selecting range: ' + 'Found `engines` in `package.json`, selecting range: ' +
selection.range selection.range
); );

View File

@@ -5,18 +5,18 @@ import debug from '../debug';
import spawn from 'cross-spawn'; import spawn from 'cross-spawn';
import { SpawnOptions } from 'child_process'; import { SpawnOptions } from 'child_process';
import { deprecate } from 'util'; import { deprecate } from 'util';
import { cpus } from 'os';
import { Meta, PackageJson, NodeVersion, Config } from '../types'; import { Meta, PackageJson, NodeVersion, Config } from '../types';
import { getSupportedNodeVersion } from './node-version'; import { getSupportedNodeVersion } from './node-version';
function spawnAsync( export function spawnAsync(
command: string, command: string,
args: string[], args: string[],
cwd: string,
opts: SpawnOptions = {} opts: SpawnOptions = {}
) { ) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const stderrLogs: Buffer[] = []; const stderrLogs: Buffer[] = [];
opts = { stdio: 'inherit', cwd, ...opts }; opts = { stdio: 'inherit', ...opts };
const child = spawn(command, args, opts); const child = spawn(command, args, opts);
if (opts.stdio === 'pipe' && child.stderr) { if (opts.stdio === 'pipe' && child.stderr) {
@@ -55,7 +55,10 @@ export async function runShellScript(
assert(path.isAbsolute(fsPath)); assert(path.isAbsolute(fsPath));
const destPath = path.dirname(fsPath); const destPath = path.dirname(fsPath);
await chmodPlusX(fsPath); await chmodPlusX(fsPath);
await spawnAsync(`./${path.basename(fsPath)}`, args, destPath, spawnOpts); await spawnAsync(`./${path.basename(fsPath)}`, args, {
cwd: destPath,
...spawnOpts,
});
return true; return true;
} }
@@ -140,33 +143,89 @@ export async function runNpmInstall(
assert(path.isAbsolute(destPath)); assert(path.isAbsolute(destPath));
let commandArgs = args; let commandArgs = args;
console.log(`installing to ${destPath}`); debug(`Installing to ${destPath}`);
const { hasPackageLockJson } = await scanParentDirs(destPath); const { hasPackageLockJson } = await scanParentDirs(destPath);
const opts = spawnOpts || { env: process.env }; const opts = { cwd: destPath, ...spawnOpts } || {
cwd: destPath,
env: process.env,
};
if (hasPackageLockJson) { if (hasPackageLockJson) {
commandArgs = args.filter(a => a !== '--prefer-offline'); commandArgs = args.filter(a => a !== '--prefer-offline');
await spawnAsync( await spawnAsync(
'npm', 'npm',
commandArgs.concat(['install', '--unsafe-perm']), commandArgs.concat(['install', '--unsafe-perm']),
destPath,
opts opts
); );
} else { } else {
await spawnAsync( await spawnAsync(
'yarn', 'yarn',
commandArgs.concat(['--ignore-engines', '--cwd', destPath]), commandArgs.concat(['--ignore-engines', '--cwd', destPath]),
destPath,
opts opts
); );
} }
} }
export async function runBundleInstall(
destPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions,
meta?: Meta
) {
if (meta && meta.isDev) {
debug('Skipping dependency installation because dev mode is enabled');
return;
}
assert(path.isAbsolute(destPath));
const opts = { cwd: destPath, ...spawnOpts } || {
cwd: destPath,
env: process.env,
};
await spawnAsync(
'bundle',
args.concat([
'install',
'--no-prune',
'--retry',
'3',
'--jobs',
String(cpus().length || 1),
]),
opts
);
}
export async function runPipInstall(
destPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions,
meta?: Meta
) {
if (meta && meta.isDev) {
debug('Skipping dependency installation because dev mode is enabled');
return;
}
assert(path.isAbsolute(destPath));
const opts = { cwd: destPath, ...spawnOpts } || {
cwd: destPath,
env: process.env,
};
await spawnAsync(
'pip3',
['install', '--disable-pip-version-check', ...args],
opts
);
}
export async function runPackageJsonScript( export async function runPackageJsonScript(
destPath: string, destPath: string,
scriptName: string, scriptName: string,
opts?: SpawnOptions spawnOpts?: SpawnOptions
) { ) {
assert(path.isAbsolute(destPath)); assert(path.isAbsolute(destPath));
const { packageJson, hasPackageLockJson } = await scanParentDirs( const { packageJson, hasPackageLockJson } = await scanParentDirs(
@@ -181,17 +240,14 @@ export async function runPackageJsonScript(
); );
if (!hasScript) return false; if (!hasScript) return false;
const opts = { cwd: destPath, ...spawnOpts };
if (hasPackageLockJson) { if (hasPackageLockJson) {
console.log(`running "npm run ${scriptName}"`); console.log(`Running "npm run ${scriptName}"`);
await spawnAsync('npm', ['run', scriptName], destPath, opts); await spawnAsync('npm', ['run', scriptName], opts);
} else { } else {
console.log(`running "yarn run ${scriptName}"`); console.log(`Running "yarn run ${scriptName}"`);
await spawnAsync( await spawnAsync('yarn', ['--cwd', destPath, 'run', scriptName], opts);
'yarn',
['--cwd', destPath, 'run', scriptName],
destPath,
opts
);
} }
return true; return true;

View File

@@ -2,14 +2,18 @@ import FileBlob from './file-blob';
import FileFsRef from './file-fs-ref'; import FileFsRef from './file-fs-ref';
import FileRef from './file-ref'; import FileRef from './file-ref';
import { Lambda, createLambda } from './lambda'; import { Lambda, createLambda } from './lambda';
import { Prerender } from './prerender';
import download, { DownloadedFiles } from './fs/download'; import download, { DownloadedFiles } from './fs/download';
import getWriteableDirectory from './fs/get-writable-directory'; import getWriteableDirectory from './fs/get-writable-directory';
import glob from './fs/glob'; import glob from './fs/glob';
import rename from './fs/rename'; import rename from './fs/rename';
import { import {
spawnAsync,
installDependencies, installDependencies,
runPackageJsonScript, runPackageJsonScript,
runNpmInstall, runNpmInstall,
runBundleInstall,
runPipInstall,
runShellScript, runShellScript,
getNodeVersion, getNodeVersion,
getSpawnOptions, getSpawnOptions,
@@ -26,14 +30,18 @@ export {
FileRef, FileRef,
Lambda, Lambda,
createLambda, createLambda,
Prerender,
download, download,
DownloadedFiles, DownloadedFiles,
getWriteableDirectory, getWriteableDirectory,
glob, glob,
rename, rename,
spawnAsync,
installDependencies, installDependencies,
runPackageJsonScript, runPackageJsonScript,
runNpmInstall, runNpmInstall,
runBundleInstall,
runPipInstall,
runShellScript, runShellScript,
getNodeVersion, getNodeVersion,
getSpawnOptions, getSpawnOptions,

View File

@@ -0,0 +1,35 @@
import FileBlob from './file-blob';
import FileFsRef from './file-fs-ref';
import FileRef from './file-ref';
import { Lambda } from './lambda';
interface PrerenderOptions {
expiration: number;
lambda: Lambda;
fallback: FileBlob | FileFsRef | FileRef;
group?: number;
}
export class Prerender {
public type: 'Prerender';
public expiration: number;
public lambda: Lambda;
public fallback: FileBlob | FileFsRef | FileRef;
public group?: number;
constructor({ expiration, lambda, fallback, group }: PrerenderOptions) {
this.type = 'Prerender';
this.expiration = expiration;
this.lambda = lambda;
this.fallback = fallback;
if (
typeof group !== 'undefined' &&
(group <= 0 || !Number.isInteger(group))
) {
throw new Error('The `group` argument for `Prerender` needs to be a natural number.');
}
this.group = group;
}
}

View File

@@ -1,6 +1,10 @@
import FileRef from './file-ref'; import FileRef from './file-ref';
import FileFsRef from './file-fs-ref'; import FileFsRef from './file-fs-ref';
export interface Env {
[name: string]: string | undefined;
}
export interface File { export interface File {
type: string; type: string;
mode: number; mode: number;
@@ -52,6 +56,8 @@ export interface Meta {
requestPath?: string; requestPath?: string;
filesChanged?: string[]; filesChanged?: string[];
filesRemoved?: string[]; filesRemoved?: string[];
env?: Env;
buildEnv?: Env;
} }
export interface AnalyzeOptions { export interface AnalyzeOptions {

View File

@@ -6,11 +6,11 @@ const { createZip } = require('../dist/lambda');
const { glob, download, detectBuilders, detectRoutes } = require('../'); const { glob, download, detectBuilders, detectRoutes } = require('../');
const { const {
getSupportedNodeVersion, getSupportedNodeVersion,
defaultSelection defaultSelection,
} = require('../dist/fs/node-version'); } = require('../dist/fs/node-version');
const { const {
packAndDeploy, packAndDeploy,
testDeployment testDeployment,
} = require('../../../test/lib/deployment/test-deployment'); } = require('../../../test/lib/deployment/test-deployment');
jest.setTimeout(4 * 60 * 1000); jest.setTimeout(4 * 60 * 1000);
@@ -38,7 +38,7 @@ it('should re-create symlinks properly', async () => {
const [linkStat, aStat] = await Promise.all([ const [linkStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')), fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'a.txt')) fs.lstat(path.join(outDir, 'a.txt')),
]); ]);
assert(linkStat.isSymbolicLink()); assert(linkStat.isSymbolicLink());
assert(aStat.isFile()); assert(aStat.isFile());
@@ -60,7 +60,7 @@ it('should create zip files with symlinks properly', async () => {
const [linkStat, aStat] = await Promise.all([ const [linkStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')), fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'a.txt')) fs.lstat(path.join(outDir, 'a.txt')),
]); ]);
assert(linkStat.isSymbolicLink()); assert(linkStat.isSymbolicLink());
assert(aStat.isFile()); assert(aStat.isFile());
@@ -120,7 +120,7 @@ it('should support require by path for legacy builders', () => {
const glob2 = require('@now/build-utils/fs/glob.js'); const glob2 = require('@now/build-utils/fs/glob.js');
const rename2 = require('@now/build-utils/fs/rename.js'); const rename2 = require('@now/build-utils/fs/rename.js');
const { const {
runNpmInstall: runNpmInstall2 runNpmInstall: runNpmInstall2,
} = require('@now/build-utils/fs/run-user-scripts.js'); } = require('@now/build-utils/fs/run-user-scripts.js');
const streamToBuffer2 = require('@now/build-utils/fs/stream-to-buffer.js'); const streamToBuffer2 = require('@now/build-utils/fs/stream-to-buffer.js');
@@ -206,7 +206,7 @@ it('Test `detectBuilders`', async () => {
// package.json + no build + next // package.json + no build + next
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
dependencies: { next: '9.0.0' } dependencies: { next: '9.0.0' },
}; };
const files = ['package.json', 'pages/index.js']; const files = ['package.json', 'pages/index.js'];
const { builders, errors } = await detectBuilders(files, pkg); const { builders, errors } = await detectBuilders(files, pkg);
@@ -218,7 +218,7 @@ it('Test `detectBuilders`', async () => {
// package.json + no build + next // package.json + no build + next
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' } devDependencies: { next: '9.0.0' },
}; };
const files = ['package.json', 'pages/index.js']; const files = ['package.json', 'pages/index.js'];
const { builders, errors } = await detectBuilders(files, pkg); const { builders, errors } = await detectBuilders(files, pkg);
@@ -282,7 +282,7 @@ it('Test `detectBuilders`', async () => {
const files = [ const files = [
'api/_utils/handler.js', 'api/_utils/handler.js',
'api/[endpoint]/.helper.js', 'api/[endpoint]/.helper.js',
'api/[endpoint]/[id].js' 'api/[endpoint]/[id].js',
]; ];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
@@ -295,7 +295,7 @@ it('Test `detectBuilders`', async () => {
// api + next + public // api + next + public
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' } devDependencies: { next: '9.0.0' },
}; };
const files = ['package.json', 'api/endpoint.js', 'public/index.html']; const files = ['package.json', 'api/endpoint.js', 'public/index.html'];
@@ -311,7 +311,7 @@ it('Test `detectBuilders`', async () => {
// api + next + raw static // api + next + raw static
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' } devDependencies: { next: '9.0.0' },
}; };
const files = ['package.json', 'api/endpoint.js', 'index.html']; const files = ['package.json', 'api/endpoint.js', 'index.html'];
@@ -343,7 +343,7 @@ it('Test `detectBuilders`', async () => {
'api/endpoint.js', 'api/endpoint.js',
'public/index.html', 'public/index.html',
'public/favicon.ico', 'public/favicon.ico',
'README.md' 'README.md',
]; ];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
@@ -367,7 +367,7 @@ it('Test `detectBuilders`', async () => {
// next + public // next + public
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' } devDependencies: { next: '9.0.0' },
}; };
const files = ['package.json', 'public/index.html', 'README.md']; const files = ['package.json', 'public/index.html', 'README.md'];
@@ -381,7 +381,7 @@ it('Test `detectBuilders`', async () => {
// nuxt // nuxt
const pkg = { const pkg = {
scripts: { build: 'nuxt build' }, scripts: { build: 'nuxt build' },
dependencies: { nuxt: '2.8.1' } dependencies: { nuxt: '2.8.1' },
}; };
const files = ['package.json', 'pages/index.js']; const files = ['package.json', 'pages/index.js'];
@@ -433,12 +433,12 @@ it('Test `detectBuilders`', async () => {
// package.json + api + canary // package.json + api + canary
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
dependencies: { next: '9.0.0' } dependencies: { next: '9.0.0' },
}; };
const files = [ const files = [
'pages/index.js', 'pages/index.js',
'api/[endpoint].js', 'api/[endpoint].js',
'api/[endpoint]/[id].js' 'api/[endpoint]/[id].js',
]; ];
const { builders } = await detectBuilders(files, pkg, { tag: 'canary' }); const { builders } = await detectBuilders(files, pkg, { tag: 'canary' });
@@ -452,12 +452,12 @@ it('Test `detectBuilders`', async () => {
// package.json + api + latest // package.json + api + latest
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
dependencies: { next: '9.0.0' } dependencies: { next: '9.0.0' },
}; };
const files = [ const files = [
'pages/index.js', 'pages/index.js',
'api/[endpoint].js', 'api/[endpoint].js',
'api/[endpoint]/[id].js' 'api/[endpoint]/[id].js',
]; ];
const { builders } = await detectBuilders(files, pkg, { tag: 'latest' }); const { builders } = await detectBuilders(files, pkg, { tag: 'latest' });
@@ -471,12 +471,12 @@ it('Test `detectBuilders`', async () => {
// package.json + api + random tag // package.json + api + random tag
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
dependencies: { next: '9.0.0' } dependencies: { next: '9.0.0' },
}; };
const files = [ const files = [
'pages/index.js', 'pages/index.js',
'api/[endpoint].js', 'api/[endpoint].js',
'api/[endpoint]/[id].js' 'api/[endpoint]/[id].js',
]; ];
const { builders } = await detectBuilders(files, pkg, { tag: 'haha' }); const { builders } = await detectBuilders(files, pkg, { tag: 'haha' });
@@ -485,6 +485,23 @@ it('Test `detectBuilders`', async () => {
expect(builders[2].use).toBe('@now/next@haha'); expect(builders[2].use).toBe('@now/next@haha');
expect(builders.length).toBe(3); expect(builders.length).toBe(3);
} }
{
// next.js pages/api + api
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const files = ['api/user.js', 'pages/api/user.js'];
const { warnings, errors, builders } = await detectBuilders(files, pkg);
expect(errors).toBe(null);
expect(warnings[0].code).toBe('conflicting_files');
expect(builders).toBeDefined();
expect(builders.length).toBe(2);
expect(builders[0].use).toBe('@now/node');
expect(builders[1].use).toBe('@now/next');
}
}); });
it('Test `detectRoutes`', async () => { it('Test `detectRoutes`', async () => {
@@ -545,7 +562,7 @@ it('Test `detectRoutes`', async () => {
const files = [ const files = [
'public/index.html', 'public/index.html',
'api/[endpoint].js', 'api/[endpoint].js',
'api/[endpoint]/[id].js' 'api/[endpoint]/[id].js',
]; ];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
@@ -560,7 +577,7 @@ it('Test `detectRoutes`', async () => {
{ {
const pkg = { const pkg = {
scripts: { build: 'next build' }, scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' } devDependencies: { next: '9.0.0' },
}; };
const files = ['public/index.html', 'api/[endpoint].js']; const files = ['public/index.html', 'api/[endpoint].js'];
@@ -617,7 +634,7 @@ it('Test `detectRoutes`', async () => {
'api/users/index.ts', 'api/users/index.ts',
'api/users/index.d.ts', 'api/users/index.d.ts',
'api/food.ts', 'api/food.ts',
'api/ts/gold.ts' 'api/ts/gold.ts',
]; ];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders); const { defaultRoutes } = await detectRoutes(files, builders);
@@ -641,39 +658,39 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
{ {
path: '/api/my-endpoint', path: '/api/my-endpoint',
mustContain: 'my-endpoint', mustContain: 'my-endpoint',
status: 200 status: 200,
}, },
{ {
path: '/api/other-endpoint', path: '/api/other-endpoint',
mustContain: 'other-endpoint', mustContain: 'other-endpoint',
status: 200 status: 200,
}, },
{ {
path: '/api/team/zeit', path: '/api/team/zeit',
mustContain: 'team/zeit', mustContain: 'team/zeit',
status: 200 status: 200,
}, },
{ {
path: '/api/user/myself', path: '/api/user/myself',
mustContain: 'user/myself', mustContain: 'user/myself',
status: 200 status: 200,
}, },
{ {
path: '/api/not-okay/', path: '/api/not-okay/',
status: 404 status: 404,
}, },
{ {
path: '/api', path: '/api',
status: 404 status: 404,
}, },
{ {
path: '/api/', path: '/api/',
status: 404 status: 404,
}, },
{ {
path: '/', path: '/',
mustContain: 'hello from index.txt' mustContain: 'hello from index.txt',
} },
]; ];
const { builders } = await detectBuilders(files, pkg); const { builders } = await detectBuilders(files, pkg);
@@ -701,32 +718,32 @@ it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
const probes = [ const probes = [
{ {
path: '/api/not-okay', path: '/api/not-okay',
status: 404 status: 404,
}, },
{ {
path: '/api', path: '/api',
mustContain: 'hello from api/index.js', mustContain: 'hello from api/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/', path: '/api/',
mustContain: 'hello from api/index.js', mustContain: 'hello from api/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/index', path: '/api/index',
mustContain: 'hello from api/index.js', mustContain: 'hello from api/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/index.js', path: '/api/index.js',
mustContain: 'hello from api/index.js', mustContain: 'hello from api/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/date.js', path: '/api/date.js',
mustContain: 'hello from api/date.js', mustContain: 'hello from api/date.js',
status: 200 status: 200,
}, },
{ {
// Someone might expect this to be `date.js`, // Someone might expect this to be `date.js`,
@@ -735,27 +752,27 @@ it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
// so it is not special cased // so it is not special cased
path: '/api/date', path: '/api/date',
mustContain: 'hello from api/date/index.js', mustContain: 'hello from api/date/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/date/', path: '/api/date/',
mustContain: 'hello from api/date/index.js', mustContain: 'hello from api/date/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/date/index', path: '/api/date/index',
mustContain: 'hello from api/date/index.js', mustContain: 'hello from api/date/index.js',
status: 200 status: 200,
}, },
{ {
path: '/api/date/index.js', path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js', mustContain: 'hello from api/date/index.js',
status: 200 status: 200,
}, },
{ {
path: '/', path: '/',
mustContain: 'hello from index.txt' mustContain: 'hello from index.txt',
} },
]; ];
const { builders } = await detectBuilders(files, pkg); const { builders } = await detectBuilders(files, pkg);

View File

@@ -1,6 +1,6 @@
{ {
"name": "now", "name": "now",
"version": "16.2.0", "version": "16.3.1",
"preferGlobal": true, "preferGlobal": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "The command-line interface for Now", "description": "The command-line interface for Now",
@@ -61,12 +61,6 @@
"node": ">= 8.11" "node": ">= 8.11"
}, },
"devDependencies": { "devDependencies": {
"@now/build-utils": "0.10.0",
"@now/go": "latest",
"@now/next": "latest",
"@now/node": "latest",
"@now/routing-utils": "1.2.3-canary.0",
"@now/static-build": "latest",
"@sentry/node": "5.5.0", "@sentry/node": "5.5.0",
"@types/ansi-escapes": "3.0.0", "@types/ansi-escapes": "3.0.0",
"@types/ansi-regex": "4.0.0", "@types/ansi-regex": "4.0.0",
@@ -98,7 +92,7 @@
"@types/which": "1.3.1", "@types/which": "1.3.1",
"@types/write-json-file": "2.2.1", "@types/write-json-file": "2.2.1",
"@zeit/dockerignore": "0.0.5", "@zeit/dockerignore": "0.0.5",
"@zeit/fun": "0.9.3", "@zeit/fun": "0.10.2",
"@zeit/ncc": "0.18.5", "@zeit/ncc": "0.18.5",
"@zeit/source-map-support": "0.6.2", "@zeit/source-map-support": "0.6.2",
"ajv": "6.10.2", "ajv": "6.10.2",
@@ -131,10 +125,10 @@
"escape-html": "1.0.3", "escape-html": "1.0.3",
"esm": "3.1.4", "esm": "3.1.4",
"execa": "1.0.0", "execa": "1.0.0",
"fetch-h2": "2.0.3",
"fs-extra": "7.0.1", "fs-extra": "7.0.1",
"glob": "7.1.2", "glob": "7.1.2",
"http-proxy": "1.17.0", "http-proxy": "1.17.0",
"ignore": "4.0.6",
"ini": "1.3.4", "ini": "1.3.4",
"inquirer": "3.3.0", "inquirer": "3.3.0",
"is-url": "1.2.2", "is-url": "1.2.2",
@@ -145,8 +139,9 @@
"mime-types": "2.1.24", "mime-types": "2.1.24",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"mri": "1.1.0", "mri": "1.1.0",
"ms": "2.1.1", "ms": "2.1.2",
"node-fetch": "1.7.3", "node-fetch": "1.7.3",
"now-client": "./packages/now-client",
"npm-package-arg": "6.1.0", "npm-package-arg": "6.1.0",
"nyc": "13.2.0", "nyc": "13.2.0",
"ora": "3.4.0", "ora": "3.4.0",

View File

@@ -8,16 +8,13 @@ import { createWriteStream, mkdirp, remove, writeJSON } from 'fs-extra';
import { getDistTag } from '../src/util/get-dist-tag'; import { getDistTag } from '../src/util/get-dist-tag';
import pkg from '../package.json'; import pkg from '../package.json';
import { getBundledBuilders } from '../src/util/dev/get-bundled-builders';
const dirRoot = join(__dirname, '..'); const dirRoot = join(__dirname, '..');
const bundledBuilders = Object.keys(pkg.devDependencies).filter(d =>
d.startsWith('@now/')
);
async function createBuildersTarball() { async function createBuildersTarball() {
const distTag = getDistTag(pkg.version); const distTag = getDistTag(pkg.version);
const builders = Array.from(bundledBuilders).map(b => `${b}@${distTag}`); const builders = Array.from(getBundledBuilders()).map(b => `${b}@${distTag}`);
console.log(`Creating builders tarball with: ${builders.join(', ')}`); console.log(`Creating builders tarball with: ${builders.join(', ')}`);
const buildersDir = join(dirRoot, '.builders'); const buildersDir = join(dirRoot, '.builders');
@@ -39,7 +36,7 @@ async function createBuildersTarball() {
const yarn = join(dirRoot, '../../node_modules/yarn/bin/yarn.js'); const yarn = join(dirRoot, '../../node_modules/yarn/bin/yarn.js');
await execa(process.execPath, [yarn, 'add', '--no-lockfile', ...builders], { await execa(process.execPath, [yarn, 'add', '--no-lockfile', ...builders], {
cwd: buildersDir, cwd: buildersDir,
stdio: 'inherit' stdio: 'inherit',
}); });
const packer = tar.pack(buildersDir); const packer = tar.pack(buildersDir);
@@ -66,7 +63,7 @@ async function main() {
// Compile the `doT.js` template files for `now dev` // Compile the `doT.js` template files for `now dev`
console.log(); console.log();
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], { await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
stdio: 'inherit' stdio: 'inherit',
}); });
// Do the initial `ncc` build // Do the initial `ncc` build
@@ -92,20 +89,22 @@ async function main() {
// get compiled into the final ncc bundle file, however, we want them to be // get compiled into the final ncc bundle file, however, we want them to be
// present in the npm package because the contents of those files are involved // present in the npm package because the contents of those files are involved
// with `fun`'s cache invalidation mechanism and they need to be shasum'd. // with `fun`'s cache invalidation mechanism and they need to be shasum'd.
const runtimes = join(dirRoot, '../../node_modules/@zeit/fun/dist/src/runtimes'); const runtimes = join(
dirRoot,
'../../node_modules/@zeit/fun/dist/src/runtimes'
);
const dest = join(dirRoot, 'dist/runtimes'); const dest = join(dirRoot, 'dist/runtimes');
await cpy('**/*', dest, { parents: true, cwd: runtimes }); await cpy('**/*', dest, { parents: true, cwd: runtimes });
console.log('Finished building `now-cli`'); console.log('Finished building `now-cli`');
} }
process.on('unhandledRejection', (err: any) => { process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
console.error('Unhandled Rejection:'); console.error('Unhandled Rejection at:', promise, 'reason:', reason);
console.error(err);
process.exit(1); process.exit(1);
}); });
process.on('uncaughtException', (err: any) => { process.on('uncaughtException', err => {
console.error('Uncaught Exception:'); console.error('Uncaught Exception:');
console.error(err); console.error(err);
process.exit(1); process.exit(1);

View File

@@ -38,7 +38,7 @@ export default async function set(
const { const {
authConfig: { token }, authConfig: { token },
config, config,
localConfig localConfig,
} = ctx; } = ctx;
const { currentTeam } = config; const { currentTeam } = config;
@@ -48,14 +48,14 @@ export default async function set(
const { const {
'--debug': debugEnabled, '--debug': debugEnabled,
'--no-verify': noVerify, '--no-verify': noVerify,
'--rules': rulesPath '--rules': rulesPath,
} = opts; } = opts;
const client = new Client({ const client = new Client({
apiUrl, apiUrl,
token, token,
currentTeam, currentTeam,
debug: debugEnabled debug: debugEnabled,
}); });
let contextName = null; let contextName = null;
let user = null; let user = null;
@@ -79,14 +79,14 @@ export default async function set(
return 1; return 1;
} }
if (!isValidName(args[0])) { if (args.length >= 1 && !isValidName(args[0])) {
output.error( output.error(
`The provided argument "${args[0]}" is not a valid deployment` `The provided argument "${args[0]}" is not a valid deployment`
); );
return 1; return 1;
} }
if (!isValidName(args[1])) { if (args.length >= 2 && !isValidName(args[1])) {
output.error(`The provided argument "${args[1]}" is not a valid domain`); output.error(`The provided argument "${args[1]}" is not a valid domain`);
return 1; return 1;
} }

View File

@@ -3,18 +3,14 @@ import bytes from 'bytes';
import { write as copy } from 'clipboardy'; import { write as copy } from 'clipboardy';
import chalk from 'chalk'; import chalk from 'chalk';
import title from 'title'; import title from 'title';
import Progress from 'progress';
import Client from '../../util/client'; import Client from '../../util/client';
import wait from '../../util/output/wait';
import { handleError } from '../../util/error'; import { handleError } from '../../util/error';
import getArgs from '../../util/get-args'; import getArgs from '../../util/get-args';
import toHumanPath from '../../util/humanize-path'; import toHumanPath from '../../util/humanize-path';
import Now from '../../util'; import Now from '../../util';
import stamp from '../../util/output/stamp.ts'; import stamp from '../../util/output/stamp.ts';
import { isReady, isDone, isFailed } from '../../util/build-state';
import createDeploy from '../../util/deploy/create-deploy'; import createDeploy from '../../util/deploy/create-deploy';
import getDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host'; import getDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host';
import sleep from '../../util/sleep';
import parseMeta from '../../util/parse-meta'; import parseMeta from '../../util/parse-meta';
import code from '../../util/output/code'; import code from '../../util/output/code';
import param from '../../util/output/param'; import param from '../../util/output/param';
@@ -36,12 +32,15 @@ import {
AliasDomainConfigured, AliasDomainConfigured,
MissingBuildScript, MissingBuildScript,
ConflictingFilePath, ConflictingFilePath,
ConflictingPathSegment ConflictingPathSegment,
BuildError,
NotDomainOwner,
} from '../../util/errors-ts'; } from '../../util/errors-ts';
import { SchemaValidationFailed } from '../../util/errors'; import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available'; import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
import handleCertError from '../../util/certs/handle-cert-error'; import handleCertError from '../../util/certs/handle-cert-error';
import isWildcardAlias from '../../util/alias/is-wildcard-alias'; import isWildcardAlias from '../../util/alias/is-wildcard-alias';
import shouldDeployDir from '../../util/deploy/should-deploy-dir';
const addProcessEnv = async (log, env) => { const addProcessEnv = async (log, env) => {
let val; let val;
@@ -72,11 +71,12 @@ const addProcessEnv = async (log, env) => {
}; };
const deploymentErrorMsg = `Your deployment failed. Please retry later. More: https://err.sh/now/deployment-error`; const deploymentErrorMsg = `Your deployment failed. Please retry later. More: https://err.sh/now/deployment-error`;
const prepareAlias = input => isWildcardAlias(input) ? input : `https://${input}`; const prepareAlias = input =>
isWildcardAlias(input) ? input : `https://${input}`;
const printDeploymentStatus = async ( const printDeploymentStatus = async (
output, output,
{ url, readyState, alias: aliasList, aliasError }, { readyState, alias: aliasList, aliasError },
deployStamp, deployStamp,
clipboardEnabled, clipboardEnabled,
localConfig, localConfig,
@@ -94,10 +94,18 @@ const printDeploymentStatus = async (
const preparedAlias = prepareAlias(firstAlias); const preparedAlias = prepareAlias(firstAlias);
try { try {
await copy(`https://${firstAlias}`); await copy(`https://${firstAlias}`);
output.ready(`Deployed to ${chalk.bold(chalk.cyan(preparedAlias))} ${chalk.gray('[in clipboard]')} ${deployStamp()}`); output.ready(
`Deployed to ${chalk.bold(
chalk.cyan(preparedAlias)
)} ${chalk.gray('[in clipboard]')} ${deployStamp()}`
);
} catch (err) { } catch (err) {
output.debug(`Error copying to clipboard: ${err}`); output.debug(`Error copying to clipboard: ${err}`);
output.ready(`Deployed to ${chalk.bold(chalk.cyan(preparedAlias))} ${deployStamp()}`); output.ready(
`Deployed to ${chalk.bold(
chalk.cyan(preparedAlias)
)} ${deployStamp()}`
);
} }
} }
} else { } else {
@@ -109,13 +117,17 @@ const printDeploymentStatus = async (
for (const alias of aliasList) { for (const alias of aliasList) {
const index = aliasList.indexOf(alias); const index = aliasList.indexOf(alias);
const isLast = index === (aliasList.length - 1); const isLast = index === aliasList.length - 1;
const shouldCopy = matching ? alias === matching : isLast; const shouldCopy = matching ? alias === matching : isLast;
if (shouldCopy && clipboardEnabled) { if (shouldCopy && clipboardEnabled) {
try { try {
await copy(`https://${alias}`); await copy(`https://${alias}`);
output.print(`- ${chalk.bold(chalk.cyan(prepareAlias(alias)))} ${chalk.gray('[in clipboard]')}\n`); output.print(
`- ${chalk.bold(chalk.cyan(prepareAlias(alias)))} ${chalk.gray(
'[in clipboard]'
)}\n`
);
continue; continue;
} catch (err) { } catch (err) {
@@ -138,20 +150,6 @@ const printDeploymentStatus = async (
return 1; return 1;
} }
const failedBuilds = builds.filter(isFailed);
const amount = failedBuilds.length;
if (amount > 0) {
output.error('Build failed');
output.error(
`Check your logs at https://${url}/_logs or run ${code(
`now logs ${url}`
)}`
);
return 1;
}
output.error(deploymentErrorMsg); output.error(deploymentErrorMsg);
return 1; return 1;
}; };
@@ -206,7 +204,15 @@ export default async function main(
return 1; return 1;
} }
const { apiUrl, authConfig: { token }, config: { currentTeam } } = ctx; if (!(await shouldDeployDir(argv._[0], output))) {
return 0;
}
const {
apiUrl,
authConfig: { token },
config: { currentTeam },
} = ctx;
const { log, debug, error, warn } = output; const { log, debug, error, warn } = output;
const paths = Object.keys(stats); const paths = Object.keys(stats);
const debugEnabled = argv['--debug']; const debugEnabled = argv['--debug'];
@@ -236,7 +242,6 @@ export default async function main(
parseMeta(argv['--meta']) parseMeta(argv['--meta'])
); );
let syncCount;
let deployStamp = stamp(); let deployStamp = stamp();
let deployment = null; let deployment = null;
@@ -289,11 +294,15 @@ export default async function main(
parseEnv(argv['--env']) parseEnv(argv['--env'])
); );
// Enable debug mode for builders
const buildDebugEnv = debugEnabled ? { NOW_BUILDER_DEBUG: '1' } : {};
// Merge build env out of `build.env` from now.json, and `--build-env` args // Merge build env out of `build.env` from now.json, and `--build-env` args
const deploymentBuildEnv = Object.assign( const deploymentBuildEnv = Object.assign(
{}, {},
parseEnv(localConfig.build && localConfig.build.env), parseEnv(localConfig.build && localConfig.build.env),
parseEnv(argv['--build-env']) parseEnv(argv['--build-env']),
buildDebugEnv
); );
// If there's any undefined values, then inherit them from this process // If there's any undefined values, then inherit them from this process
@@ -313,33 +322,45 @@ export default async function main(
try { try {
// $FlowFixMe // $FlowFixMe
const project = getProjectName({argv, nowConfig: localConfig, isFile, paths}); const project = getProjectName({
argv,
nowConfig: localConfig,
isFile,
paths,
});
log(`Using project ${chalk.bold(project)}`); log(`Using project ${chalk.bold(project)}`);
const createArgs = { const createArgs = {
name: project, name: project,
env: deploymentEnv, env: deploymentEnv,
build: { env: deploymentBuildEnv }, build: { env: deploymentBuildEnv },
forceNew: argv['--force'], forceNew: argv['--force'],
quiet, quiet,
wantsPublic: argv['--public'] || localConfig.public, wantsPublic: argv['--public'] || localConfig.public,
isFile, isFile,
type: null, type: null,
nowConfig: localConfig, nowConfig: localConfig,
regions, regions,
meta meta,
deployStamp,
}; };
if (argv['--target']) { if (argv['--target']) {
const deprecatedTarget = argv['--target']; const deprecatedTarget = argv['--target'];
if (!['staging', 'production'].includes(deprecatedTarget)) { if (!['staging', 'production'].includes(deprecatedTarget)) {
error(`The specified ${param('--target')} ${code(deprecatedTarget)} is not valid`); error(
`The specified ${param('--target')} ${code(
deprecatedTarget
)} is not valid`
);
return 1; return 1;
} }
if (deprecatedTarget === 'production') { if (deprecatedTarget === 'production') {
warn('We recommend using the much shorter `--prod` option instead of `--target production` (deprecated)'); warn(
'We recommend using the much shorter `--prod` option instead of `--target production` (deprecated)'
);
} }
output.debug(`Setting target to ${deprecatedTarget}`); output.debug(`Setting target to ${deprecatedTarget}`);
@@ -351,7 +372,7 @@ export default async function main(
deployStamp = stamp(); deployStamp = stamp();
const firstDeployCall = await createDeploy( deployment = await createDeploy(
output, output,
now, now,
contextName, contextName,
@@ -360,13 +381,49 @@ export default async function main(
ctx ctx
); );
if (deployment instanceof NotDomainOwner) {
output.error(deployment);
return 1;
}
const deploymentResponse = handleCertError(
output,
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v9')
);
if (deploymentResponse === 1) {
return deploymentResponse;
}
if ( if (
firstDeployCall instanceof DomainNotFound && deploymentResponse instanceof DeploymentNotFound ||
firstDeployCall.meta && firstDeployCall.meta.domain deploymentResponse instanceof DeploymentPermissionDenied ||
deploymentResponse instanceof InvalidDeploymentId
) { ) {
output.debug(`The domain ${ output.error(deploymentResponse.message);
firstDeployCall.meta.domain return 1;
} was not found, trying to purchase it`); }
if (handleCertError(output, deployment) === 1) {
return 1;
}
if (deployment === null) {
error('Uploading failed. Please try again.');
return 1;
}
} catch (err) {
debug(`Error: ${err}\n${err.stack}`);
if (err instanceof NotDomainOwner) {
output.error(err.message);
return 1;
}
if (err instanceof DomainNotFound && err.meta && err.meta.domain) {
output.debug(
`The domain ${err.meta.domain} was not found, trying to purchase it`
);
const purchase = await purchaseDomainIfAvailable( const purchase = await purchaseDomainIfAvailable(
output, output,
@@ -374,16 +431,14 @@ export default async function main(
apiUrl: ctx.apiUrl, apiUrl: ctx.apiUrl,
token: ctx.authConfig.token, token: ctx.authConfig.token,
currentTeam: ctx.config.currentTeam, currentTeam: ctx.config.currentTeam,
debug: debugEnabled debug: debugEnabled,
}), }),
firstDeployCall.meta.domain, err.meta.domain,
contextName contextName
); );
if (purchase === true) { if (purchase === true) {
output.success(`Successfully purchased the domain ${ output.success(`Successfully purchased the domain ${err.meta.domain}!`);
firstDeployCall.meta.domain
}!`);
// We exit if the purchase is completed since // We exit if the purchase is completed since
// the domain verification can take some time // the domain verification can take some time
@@ -391,7 +446,7 @@ export default async function main(
} }
if (purchase === false || purchase instanceof UserAborted) { if (purchase === false || purchase instanceof UserAborted) {
handleCreateDeployError(output, firstDeployCall); handleCreateDeployError(output, deployment);
return 1; return 1;
} }
@@ -399,120 +454,36 @@ export default async function main(
return 1; return 1;
} }
if (handleCertError(output, firstDeployCall) === 1) {
return 1;
}
if ( if (
firstDeployCall instanceof DomainNotFound || err instanceof DomainNotFound ||
firstDeployCall instanceof DomainNotVerified || err instanceof DomainNotVerified ||
firstDeployCall instanceof DomainPermissionDenied || err instanceof NotDomainOwner ||
firstDeployCall instanceof DomainVerificationFailed || err instanceof DomainPermissionDenied ||
firstDeployCall instanceof SchemaValidationFailed || err instanceof DomainVerificationFailed ||
firstDeployCall instanceof InvalidDomain || err instanceof SchemaValidationFailed ||
firstDeployCall instanceof DeploymentNotFound || err instanceof InvalidDomain ||
firstDeployCall instanceof BuildsRateLimited || err instanceof DeploymentNotFound ||
firstDeployCall instanceof DeploymentsRateLimited || err instanceof BuildsRateLimited ||
firstDeployCall instanceof AliasDomainConfigured || err instanceof DeploymentsRateLimited ||
firstDeployCall instanceof MissingBuildScript || err instanceof AliasDomainConfigured ||
firstDeployCall instanceof ConflictingFilePath || err instanceof MissingBuildScript ||
firstDeployCall instanceof ConflictingPathSegment err instanceof ConflictingFilePath ||
err instanceof ConflictingPathSegment
) { ) {
handleCreateDeployError(output, firstDeployCall); handleCreateDeployError(output, err);
return 1; return 1;
} }
deployment = firstDeployCall; if (err instanceof BuildError) {
output.error('Build failed');
output.error(
`Check your logs at ${now.url}/_logs or run ${code(
`now logs ${now.url}`
)}`
);
if (now.syncFileCount > 0) { return 1;
const uploadStamp = stamp();
await new Promise((resolve, reject) => {
if (now.syncFileCount !== now.fileCount) {
debug(`Total files ${now.fileCount}, ${now.syncFileCount} changed`);
}
const size = bytes(now.syncAmount);
syncCount = `${now.syncFileCount} file${now.syncFileCount > 1
? 's'
: ''}`;
const bar = new Progress(
`${chalk.gray(
'>'
)} Upload [:bar] :percent :etas (${size}) [${syncCount}]`,
{
width: 20,
complete: '=',
incomplete: '',
total: now.syncAmount,
clear: true
}
);
now.upload({ scale: {} });
now.on('upload', ({ names, data }) => {
debug(`Uploaded: ${names.join(' ')} (${bytes(data.length)})`);
});
now.on('uploadProgress', progress => {
bar.tick(progress);
});
now.on('complete', resolve);
now.on('error', err => {
error('Upload failed');
reject(err);
});
});
if (!quiet && syncCount) {
log(`Synced ${syncCount} (${bytes(now.syncAmount)}) ${uploadStamp()}`);
}
for (let i = 0; i < 4; i += 1) {
deployStamp = stamp();
const secondDeployCall = await createDeploy(
output,
now,
contextName,
paths,
createArgs
);
if (handleCertError(output, secondDeployCall) === 1) {
return 1;
}
if (
secondDeployCall instanceof DomainPermissionDenied ||
secondDeployCall instanceof DomainVerificationFailed ||
secondDeployCall instanceof SchemaValidationFailed ||
secondDeployCall instanceof DeploymentNotFound ||
secondDeployCall instanceof DeploymentsRateLimited ||
secondDeployCall instanceof AliasDomainConfigured ||
secondDeployCall instanceof MissingBuildScript ||
secondDeployCall instanceof ConflictingFilePath ||
secondDeployCall instanceof ConflictingPathSegment
) {
handleCreateDeployError(output, secondDeployCall);
return 1;
}
if (now.syncFileCount === 0) {
deployment = secondDeployCall;
break;
}
}
if (deployment === null) {
error('Uploading failed. Please try again.');
return 1;
}
} }
} catch (err) {
debug(`Error: ${err}\n${err.stack}`);
if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') { if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') {
const { additionalProperty = '' } = err.params || {}; const { additionalProperty = '' } = err.params || {};
@@ -531,114 +502,14 @@ export default async function main(
return 1; return 1;
} }
const { url } = now; return printDeploymentStatus(
output,
if (isTTY) { deployment,
log(`${url} ${chalk.gray(`[v2]`)} ${deployStamp()}`); deployStamp,
} else { !argv['--no-clipboard'],
process.stdout.write(url); localConfig
} );
}
// If an error occurred, we want to let it fall down to rendering
// builds so the user can see in which build the error occurred.
if (isReady(deployment)) {
return printDeploymentStatus(output, deployment, deployStamp, !argv['--no-clipboard'], localConfig);
}
const sleepingTime = ms('1.5s');
const allBuildsTime = stamp();
const times = {};
const buildsUrl = `/v1/now/deployments/${deployment.id}/builds`;
let builds = [];
let buildsCompleted = false;
let buildSpinner = null;
let deploymentSpinner = null;
// eslint-disable-next-line no-constant-condition
while (true) {
if (!buildsCompleted) {
const { builds: freshBuilds } = await now.fetch(buildsUrl);
// If there are no builds, we need to exit.
if (freshBuilds.length === 0 || freshBuilds.every(isDone)) {
builds = freshBuilds;
buildsCompleted = true;
} else {
for (const build of freshBuilds) {
const id = build.id;
const done = isDone(build);
if (times[id]) {
if (done && typeof times[id] === 'function') {
times[id] = times[id]();
}
} else {
times[id] = done ? allBuildsTime() : stamp();
}
}
if (JSON.stringify(builds) !== JSON.stringify(freshBuilds)) {
builds = freshBuilds;
if (buildSpinner === null) {
buildSpinner = wait('Building...');
}
buildsCompleted = builds.every(isDone);
if (builds.some(isFailed)) {
break;
}
}
}
} else {
const deploymentResponse = handleCertError(
output,
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v9')
)
if (deploymentResponse === 1) {
return deploymentResponse;
}
if (
deploymentResponse instanceof DeploymentNotFound ||
deploymentResponse instanceof DeploymentPermissionDenied ||
deploymentResponse instanceof InvalidDeploymentId
) {
output.error(deploymentResponse.message);
return 1;
}
if (isReady(deploymentResponse) || isFailed(deploymentResponse)) {
deployment = deploymentResponse;
if (typeof deploymentSpinner === 'function') {
// This stops it
deploymentSpinner();
}
break;
} else if (!deploymentSpinner) {
if (typeof buildSpinner === 'function') {
buildSpinner();
}
deploymentSpinner = wait('Finalizing...');
}
}
await sleep(sleepingTime);
}
if (typeof buildSpinner === 'function') {
buildSpinner();
}
return printDeploymentStatus(output, deployment, deployStamp, !argv['--no-clipboard'], localConfig, builds);
};
function handleCreateDeployError(output, error) { function handleCreateDeployError(output, error) {
if (error instanceof InvalidDomain) { if (error instanceof InvalidDomain) {
@@ -708,18 +579,20 @@ function handleCreateDeployError(output, error) {
} }
if (error instanceof TooManyRequests) { if (error instanceof TooManyRequests) {
output.error( output.error(
`Too many requests detected for ${error.meta `Too many requests detected for ${error.meta.api} API. Try again in ${ms(
.api} API. Try again in ${ms(error.meta.retryAfter * 1000, { error.meta.retryAfter * 1000,
long: true {
})}.` long: true,
}
)}.`
); );
return 1; return 1;
} }
if (error instanceof DomainNotVerified) { if (error instanceof DomainNotVerified) {
output.error( output.error(
`The domain used as an alias ${ `The domain used as an alias ${chalk.underline(
chalk.underline(error.meta.domain) error.meta.domain
} is not verified yet. Please verify it.` )} is not verified yet. Please verify it.`
); );
return 1; return 1;
} }
@@ -730,6 +603,7 @@ function handleCreateDeployError(output, error) {
} }
if ( if (
error instanceof DeploymentNotFound || error instanceof DeploymentNotFound ||
error instanceof NotDomainOwner ||
error instanceof DeploymentsRateLimited || error instanceof DeploymentsRateLimited ||
error instanceof AliasDomainConfigured || error instanceof AliasDomainConfigured ||
error instanceof MissingBuildScript || error instanceof MissingBuildScript ||

View File

@@ -2,7 +2,6 @@ import { resolve, basename, join } from 'path';
import { eraseLines } from 'ansi-escapes'; import { eraseLines } from 'ansi-escapes';
// @ts-ignore // @ts-ignore
import { write as copy } from 'clipboardy'; import { write as copy } from 'clipboardy';
import bytes from 'bytes';
import chalk from 'chalk'; import chalk from 'chalk';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import fs from 'fs-extra'; import fs from 'fs-extra';
@@ -13,7 +12,6 @@ import ms from 'ms';
// @ts-ignore // @ts-ignore
import title from 'title'; import title from 'title';
import plural from 'pluralize'; import plural from 'pluralize';
import Progress from 'progress';
// @ts-ignore // @ts-ignore
import { handleError } from '../../util/error'; import { handleError } from '../../util/error';
import chars from '../../util/output/chars'; import chars from '../../util/output/chars';
@@ -34,19 +32,16 @@ import promptOptions from '../../util/prompt-options';
// @ts-ignore // @ts-ignore
import readMetaData from '../../util/read-metadata'; import readMetaData from '../../util/read-metadata';
import toHumanPath from '../../util/humanize-path'; import toHumanPath from '../../util/humanize-path';
import combineAsyncGenerators from '../../util/combine-async-generators';
// @ts-ignore // @ts-ignore
import createDeploy from '../../util/deploy/create-deploy'; import createDeploy from '../../util/deploy/create-deploy';
import eventListenerToGenerator from '../../util/event-listener-to-generator'; import eventListenerToGenerator from '../../util/event-listener-to-generator';
// @ts-ignore // @ts-ignore
import formatLogCmd from '../../util/output/format-log-cmd';
// @ts-ignore
import formatLogOutput from '../../util/output/format-log-output'; import formatLogOutput from '../../util/output/format-log-output';
// @ts-ignore // @ts-ignore
import getEventsStream from '../../util/deploy/get-events-stream'; import getEventsStream from '../../util/deploy/get-events-stream';
import shouldDeployDir from '../../util/deploy/should-deploy-dir';
// @ts-ignore // @ts-ignore
import getInstanceIndex from '../../util/deploy/get-instance-index'; import getInstanceIndex from '../../util/deploy/get-instance-index';
import getStateChangeFromPolling from '../../util/deploy/get-state-change-from-polling';
import joinWords from '../../util/output/join-words'; import joinWords from '../../util/output/join-words';
// @ts-ignore // @ts-ignore
import normalizeRegionsList from '../../util/scale/normalize-regions-list'; import normalizeRegionsList from '../../util/scale/normalize-regions-list';
@@ -67,11 +62,12 @@ import {
DomainVerificationFailed, DomainVerificationFailed,
TooManyRequests, TooManyRequests,
VerifyScaleTimeout, VerifyScaleTimeout,
DeploymentsRateLimited DeploymentsRateLimited,
NotDomainOwner,
} from '../../util/errors-ts'; } from '../../util/errors-ts';
import { import {
InvalidAllForScale, InvalidAllForScale,
InvalidRegionOrDCForScale InvalidRegionOrDCForScale,
} from '../../util/errors'; } from '../../util/errors';
import { SchemaValidationFailed } from '../../util/errors'; import { SchemaValidationFailed } from '../../util/errors';
import handleCertError from '../../util/certs/handle-cert-error'; import handleCertError from '../../util/certs/handle-cert-error';
@@ -198,7 +194,7 @@ const promptForEnvFields = async (list: string[]) => {
for (const field of list) { for (const field of list) {
questions.push({ questions.push({
name: field, name: field,
message: field message: field,
}); });
} }
@@ -220,7 +216,7 @@ const promptForEnvFields = async (list: string[]) => {
async function canUseZeroConfig(cwd: string): Promise<boolean> { async function canUseZeroConfig(cwd: string): Promise<boolean> {
try { try {
const pkg = (await readPackage(join(cwd, 'package.json'))); const pkg = await readPackage(join(cwd, 'package.json'));
if (!pkg || pkg instanceof Error) { if (!pkg || pkg instanceof Error) {
return false; return false;
@@ -250,7 +246,7 @@ async function canUseZeroConfig(cwd: string): Promise<boolean> {
) { ) {
return true; return true;
} }
} catch(_) {} } catch (_) {}
return false; return false;
} }
@@ -275,6 +271,10 @@ export default async function main(
paths = [process.cwd()]; paths = [process.cwd()];
} }
if (!(await shouldDeployDir(argv._[0], output))) {
return 0;
}
// Options // Options
forceNew = argv.force; forceNew = argv.force;
deploymentName = argv.name; deploymentName = argv.name;
@@ -296,15 +296,17 @@ export default async function main(
quiet = !isTTY; quiet = !isTTY;
({ log, error, note, debug, warn } = output); ({ log, error, note, debug, warn } = output);
const infoUrl = await canUseZeroConfig(paths[0]) const infoUrl = (await canUseZeroConfig(paths[0]))
? 'https://zeit.co/guides/migrate-to-zeit-now' ? 'https://zeit.co/guides/migrate-to-zeit-now'
: 'https://zeit.co/docs/v2/advanced/platform/changes-in-now-2-0' : 'https://zeit.co/docs/v2/advanced/platform/changes-in-now-2-0';
warn(`You are using an old version of the Now Platform. More: ${link(infoUrl)}`); warn(
`You are using an old version of the Now Platform. More: ${link(infoUrl)}`
);
const { const {
authConfig: { token }, authConfig: { token },
config config,
} = ctx; } = ctx;
try { try {
@@ -314,7 +316,7 @@ export default async function main(
token, token,
config, config,
firstRun: true, firstRun: true,
deploymentType: undefined deploymentType: undefined,
}); });
} catch (err) { } catch (err) {
await stopDeployment(err); await stopDeployment(err);
@@ -327,7 +329,7 @@ async function sync({
token, token,
config: { currentTeam }, config: { currentTeam },
firstRun, firstRun,
deploymentType deploymentType,
}: SyncOptions): Promise<void> { }: SyncOptions): Promise<void> {
return new Promise(async (_resolve, reject) => { return new Promise(async (_resolve, reject) => {
let deployStamp = stamp(); let deployStamp = stamp();
@@ -476,7 +478,7 @@ async function sync({
// XXX: legacy // XXX: legacy
deploymentType, deploymentType,
sessionAffinity sessionAffinity,
}; };
} }
@@ -486,7 +488,7 @@ async function sync({
meta, meta,
deploymentName, deploymentName,
deploymentType, deploymentType,
sessionAffinity sessionAffinity,
} = await readMeta( } = await readMeta(
paths[0], paths[0],
deploymentName, deploymentName,
@@ -499,7 +501,7 @@ async function sync({
'dockerfile_missing', 'dockerfile_missing',
'no_dockerfile_commands', 'no_dockerfile_commands',
'unsupported_deployment_type', 'unsupported_deployment_type',
'multiple_manifests' 'multiple_manifests',
]; ];
if ( if (
@@ -537,7 +539,7 @@ async function sync({
// Read scale and fail if we have both regions and scale // Read scale and fail if we have both regions and scale
if (regions.length > 0 && Object.keys(scaleFromConfig).length > 0) { if (regions.length > 0 && Object.keys(scaleFromConfig).length > 0) {
error( error(
'Can\'t set both `regions` and `scale` options simultaneously', "Can't set both `regions` and `scale` options simultaneously",
'regions-and-scale-at-once' 'regions-and-scale-at-once'
); );
await exit(1); await exit(1);
@@ -548,9 +550,7 @@ async function sync({
dcIds = normalizeRegionsList(regions); dcIds = normalizeRegionsList(regions);
if (dcIds instanceof InvalidRegionOrDCForScale) { if (dcIds instanceof InvalidRegionOrDCForScale) {
error( error(
`The value "${ `The value "${dcIds.meta.regionOrDC}" is not a valid region or DC identifier`
dcIds.meta.regionOrDC
}" is not a valid region or DC identifier`
); );
await exit(1); await exit(1);
return 1; return 1;
@@ -565,7 +565,7 @@ async function sync({
scale = dcIds.reduce( scale = dcIds.reduce(
(result: DcScale, dcId: string) => ({ (result: DcScale, dcId: string) => ({
...result, ...result,
[dcId]: { min: 0, max: 1 } [dcId]: { min: 0, max: 1 },
}), }),
{} {}
); );
@@ -661,8 +661,9 @@ async function sync({
} }
const hasSecrets = Object.keys(deploymentEnv).some(key => const hasSecrets = Object.keys(deploymentEnv).some(key =>
deploymentEnv[key].startsWith('@') (deploymentEnv[key] || '').startsWith('@')
); );
const secretsPromise = hasSecrets ? now.listSecrets() : null; const secretsPromise = hasSecrets ? now.listSecrets() : null;
const findSecret = async (uidOrName: string) => { const findSecret = async (uidOrName: string) => {
@@ -754,15 +755,13 @@ async function sync({
parseMeta(argv.meta) parseMeta(argv.meta)
); );
let syncCount;
try { try {
meta.name = getProjectName({ meta.name = getProjectName({
argv, argv,
nowConfig, nowConfig,
isFile, isFile,
paths, paths,
pre: meta.name pre: meta.name,
}); });
log(`Using project ${chalk.bold(meta.name)}`); log(`Using project ${chalk.bold(meta.name)}`);
const createArgs = Object.assign( const createArgs = Object.assign(
@@ -776,13 +775,15 @@ async function sync({
scale, scale,
wantsPublic, wantsPublic,
sessionAffinity, sessionAffinity,
isFile isFile,
nowConfig,
deployStamp,
}, },
meta meta
); );
deployStamp = stamp(); deployStamp = stamp();
const firstDeployCall = await createDeploy( deployment = await createDeploy(
output, output,
now, now,
contextName, contextName,
@@ -790,118 +791,24 @@ async function sync({
createArgs createArgs
); );
const handledResult = handleCertError(output, firstDeployCall); const handledResult = handleCertError(output, deployment);
if (handledResult === 1) { if (handledResult === 1) {
return handledResult; return handledResult;
} }
if ( if (
firstDeployCall instanceof DomainNotFound || deployment instanceof DomainNotFound ||
firstDeployCall instanceof DomainPermissionDenied || deployment instanceof NotDomainOwner ||
firstDeployCall instanceof DomainVerificationFailed || deployment instanceof DomainPermissionDenied ||
firstDeployCall instanceof SchemaValidationFailed || deployment instanceof DomainVerificationFailed ||
firstDeployCall instanceof DeploymentNotFound || deployment instanceof SchemaValidationFailed ||
firstDeployCall instanceof DeploymentsRateLimited deployment instanceof DeploymentNotFound ||
deployment instanceof DeploymentsRateLimited
) { ) {
handleCreateDeployError(output, firstDeployCall); handleCreateDeployError(output, deployment);
await exit(1); await exit(1);
return; return;
} }
deployment = firstDeployCall;
if (now.syncFileCount > 0) {
const uploadStamp = stamp();
await new Promise(resolve => {
if (now.syncFileCount !== now.fileCount) {
debug(`Total files ${now.fileCount}, ${now.syncFileCount} changed`);
}
const size = bytes(now.syncAmount);
syncCount = `${now.syncFileCount} file${
now.syncFileCount > 1 ? 's' : ''
}`;
const bar = new Progress(
`${chalk.gray(
'>'
)} Upload [:bar] :percent :etas (${size}) [${syncCount}]`,
{
width: 20,
complete: '=',
incomplete: '',
total: now.syncAmount,
clear: true
}
);
now.upload({ scale });
now.on(
'upload',
({ names, data }: { names: string[]; data: Buffer }) => {
debug(`Uploaded: ${names.join(' ')} (${bytes(data.length)})`);
}
);
now.on('uploadProgress', (progress: number) => {
bar.tick(progress);
});
now.on('complete', resolve);
now.on('error', (err: Error) => {
error('Upload failed');
reject(err);
});
});
if (!quiet && syncCount) {
log(
`Synced ${syncCount} (${bytes(now.syncAmount)}) ${uploadStamp()}`
);
}
for (let i = 0; i < 4; i += 1) {
deployStamp = stamp();
const secondDeployCall = await createDeploy(
output,
now,
contextName,
paths,
createArgs
);
const handledResult = handleCertError(output, secondDeployCall);
if (handledResult === 1) {
return handledResult;
}
if (
secondDeployCall instanceof DomainNotFound ||
secondDeployCall instanceof DomainPermissionDenied ||
secondDeployCall instanceof DomainVerificationFailed ||
secondDeployCall instanceof SchemaValidationFailed ||
secondDeployCall instanceof TooManyRequests ||
secondDeployCall instanceof DeploymentNotFound ||
secondDeployCall instanceof DeploymentsRateLimited
) {
handleCreateDeployError(output, secondDeployCall);
await exit(1);
return;
}
if (now.syncFileCount === 0) {
deployment = secondDeployCall;
break;
}
}
if (deployment === null) {
error('Uploading failed. Please try again.');
await exit(1);
return;
}
}
} catch (err) { } catch (err) {
if (err.code === 'plan_requires_public') { if (err.code === 'plan_requires_public') {
if (!wantsPublic) { if (!wantsPublic) {
@@ -914,7 +821,7 @@ async function sync({
if (isTTY) { if (isTTY) {
proceed = await promptBool('Are you sure you want to proceed?', { proceed = await promptBool('Are you sure you want to proceed?', {
trailing: eraseLines(1) trailing: eraseLines(1),
}); });
} }
@@ -954,10 +861,10 @@ async function sync({
output, output,
token, token,
config: { config: {
currentTeam currentTeam,
}, },
firstRun: false, firstRun: false,
deploymentType deploymentType,
}); });
} }
@@ -1002,8 +909,6 @@ async function sync({
} else { } else {
log(`${chalk.bold(chalk.cyan(url))}${dcs} ${deployStamp()}`); log(`${chalk.bold(chalk.cyan(url))}${dcs} ${deployStamp()}`);
} }
} else {
process.stdout.write(url);
} }
if (deploymentType === 'static') { if (deploymentType === 'static') {
@@ -1022,96 +927,52 @@ async function sync({
// Show build logs // Show build logs
// (We have to add this check for flow but it will never happen) // (We have to add this check for flow but it will never happen)
if (deployment !== null) { if (deployment !== null) {
// If the created deployment is ready it was a deduping and we should exit const instanceIndex = getInstanceIndex();
if (deployment.readyState !== 'READY') { const eventsStream = await maybeGetEventsStream(now, deployment);
require('assert')(deployment); // mute linter
const instanceIndex = getInstanceIndex(); if (!noVerify) {
const eventsStream = await maybeGetEventsStream(now, deployment); output.log(
const eventsGenerator = getEventsGenerator( `Verifying instantiation in ${joinWords(
Object.keys(deployment.scale).map(dc => chalk.bold(dc))
)}`
);
const verifyStamp = stamp();
const verifyDCsGenerator = getVerifyDCsGenerator(
output,
now, now,
contextName,
deployment, deployment,
eventsStream eventsStream
); );
for await (const _event of eventsGenerator) { for await (const _dcOrEvent of verifyDCsGenerator) {
const event = _event as any; const dcOrEvent = _dcOrEvent as any;
// Stop when the deployment is ready if (dcOrEvent instanceof VerifyScaleTimeout) {
if ( output.error(
event.type === 'state-change' && `Instance verification timed out (${ms(dcOrEvent.meta.timeout)})`
event.payload.value === 'READY' );
) { output.log(
output.log(`Build completed`); 'Read more: https://err.sh/now-cli/verification-timeout'
break; );
} await exit(1);
} else if (Array.isArray(dcOrEvent)) {
// Stop then there is an error state const [dc, instances] = dcOrEvent;
if ( output.log(
event.type === 'state-change' && `${chalk.cyan(chars.tick)} Scaled ${plural(
event.payload.value === 'ERROR' 'instance',
) { instances,
output.error(`Build failed`); true
await exit(1); )} in ${chalk.bold(dc)} ${verifyStamp()}`
} );
} else if (
// For any relevant event we receive, print the result dcOrEvent &&
if (event.type === 'build-start') { (dcOrEvent.type === 'stdout' || dcOrEvent.type === 'stderr')
output.log('Building…'); ) {
} else if (event.type === 'command') { const prefix = chalk.gray(
output.log(formatLogCmd(event.payload.text)); `[${instanceIndex(dcOrEvent.payload.instanceId)}] `
} else if (event.type === 'stdout' || event.type === 'stderr') { );
formatLogOutput(event.payload.text).forEach((msg: string) => formatLogOutput(dcOrEvent.payload.text, prefix).forEach(
output.log(msg) (msg: string) => output.log(msg)
); );
}
}
if (!noVerify) {
output.log(
`Verifying instantiation in ${joinWords(
Object.keys(deployment.scale).map(dc => chalk.bold(dc))
)}`
);
const verifyStamp = stamp();
const verifyDCsGenerator = getVerifyDCsGenerator(
output,
now,
deployment,
eventsStream
);
for await (const _dcOrEvent of verifyDCsGenerator) {
const dcOrEvent = _dcOrEvent as any;
if (dcOrEvent instanceof VerifyScaleTimeout) {
output.error(
`Instance verification timed out (${ms(
dcOrEvent.meta.timeout
)})`
);
output.log(
'Read more: https://err.sh/now/verification-timeout'
);
await exit(1);
} else if (Array.isArray(dcOrEvent)) {
const [dc, instances] = dcOrEvent;
output.log(
`${chalk.cyan(chars.tick)} Scaled ${plural(
'instance',
instances,
true
)} in ${chalk.bold(dc)} ${verifyStamp()}`
);
} else if (
dcOrEvent &&
(dcOrEvent.type === 'stdout' || dcOrEvent.type === 'stderr')
) {
const prefix = chalk.gray(
`[${instanceIndex(dcOrEvent.payload.instanceId)}] `
);
formatLogOutput(dcOrEvent.payload.text, prefix).forEach(
(msg: string) => output.log(msg)
);
}
} }
} }
} }
@@ -1133,7 +994,7 @@ async function readMeta(
deploymentType, deploymentType,
deploymentName: _deploymentName, deploymentName: _deploymentName,
quiet: true, quiet: true,
sessionAffinity: _sessionAffinity sessionAffinity: _sessionAffinity,
}); });
if (!deploymentType) { if (!deploymentType) {
@@ -1150,7 +1011,7 @@ async function readMeta(
meta, meta,
deploymentName: _deploymentName, deploymentName: _deploymentName,
deploymentType, deploymentType,
sessionAffinity: _sessionAffinity sessionAffinity: _sessionAffinity,
}; };
} catch (err) { } catch (err) {
if (isTTY && err.code === 'multiple_manifests') { if (isTTY && err.code === 'multiple_manifests') {
@@ -1164,7 +1025,7 @@ async function readMeta(
try { try {
deploymentType = await promptOptions([ deploymentType = await promptOptions([
['npm', `${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `], ['npm', `${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `],
['docker', `${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `] ['docker', `${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `],
]); ]);
} catch (_) { } catch (_) {
throw err; throw err;
@@ -1190,35 +1051,13 @@ async function maybeGetEventsStream(now: Now, deployment: any) {
try { try {
return await getEventsStream(now, deployment.deploymentId, { return await getEventsStream(now, deployment.deploymentId, {
direction: 'forward', direction: 'forward',
follow: true follow: true,
}); });
} catch (error) { } catch (error) {
return null; return null;
} }
} }
function getEventsGenerator(
now: Now,
contextName: string,
deployment: any,
eventsStream: any
) {
const stateChangeFromPollingGenerator = getStateChangeFromPolling(
now,
contextName,
deployment.deploymentId,
deployment.readyState
);
if (eventsStream !== null) {
return combineAsyncGenerators(
eventListenerToGenerator('data', eventsStream),
stateChangeFromPollingGenerator
);
}
return stateChangeFromPollingGenerator;
}
function getVerifyDCsGenerator( function getVerifyDCsGenerator(
output: Output, output: Output,
now: Now, now: Now,
@@ -1228,7 +1067,7 @@ function getVerifyDCsGenerator(
const verifyDeployment = verifyDeploymentScale( const verifyDeployment = verifyDeploymentScale(
output, output,
now, now,
deployment.deploymentId, deployment.deploymentId || deployment.uid,
deployment.scale deployment.scale
); );
@@ -1295,9 +1134,9 @@ function handleCreateDeployError(output: Output, error: Error) {
output.error( output.error(
`Failed to validate ${highlight( `Failed to validate ${highlight(
'now.json' 'now.json'
)}: ${message}\nDocumentation: ${ )}: ${message}\nDocumentation: ${link(
link('https://zeit.co/docs/v2/advanced/configuration') 'https://zeit.co/docs/v2/advanced/configuration'
}` )}`
); );
return 1; return 1;
@@ -1307,7 +1146,7 @@ function handleCreateDeployError(output: Output, error: Error) {
`Too many requests detected for ${error.meta.api} API. Try again in ${ms( `Too many requests detected for ${error.meta.api} API. Try again in ${ms(
error.meta.retryAfter * 1000, error.meta.retryAfter * 1000,
{ {
long: true long: true,
} }
)}.` )}.`
); );

View File

@@ -1,5 +1,6 @@
import path from 'path'; import path from 'path';
import chalk from 'chalk'; import chalk from 'chalk';
import { PackageJson } from '@now/build-utils';
import getArgs from '../../util/get-args'; import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand'; import getSubcommand from '../../util/get-subcommand';
@@ -11,11 +12,10 @@ import logo from '../../util/output/logo';
import cmd from '../../util/output/cmd'; import cmd from '../../util/output/cmd';
import dev from './dev'; import dev from './dev';
import readPackage from '../../util/read-package'; import readPackage from '../../util/read-package';
import { Package } from '../../util/dev/types';
import readConfig from '../../util/config/read-config'; import readConfig from '../../util/config/read-config';
const COMMAND_CONFIG = { const COMMAND_CONFIG = {
dev: ['dev'] dev: ['dev'],
}; };
const help = () => { const help = () => {
@@ -54,7 +54,7 @@ export default async function main(ctx: NowContext) {
// Deprecated // Deprecated
'--port': Number, '--port': Number,
'-p': '--port' '-p': '--port',
}); });
const debug = argv['--debug']; const debug = argv['--debug'];
args = getSubcommand(argv._.slice(1), COMMAND_CONFIG).args; args = getSubcommand(argv._.slice(1), COMMAND_CONFIG).args;
@@ -90,7 +90,7 @@ export default async function main(ctx: NowContext) {
const pkg = await readPackage(path.join(dir, 'package.json')); const pkg = await readPackage(path.join(dir, 'package.json'));
if (pkg) { if (pkg) {
const { scripts } = pkg as Package; const { scripts } = pkg as PackageJson;
if (scripts && scripts.dev && /\bnow\b\W+\bdev\b/.test(scripts.dev)) { if (scripts && scripts.dev && /\bnow\b\W+\bdev\b/.test(scripts.dev)) {
output.error( output.error(
@@ -98,9 +98,7 @@ export default async function main(ctx: NowContext) {
'package.json' 'package.json'
)} must not contain ${cmd('now dev')}` )} must not contain ${cmd('now dev')}`
); );
output.error( output.error(`More details: http://err.sh/now/now-dev-as-dev-script`);
`More details: http://err.sh/now/now-dev-as-dev-script`
);
return 1; return 1;
} }
} }

View File

@@ -74,7 +74,7 @@ export default async function main(ctx) {
'--all': Boolean, '--all': Boolean,
'--meta': [String], '--meta': [String],
'-a': '--all', '-a': '--all',
'-m': '--meta' '-m': '--meta',
}); });
} catch (err) { } catch (err) {
handleError(err); handleError(err);
@@ -84,7 +84,7 @@ export default async function main(ctx) {
const debugEnabled = argv['--debug']; const debugEnabled = argv['--debug'];
const { print, log, error, note, debug } = createOutput({ const { print, log, error, note, debug } = createOutput({
debug: debugEnabled debug: debugEnabled,
}); });
if (argv._.length > 2) { if (argv._.length > 2) {
@@ -103,13 +103,16 @@ export default async function main(ctx) {
} }
const meta = parseMeta(argv['--meta']); const meta = parseMeta(argv['--meta']);
const { authConfig: { token }, config } = ctx; const {
authConfig: { token },
config,
} = ctx;
const { currentTeam, includeScheme } = config; const { currentTeam, includeScheme } = config;
const client = new Client({ const client = new Client({
apiUrl, apiUrl,
token, token,
currentTeam, currentTeam,
debug: debugEnabled debug: debugEnabled,
}); });
let contextName = null; let contextName = null;
@@ -202,7 +205,16 @@ export default async function main(ctx) {
const item = aliases.find(e => e.uid === app || e.alias === app); const item = aliases.find(e => e.uid === app || e.alias === app);
if (item) { if (item) {
debug('Found alias that matches app name'); debug(`Found alias that matches app name: ${item.alias}`);
if (Array.isArray(item.rules)) {
now.close();
stopSpinner();
log(`Found matching path alias: ${chalk.cyan(item.alias)}`);
log(`Please run ${cmd(`now alias ls ${item.alias}`)} instead`);
return 0;
}
const match = await now.findDeployment(item.deploymentId); const match = await now.findDeployment(item.deploymentId);
const instances = await getDeploymentInstances( const instances = await getDeploymentInstances(
now, now,
@@ -250,7 +262,9 @@ export default async function main(ctx) {
// information to help the user find other deployments or instances // information to help the user find other deployments or instances
if (app == null) { if (app == null) {
log(`To list more deployments for a project run ${cmd('now ls [project]')}`); log(
`To list more deployments for a project run ${cmd('now ls [project]')}`
);
} else if (!argv['--all']) { } else if (!argv['--all']) {
log(`To list deployment instances run ${cmd('now ls --all [project]')}`); log(`To list deployment instances run ${cmd('now ls --all [project]')}`);
} }
@@ -260,7 +274,9 @@ export default async function main(ctx) {
console.log( console.log(
`${table( `${table(
[ [
['project', 'latest deployment', 'inst #', 'type', 'state', 'age'].map(s => chalk.dim(s)), ['project', 'latest deployment', 'inst #', 'type', 'state', 'age'].map(
s => chalk.dim(s)
),
...deployments ...deployments
.sort(sortRecent()) .sort(sortRecent())
.map(dep => [ .map(dep => [
@@ -272,7 +288,7 @@ export default async function main(ctx) {
: dep.instanceCount, : dep.instanceCount,
dep.type === 'LAMBDAS' ? chalk.gray('-') : dep.type, dep.type === 'LAMBDAS' ? chalk.gray('-') : dep.type,
stateString(dep.state), stateString(dep.state),
chalk.gray(ms(Date.now() - new Date(dep.created))) chalk.gray(ms(Date.now() - new Date(dep.created))),
], ],
...(argv['--all'] ...(argv['--all']
? dep.instances.map(i => [ ? dep.instances.map(i => [
@@ -280,9 +296,9 @@ export default async function main(ctx) {
` ${chalk.gray('-')} ${i.url} `, ` ${chalk.gray('-')} ${i.url} `,
'', '',
'', '',
'' '',
]) ])
: []) : []),
]) ])
// flatten since the previous step returns a nested // flatten since the previous step returns a nested
// array of the deployment and (optionally) its instances // array of the deployment and (optionally) its instances
@@ -293,12 +309,12 @@ export default async function main(ctx) {
// we only want to render one deployment per app // we only want to render one deployment per app
filterUniqueApps() filterUniqueApps()
: () => true : () => true
) ),
], ],
{ {
align: ['l', 'l', 'r', 'l', 'b'], align: ['l', 'l', 'r', 'l', 'b'],
hsep: ' '.repeat(4), hsep: ' '.repeat(4),
stringLength: strlen stringLength: strlen,
} }
).replace(/^/gm, ' ')}\n\n` ).replace(/^/gm, ' ')}\n\n`
); );
@@ -310,7 +326,7 @@ function getProjectName(d) {
return 'files'; return 'files';
} }
return d.name return d.name;
} }
// renders the state string // renders the state string

View File

@@ -84,8 +84,8 @@ export default async function main(ctx) {
debug: 'd', debug: 'd',
query: 'q', query: 'q',
follow: 'f', follow: 'f',
output: 'o' output: 'o',
} },
}); });
argv._ = argv._.slice(1); argv._ = argv._.slice(1);
@@ -138,7 +138,7 @@ export default async function main(ctx) {
const { const {
authConfig: { token }, authConfig: { token },
config config,
} = ctx; } = ctx;
const { currentTeam } = config; const { currentTeam } = config;
const now = new Now({ apiUrl, token, debug, currentTeam }); const now = new Now({ apiUrl, token, debug, currentTeam });
@@ -146,7 +146,7 @@ export default async function main(ctx) {
apiUrl, apiUrl,
token, token,
currentTeam, currentTeam,
debug: debugEnabled debug: debugEnabled,
}); });
let contextName = null; let contextName = null;
@@ -209,7 +209,7 @@ export default async function main(ctx) {
types, types,
instanceId, instanceId,
since, since,
until until,
}; // no follow }; // no follow
const storage = []; const storage = [];
const storeEvent = event => storage.push(event); const storeEvent = event => storage.push(event);
@@ -219,7 +219,7 @@ export default async function main(ctx) {
onEvent: storeEvent, onEvent: storeEvent,
quiet: false, quiet: false,
debug, debug,
findOpts: findOpts1 findOpts: findOpts1,
}); });
const printedEventIds = new Set(); const printedEventIds = new Set();
@@ -241,14 +241,14 @@ export default async function main(ctx) {
types, types,
instanceId, instanceId,
since: since2, since: since2,
follow: true follow: true,
}; };
await printEvents(now, deployment.uid || deployment.id, currentTeam, { await printEvents(now, deployment.uid || deployment.id, currentTeam, {
mode: 'logs', mode: 'logs',
onEvent: printEvent, onEvent: printEvent,
quiet: false, quiet: false,
debug, debug,
findOpts: findOpts2 findOpts: findOpts2,
}); });
} }
@@ -283,27 +283,40 @@ function printLogShort(log) {
` ${obj.status} ${obj.bodyBytesSent}`; ` ${obj.status} ${obj.bodyBytesSent}`;
} else if (log.type === 'event') { } else if (log.type === 'event') {
data = `EVENT ${log.event} ${JSON.stringify(log.payload)}`; data = `EVENT ${log.event} ${JSON.stringify(log.payload)}`;
} else if (obj) {
data = JSON.stringify(obj, null, 2);
} else { } else {
data = obj data = (log.text || '')
? JSON.stringify(obj, null, 2)
: (log.text || '')
.replace(/\n$/, '') .replace(/\n$/, '')
.replace(/^\n/, '') .replace(/^\n/, '')
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
.replace(/\x1b\[1000D/g, '') .replace(/\x1b\[1000D/g, '')
.replace(/\x1b\[0K/g, '') .replace(/\x1b\[0K/g, '')
.replace(/\x1b\[1A/g, ''); .replace(/\x1b\[1A/g, '');
if (/warning/i.test(data)) {
data = chalk.yellow(data);
} else if (log.type === 'stderr') {
data = chalk.red(data);
}
} }
const date = new Date(log.created).toISOString(); const date = new Date(log.created).toISOString();
data.split('\n').forEach((line, i) => { data.split('\n').forEach((line, i) => {
if (line.includes('START RequestId:') || line.includes('END RequestId:')) { if (
line.includes('START RequestId:') ||
line.includes('END RequestId:') ||
line.includes('XRAY TraceId:')
) {
return; return;
} }
if (line.includes('REPORT RequestId:')) { if (line.includes('REPORT RequestId:')) {
line = line.substring(line.indexOf('Duration:'), line.length); line = line.substring(line.indexOf('Duration:'), line.length);
if (line.includes('Init Duration:')) {
line = line.substring(0, line.indexOf('Init Duration:'));
}
} }
if (i === 0) { if (i === 0) {
@@ -345,7 +358,7 @@ function printLogRaw(log) {
const logPrinters = { const logPrinters = {
short: printLogShort, short: printLogShort,
raw: printLogRaw raw: printLogRaw,
}; };
function toTimestamp(datestr) { function toTimestamp(datestr) {

View File

@@ -10,7 +10,7 @@ import Client from '../util/client.ts';
import logo from '../util/output/logo'; import logo from '../util/output/logo';
import getScope from '../util/get-scope'; import getScope from '../util/get-scope';
const e = encodeURIComponent const e = encodeURIComponent;
const help = () => { const help = () => {
console.log(` console.log(`
@@ -48,8 +48,8 @@ const main = async ctx => {
argv = mri(ctx.argv.slice(2), { argv = mri(ctx.argv.slice(2), {
boolean: ['help'], boolean: ['help'],
alias: { alias: {
help: 'h' help: 'h',
} },
}); });
argv._ = argv._.slice(1); argv._ = argv._.slice(1);
@@ -63,7 +63,10 @@ const main = async ctx => {
await exit(0); await exit(0);
} }
const { authConfig: { token }, config: { currentTeam }} = ctx; const {
authConfig: { token },
config: { currentTeam },
} = ctx;
const client = new Client({ apiUrl, token, currentTeam, debug }); const client = new Client({ apiUrl, token, currentTeam, debug });
const { contextName } = await getScope(client); const { contextName } = await getScope(client);
@@ -93,17 +96,21 @@ async function run({ client, contextName }) {
if (args.length !== 0) { if (args.length !== 0) {
console.error( console.error(
error( error(
`Invalid number of arguments. Usage: ${chalk.cyan('`now projects ls`')}` `Invalid number of arguments. Usage: ${chalk.cyan(
'`now projects ls`'
)}`
) )
); );
return exit(1); return exit(1);
} }
const list = await client.fetch('/projects/list', {method: 'GET'}); const list = await client.fetch('/v2/projects/', { method: 'GET' });
const elapsed = ms(new Date() - start); const elapsed = ms(new Date() - start);
console.log( console.log(
`> ${plural('project', list.length, true)} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}` `> ${plural('project', list.length, true)} found under ${chalk.bold(
contextName
)} ${chalk.gray(`[${elapsed}]`)}`
); );
if (list.length > 0) { if (list.length > 0) {
@@ -114,19 +121,19 @@ async function run({ client, contextName }) {
header.concat( header.concat(
list.map(secret => [ list.map(secret => [
'', '',
chalk.bold(secret.name), chalk.bold(secret.name),
chalk.gray(`${ms(cur - new Date(secret.updatedAt)) } ago`) chalk.gray(`${ms(cur - new Date(secret.updatedAt))} ago`),
]) ])
), ),
{ {
align: ['l', 'l', 'l'], align: ['l', 'l', 'l'],
hsep: ' '.repeat(2), hsep: ' '.repeat(2),
stringLength: strlen stringLength: strlen,
} }
); );
if (out) { if (out) {
console.log(`\n${ out }\n`); console.log(`\n${out}\n`);
} }
} }
return; return;
@@ -148,11 +155,11 @@ async function run({ client, contextName }) {
// Check the existence of the project // Check the existence of the project
try { try {
await client.fetch(`/projects/info/${e(name)}`) await client.fetch(`/projects/info/${e(name)}`);
} catch(err) { } catch (err) {
if (err.status === 404) { if (err.status === 404) {
console.error(error('No such project exists')) console.error(error('No such project exists'));
return exit(1) return exit(1);
} }
} }
@@ -162,7 +169,9 @@ async function run({ client, contextName }) {
return exit(0); return exit(0);
} }
await client.fetch('/projects/remove', {method: 'DELETE', body: {name}}); await client.fetch(`/v2/projects/${name}`, {
method: 'DELETE',
});
const elapsed = ms(new Date() - start); const elapsed = ms(new Date() - start);
console.log( console.log(
`${chalk.cyan('> Success!')} Project ${chalk.bold( `${chalk.cyan('> Success!')} Project ${chalk.bold(
@@ -193,7 +202,10 @@ async function run({ client, contextName }) {
} }
const [name] = args; const [name] = args;
await client.fetch('/projects/ensure-project', {method: 'POST', body: {name}}); await client.fetch('/projects/ensure-project', {
method: 'POST',
body: { name },
});
const elapsed = ms(new Date() - start); const elapsed = ms(new Date() - start);
console.log( console.log(
@@ -204,9 +216,7 @@ async function run({ client, contextName }) {
return; return;
} }
console.error( console.error(error('Please specify a valid subcommand: ls | add | rm'));
error('Please specify a valid subcommand: ls | add | rm')
);
help(); help();
exit(1); exit(1);
} }
@@ -220,7 +230,7 @@ function readConfirmation(projectName) {
return new Promise(resolve => { return new Promise(resolve => {
process.stdout.write( process.stdout.write(
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` + `The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
`It will also delete everything under the project including deployments.\n` `It will also delete everything under the project including deployments.\n`
); );
process.stdout.write( process.stdout.write(

View File

@@ -70,11 +70,12 @@ let subcommand;
const main = async ctx => { const main = async ctx => {
argv = mri(ctx.argv.slice(2), { argv = mri(ctx.argv.slice(2), {
boolean: ['help', 'debug'], boolean: ['help', 'debug', 'yes'],
alias: { alias: {
help: 'h', help: 'h',
debug: 'd' debug: 'd',
} yes: 'y',
},
}); });
argv._ = argv._.slice(1); argv._ = argv._.slice(1);
@@ -88,7 +89,10 @@ const main = async ctx => {
await exit(0); await exit(0);
} }
const { authConfig: { token }, config: { currentTeam } } = ctx; const {
authConfig: { token },
config: { currentTeam },
} = ctx;
const output = createOutput({ debug }); const output = createOutput({ debug });
const client = new Client({ apiUrl, token, currentTeam, debug }); const client = new Client({ apiUrl, token, currentTeam, debug });
let contextName = null; let contextName = null;
@@ -105,7 +109,7 @@ const main = async ctx => {
} }
try { try {
await run({ token, contextName, currentTeam }); await run({ output, token, contextName, currentTeam });
} catch (err) { } catch (err) {
handleError(err); handleError(err);
exit(1); exit(1);
@@ -121,7 +125,7 @@ export default async ctx => {
} }
}; };
async function run({ token, contextName, currentTeam }) { async function run({ output, token, contextName, currentTeam }) {
const secrets = new NowSecrets({ apiUrl, token, debug, currentTeam }); const secrets = new NowSecrets({ apiUrl, token, debug, currentTeam });
const args = argv._.slice(1); const args = argv._.slice(1);
const start = Date.now(); const start = Date.now();
@@ -153,13 +157,13 @@ async function run({ token, contextName, currentTeam }) {
list.map(secret => [ list.map(secret => [
'', '',
chalk.bold(secret.name), chalk.bold(secret.name),
chalk.gray(`${ms(cur - new Date(secret.created))} ago`) chalk.gray(`${ms(cur - new Date(secret.created))} ago`),
]) ])
), ),
{ {
align: ['l', 'l', 'l'], align: ['l', 'l', 'l'],
hsep: ' '.repeat(2), hsep: ' '.repeat(2),
stringLength: strlen stringLength: strlen,
} }
); );
@@ -185,7 +189,7 @@ async function run({ token, contextName, currentTeam }) {
const theSecret = list.find(secret => secret.name === args[0]); const theSecret = list.find(secret => secret.name === args[0]);
if (theSecret) { if (theSecret) {
const yes = await readConfirmation(theSecret); const yes = argv.yes || (await readConfirmation(theSecret));
if (!yes) { if (!yes) {
console.error(error('User abort')); console.error(error('User abort'));
return exit(0); return exit(0);
@@ -250,6 +254,10 @@ async function run({ token, contextName, currentTeam }) {
await secrets.add(name, value); await secrets.add(name, value);
const elapsed = ms(new Date() - start); const elapsed = ms(new Date() - start);
if (name !== name.toLowerCase()) {
output.warn(`Your secret name was converted to lower-case`);
}
console.log( console.log(
`${chalk.cyan('> Success!')} Secret ${chalk.bold( `${chalk.cyan('> Success!')} Secret ${chalk.bold(
name.toLowerCase() name.toLowerCase()
@@ -275,7 +283,7 @@ function readConfirmation(secret) {
const time = chalk.gray(`${ms(new Date() - new Date(secret.created))} ago`); const time = chalk.gray(`${ms(new Date() - new Date(secret.created))} ago`);
const tbl = table([[chalk.bold(secret.name), time]], { const tbl = table([[chalk.bold(secret.name), time]], {
align: ['r', 'l'], align: ['r', 'l'],
hsep: ' '.repeat(6) hsep: ' '.repeat(6),
}); });
process.stdout.write( process.stdout.write(

View File

@@ -17,7 +17,7 @@ import info from './util/output/info';
import getNowDir from './util/config/global-path'; import getNowDir from './util/config/global-path';
import { import {
getDefaultConfig, getDefaultConfig,
getDefaultAuthConfig getDefaultAuthConfig,
} from './util/config/get-default'; } from './util/config/get-default';
import hp from './util/humanize-path'; import hp from './util/humanize-path';
import commands from './commands/index.ts'; import commands from './commands/index.ts';
@@ -53,7 +53,7 @@ sourceMap.install();
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
release: `now-cli@${pkg.version}`, release: `now-cli@${pkg.version}`,
environment: pkg.version.includes('canary') ? 'canary' : 'stable' environment: pkg.version.includes('canary') ? 'canary' : 'stable',
}); });
let debug = () => {}; let debug = () => {};
@@ -71,7 +71,7 @@ const main = async argv_ => {
'--version': Boolean, '--version': Boolean,
'-v': '--version', '-v': '--version',
'--debug': Boolean, '--debug': Boolean,
'-d': '--debug' '-d': '--debug',
}, },
{ permissive: true } { permissive: true }
); );
@@ -102,7 +102,10 @@ const main = async argv_ => {
return 1; return 1;
} }
if (localConfig instanceof NowError && !(localConfig instanceof ERRORS.CantFindConfig)) { if (
localConfig instanceof NowError &&
!(localConfig instanceof ERRORS.CantFindConfig)
) {
output.error(`Failed to load local config file: ${localConfig.message}`); output.error(`Failed to load local config file: ${localConfig.message}`);
return 1; return 1;
} }
@@ -118,7 +121,7 @@ const main = async argv_ => {
if (targetOrSubcommand !== 'update') { if (targetOrSubcommand !== 'update') {
update = await checkForUpdate(pkg, { update = await checkForUpdate(pkg, {
interval: ms('1d'), interval: ms('1d'),
distTag: pkg.version.includes('canary') ? 'canary' : 'latest' distTag: pkg.version.includes('canary') ? 'canary' : 'latest',
}); });
} }
} catch (err) { } catch (err) {
@@ -135,7 +138,15 @@ const main = async argv_ => {
console.log( console.log(
info( info(
`${chalk.bgRed('UPDATE AVAILABLE')} ` + `${chalk.bgRed('UPDATE AVAILABLE')} ` +
`Run ${cmd(await getUpdateCommand())} to install Now CLI ${update.latest}` `Run ${cmd(await getUpdateCommand())} to install Now CLI ${
update.latest
}`
)
);
console.log(
info(
`Changelog: https://github.com/zeit/now/releases/tag/now@${update.latest}`
) )
); );
} }
@@ -307,9 +318,9 @@ const main = async argv_ => {
console.error( console.error(
error( error(
`${'An unexpected error occurred while trying to write the ' + `${'An unexpected error occurred while trying to write the ' +
`default now config to "${hp( `default now config to "${hp(NOW_AUTH_CONFIG_PATH)}" `}${
NOW_AUTH_CONFIG_PATH err.message
)}" `}${err.message}` }`
) )
); );
return 1; return 1;
@@ -329,7 +340,7 @@ const main = async argv_ => {
config, config,
authConfig, authConfig,
localConfig, localConfig,
argv: argv_ argv: argv_,
}; };
let subcommand; let subcommand;
@@ -339,7 +350,8 @@ const main = async argv_ => {
const targetPath = join(process.cwd(), targetOrSubcommand); const targetPath = join(process.cwd(), targetOrSubcommand);
const targetPathExists = existsSync(targetPath); const targetPathExists = existsSync(targetPath);
const subcommandExists = const subcommandExists =
GLOBAL_COMMANDS.has(targetOrSubcommand) || commands.has(targetOrSubcommand); GLOBAL_COMMANDS.has(targetOrSubcommand) ||
commands.has(targetOrSubcommand);
if (targetPathExists && subcommandExists) { if (targetPathExists && subcommandExists) {
console.error( console.error(
@@ -412,7 +424,7 @@ const main = async argv_ => {
message: message:
'No existing credentials found. Please run ' + 'No existing credentials found. Please run ' +
`${param('now login')} or pass ${param('--token')}`, `${param('now login')} or pass ${param('--token')}`,
slug: 'no-credentials-found' slug: 'no-credentials-found',
}) })
); );
@@ -426,7 +438,7 @@ const main = async argv_ => {
message: `This command doesn't work with ${param( message: `This command doesn't work with ${param(
'--token' '--token'
)}. Please use ${param('--scope')}.`, )}. Please use ${param('--scope')}.`,
slug: 'no-token-allowed' slug: 'no-token-allowed',
}) })
); );
@@ -440,7 +452,7 @@ const main = async argv_ => {
console.error( console.error(
error({ error({
message: `You defined ${param('--token')}, but it's missing a value`, message: `You defined ${param('--token')}, but it's missing a value`,
slug: 'missing-token-value' slug: 'missing-token-value',
}) })
); );
@@ -459,11 +471,22 @@ const main = async argv_ => {
const targetCommand = commands.get(subcommand); const targetCommand = commands.get(subcommand);
if (argv['--team']) { if (argv['--team']) {
output.warn(`The ${param('--team')} flag is deprecated. Please use ${param('--scope')} instead.`); output.warn(
`The ${param('--team')} flag is deprecated. Please use ${param(
'--scope'
)} instead.`
);
} }
if (typeof scope === 'string' && targetCommand !== 'login' && targetCommand !== 'dev' && !(targetCommand === 'teams' && argv._[3] !== 'invite')) { if (
const { authConfig: { token } } = ctx; typeof scope === 'string' &&
targetCommand !== 'login' &&
targetCommand !== 'dev' &&
!(targetCommand === 'teams' && argv._[3] !== 'invite')
) {
const {
authConfig: { token },
} = ctx;
const client = new Client({ apiUrl, token }); const client = new Client({ apiUrl, token });
let user = null; let user = null;
@@ -475,7 +498,7 @@ const main = async argv_ => {
console.error( console.error(
error({ error({
message: `You do not have access to the specified account`, message: `You do not have access to the specified account`,
slug: 'scope-not-accessible' slug: 'scope-not-accessible',
}) })
); );
@@ -499,7 +522,7 @@ const main = async argv_ => {
console.error( console.error(
error({ error({
message: `You do not have access to the specified team`, message: `You do not have access to the specified team`,
slug: 'scope-not-accessible' slug: 'scope-not-accessible',
}) })
); );
@@ -517,7 +540,7 @@ const main = async argv_ => {
console.error( console.error(
error({ error({
message: 'The specified scope does not exist', message: 'The specified scope does not exist',
slug: 'scope-not-existent' slug: 'scope-not-existent',
}) })
); );
@@ -577,7 +600,8 @@ const main = async argv_ => {
if (shouldCollectMetrics) { if (shouldCollectMetrics) {
metric metric
.event(eventCategory, '1', pkg.version) .event(eventCategory, '1', pkg.version)
.exception(err.message).send(); .exception(err.message)
.send();
} }
return 1; return 1;
@@ -586,7 +610,8 @@ const main = async argv_ => {
if (shouldCollectMetrics) { if (shouldCollectMetrics) {
metric metric
.event(eventCategory, '1', pkg.version) .event(eventCategory, '1', pkg.version)
.exception(err.message).send(); .exception(err.message)
.send();
} }
// Otherwise it is an unexpected error and we should show the trace // Otherwise it is an unexpected error and we should show the trace
@@ -647,9 +672,7 @@ process.on('uncaughtException', handleUnexpected);
// subcommands waiting for further data won't work (like `logs` and `logout`)! // subcommands waiting for further data won't work (like `logs` and `logout`)!
main(process.argv) main(process.argv)
.then(exitCode => { .then(exitCode => {
process.exitCode = exitCode;
process.emit('nowExit'); process.emit('nowExit');
process.on('beforeExit', () => {
process.exit(exitCode);
});
}) })
.catch(handleUnexpected); .catch(handleUnexpected);

View File

@@ -1,142 +0,0 @@
import { JsonBody, StreamBody, context } from 'fetch-h2';
// Packages
import { parse } from 'url';
import Sema from 'async-sema';
import createOutput, { Output } from './output/create-output';
const MAX_REQUESTS_PER_CONNECTION = 1000;
type CurrentContext = ReturnType<typeof context> & {
fetchesMade: number;
ongoingFetches: number;
};
export interface AgentFetchOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
body?: NodeJS.ReadableStream | string;
headers: { [key: string]: string };
}
/**
* 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
*/
export default class NowAgent {
_contexts: ReturnType<typeof context>[];
_currContext: CurrentContext;
_output: Output;
_protocol?: string;
_sema: Sema;
_url: string;
constructor(url: string, { debug = false } = {}) {
// We use multiple contexts because each context represent one connection
// With nginx, we're limited to 1000 requests before a connection is closed
// http://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_max_requests
// To get arround this, we keep track of requests made on a connection. when we're about to hit 1000
// we start up a new connection, and re-route all future traffic through the new connection
// and when the final request from the old connection resolves, we auto-close the old connection
this._contexts = [context()];
this._currContext = {
...this._contexts[0],
fetchesMade: 0,
ongoingFetches: 0
};
const parsed = parse(url);
this._url = url;
this._protocol = parsed.protocol;
this._sema = new Sema(20);
this._output = createOutput({ debug });
}
setConcurrency({
maxStreams,
capacity
}: {
maxStreams: number;
capacity: number;
}) {
this._sema = new Sema(maxStreams || 20, { capacity });
}
async fetch(path: string, opts: AgentFetchOptions) {
const { debug } = this._output;
await this._sema.acquire();
let currentContext: CurrentContext;
this._currContext.fetchesMade++;
if (this._currContext.fetchesMade >= MAX_REQUESTS_PER_CONNECTION) {
const ctx = { ...context(), fetchesMade: 1, ongoingFetches: 0 };
this._contexts.push(ctx);
this._currContext = ctx;
}
// If we're changing contexts, we don't want to record the ongoingFetch on the old context
// That'll cause an off-by-one error when trying to close the old socket later
this._currContext.ongoingFetches++;
currentContext = this._currContext;
debug(
`Total requests made on socket #${this._contexts.length}: ${this
._currContext.fetchesMade}`
);
debug(
`Concurrent requests on socket #${this._contexts.length}: ${this
._currContext.ongoingFetches}`
);
let body: JsonBody | StreamBody | string | undefined;
if (opts.body && typeof opts.body === 'object') {
if (typeof (<NodeJS.ReadableStream>opts.body).pipe === 'function') {
body = new StreamBody(<NodeJS.ReadableStream>opts.body);
} else {
opts.headers['Content-Type'] = 'application/json';
body = new JsonBody(opts.body);
}
} else {
body = opts.body;
}
const { host, protocol } = parse(path);
const url = host ? `${protocol}//${host}` : this._url;
const handleCompleted = async <T>(res: T) => {
currentContext.ongoingFetches--;
if (
(currentContext !== this._currContext || host) &&
currentContext.ongoingFetches <= 0
) {
// We've completely moved on to a new socket
// close the old one
// TODO: Fix race condition:
// If the response is a stream, and the server is still streaming data
// we should check if the stream has closed before disconnecting
// hasCompleted CAN technically be called before the res body stream is closed
debug('Closing old socket');
currentContext.disconnect(url);
}
this._sema.release();
return res;
};
return currentContext
.fetch((host ? '' : this._url) + path, { ...opts, body })
.then(res => handleCompleted(res))
.catch((err: Error) => {
handleCompleted(null);
throw err;
});
}
close() {
const { debug } = this._output;
debug('Closing agent');
this._currContext.disconnect(this._url);
}
}

View File

@@ -34,6 +34,11 @@ export default async function getDeploymentForAlias(
} }
const appName = await getAppName(output, localConfig, localConfigPath); const appName = await getAppName(output, localConfig, localConfigPath);
if (!appName) {
return null;
}
const deployment = await getAppLastDeployment( const deployment = await getAppLastDeployment(
output, output,
client, client,

View File

@@ -7,7 +7,11 @@ export default async function getInferredTargets(
output: Output, output: Output,
config: Config config: Config
) { ) {
output.warn(`The ${cmd('now alias')} command (no arguments) was deprecated in favour of ${cmd('now --prod')}.`); output.warn(
`The ${cmd(
'now alias'
)} command (no arguments) was deprecated in favor of ${cmd('now --prod')}.`
);
// This field is deprecated, warn about it // This field is deprecated, warn about it
if (config.aliases) { if (config.aliases) {

View File

@@ -1,9 +1,9 @@
import qs from 'querystring'; import qs from 'querystring';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { parse as parseUrl } from 'url'; import { parse as parseUrl } from 'url';
import fetch, { RequestInit } from 'node-fetch';
import retry, { RetryFunction, Options as RetryOptions } from 'async-retry'; import retry, { RetryFunction, Options as RetryOptions } from 'async-retry';
import createOutput, { Output } from './output/create-output'; import createOutput, { Output } from './output/create-output';
import Agent, { AgentFetchOptions } from './agent';
import responseError from './response-error'; import responseError from './response-error';
import ua from './ua'; import ua from './ua';
@@ -17,7 +17,6 @@ export type FetchOptions = {
}; };
export default class Client extends EventEmitter { export default class Client extends EventEmitter {
_agent: Agent;
_apiUrl: string; _apiUrl: string;
_debug: boolean; _debug: boolean;
_forceNew: boolean; _forceNew: boolean;
@@ -30,7 +29,7 @@ export default class Client extends EventEmitter {
token, token,
currentTeam, currentTeam,
forceNew = false, forceNew = false,
debug = false debug = false,
}: { }: {
apiUrl: string; apiUrl: string;
token: string; token: string;
@@ -44,30 +43,23 @@ export default class Client extends EventEmitter {
this._forceNew = forceNew; this._forceNew = forceNew;
this._output = createOutput({ debug }); this._output = createOutput({ debug });
this._apiUrl = apiUrl; this._apiUrl = apiUrl;
this._agent = new Agent(apiUrl, { debug });
this._onRetry = this._onRetry.bind(this); this._onRetry = this._onRetry.bind(this);
this.currentTeam = currentTeam; this.currentTeam = currentTeam;
const closeAgent = () => {
this._agent.close();
process.removeListener('nowExit', closeAgent);
};
// @ts-ignore
process.on('nowExit', closeAgent);
} }
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) { retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
return retry(fn, { return retry(fn, {
retries, retries,
maxTimeout, maxTimeout,
onRetry: this._onRetry onRetry: this._onRetry,
}); });
} }
_fetch(_url: string, opts: FetchOptions = {}) { _fetch(_url: string, opts: FetchOptions = {}) {
const parsedUrl = parseUrl(_url, true); const parsedUrl = parseUrl(_url, true);
const apiUrl = parsedUrl.host ? `${parsedUrl.protocol}//${parsedUrl.host}` : ''; const apiUrl = parsedUrl.host
? `${parsedUrl.protocol}//${parsedUrl.host}`
: '';
if (opts.useCurrentTeam !== false && this.currentTeam) { if (opts.useCurrentTeam !== false && this.currentTeam) {
const query = parsedUrl.query; const query = parsedUrl.query;
@@ -80,20 +72,19 @@ export default class Client extends EventEmitter {
Object.assign(opts, { Object.assign(opts, {
body: JSON.stringify(opts.body), body: JSON.stringify(opts.body),
headers: Object.assign({}, opts.headers, { headers: Object.assign({}, opts.headers, {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}) }),
}); });
} }
opts.headers = opts.headers || {}; opts.headers = opts.headers || {};
opts.headers.authorization = `Bearer ${this._token}`; opts.headers.Authorization = `Bearer ${this._token}`;
opts.headers['user-agent'] = ua; opts.headers['user-agent'] = ua;
const url = `${apiUrl ? '' : this._apiUrl}${_url}`;
return this._output.time( return this._output.time(
`${opts.method || 'GET'} ${apiUrl ? '' : this._apiUrl}${_url} ${JSON.stringify( `${opts.method || 'GET'} ${url} ${JSON.stringify(opts.body) || ''}`,
opts.body fetch(url, opts as RequestInit)
) || ''}`,
this._agent.fetch(_url, opts as AgentFetchOptions)
); );
} }
@@ -126,7 +117,5 @@ export default class Client extends EventEmitter {
this._output.debug(`Retrying: ${error}\n${error.stack}`); this._output.debug(`Retrying: ${error}\n${error.stack}`);
} }
close() { close() {}
this._agent.close();
}
} }

View File

@@ -15,40 +15,45 @@ export default async function createDeploy(
return await now.create(paths, createArgs); return await now.create(paths, createArgs);
} catch (error) { } catch (error) {
if (error.code === 'rate_limited') { if (error.code === 'rate_limited') {
return new ERRORS_TS.DeploymentsRateLimited(error.message); throw new ERRORS_TS.DeploymentsRateLimited(error.message);
} }
// Means that the domain used as a suffix no longer exists // Means that the domain used as a suffix no longer exists
if (error.code === 'domain_missing') { if (error.code === 'domain_missing') {
return new ERRORS_TS.DomainNotFound(error.value); throw new ERRORS_TS.DomainNotFound(error.value);
} }
if (error.code === 'domain_not_found' && error.domain) { if (error.code === 'domain_not_found' && error.domain) {
return new ERRORS_TS.DomainNotFound(error.domain); throw new ERRORS_TS.DomainNotFound(error.domain);
} }
// This error occures when a domain used in the `alias` // This error occures when a domain used in the `alias`
// is not yet verified // is not yet verified
if (error.code === 'domain_not_verified' && error.domain) { if (error.code === 'domain_not_verified' && error.domain) {
return new ERRORS_TS.DomainNotVerified(error.domain); throw new ERRORS_TS.DomainNotVerified(error.domain);
} }
// If the domain used as a suffix is not verified, we fail // If the domain used as a suffix is not verified, we fail
if (error.code === 'domain_not_verified' && error.value) { if (error.code === 'domain_not_verified' && error.value) {
return new ERRORS_TS.DomainVerificationFailed(error.value); throw new ERRORS_TS.DomainVerificationFailed(error.value);
}
// If the domain isn't owned by the user
if (error.code === 'not_domain_owner') {
throw new ERRORS_TS.NotDomainOwner(error.message);
} }
if (error.code === 'builds_rate_limited') { if (error.code === 'builds_rate_limited') {
return new ERRORS_TS.BuildsRateLimited(error.message); throw new ERRORS_TS.BuildsRateLimited(error.message);
} }
// If the user doesn't have permissions over the domain used as a suffix we fail // If the user doesn't have permissions over the domain used as a suffix we fail
if (error.code === 'forbidden') { if (error.code === 'forbidden') {
return new ERRORS_TS.DomainPermissionDenied(error.value, contextName); throw new ERRORS_TS.DomainPermissionDenied(error.value, contextName);
} }
if (error.code === 'bad_request' && error.keyword) { if (error.code === 'bad_request' && error.keyword) {
return new ERRORS.SchemaValidationFailed( throw new ERRORS.SchemaValidationFailed(
error.message, error.message,
error.keyword, error.keyword,
error.dataPath, error.dataPath,
@@ -57,19 +62,19 @@ export default async function createDeploy(
} }
if (error.code === 'domain_configured') { if (error.code === 'domain_configured') {
return new ERRORS_TS.AliasDomainConfigured(error); throw new ERRORS_TS.AliasDomainConfigured(error);
} }
if (error.code === 'missing_build_script') { if (error.code === 'missing_build_script') {
return new ERRORS_TS.MissingBuildScript(error); throw new ERRORS_TS.MissingBuildScript(error);
} }
if (error.code === 'conflicting_file_path') { if (error.code === 'conflicting_file_path') {
return new ERRORS_TS.ConflictingFilePath(error); throw new ERRORS_TS.ConflictingFilePath(error);
} }
if (error.code === 'conflicting_path_segment') { if (error.code === 'conflicting_path_segment') {
return new ERRORS_TS.ConflictingPathSegment(error); throw new ERRORS_TS.ConflictingPathSegment(error);
} }
// If the cert is missing we try to generate a new one and the retry // If the cert is missing we try to generate a new one and the retry
@@ -87,10 +92,10 @@ export default async function createDeploy(
} }
if (error.code === 'not_found') { if (error.code === 'not_found') {
return new ERRORS_TS.DeploymentNotFound({ context: contextName }); throw new ERRORS_TS.DeploymentNotFound({ context: contextName });
} }
const certError = mapCertError(error) const certError = mapCertError(error);
if (certError) { if (certError) {
return certError; return certError;
} }

View File

@@ -1,35 +0,0 @@
//
import sleep from '../sleep';
import createPollingFn from '../create-polling-fn';
import getDeploymentByIdOrThrow from './get-deployment-by-id-or-throw';
const POLLING_INTERVAL = 5000;
async function* getStatusChangeFromPolling(
now: any,
contextName: string,
idOrHost: string,
initialState: string
) {
const pollDeployment = createPollingFn(
getDeploymentByIdOrThrow,
POLLING_INTERVAL
);
let prevState = initialState;
for await (const deployment of pollDeployment(now, contextName, idOrHost)) {
if (prevState !== deployment.state) {
await sleep(5000);
yield {
type: 'state-change',
created: Date.now(),
payload: { value: deployment.state }
};
} else {
prevState = deployment.state;
}
}
}
export default getStatusChangeFromPolling;

View File

@@ -0,0 +1,222 @@
import bytes from 'bytes';
import Progress from 'progress';
import chalk from 'chalk';
import pluralize from 'pluralize';
import {
createDeployment,
createLegacyDeployment,
DeploymentOptions,
} from '../../../../now-client';
import wait from '../output/wait';
import { Output } from '../output';
// @ts-ignore
import Now from '../../util';
import { NowConfig } from '../dev/types';
export default async function processDeployment({
now,
output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
legacy,
env,
quiet,
nowConfig,
}: {
now: Now;
output: Output;
hashes: { [key: string]: any };
paths: string[];
requestBody: DeploymentOptions;
uploadStamp: () => number;
deployStamp: () => number;
legacy: boolean;
env: any;
quiet: boolean;
nowConfig?: NowConfig;
}) {
const { warn, log, debug, note } = output;
let bar: Progress | null = null;
const path0 = paths[0];
const opts: DeploymentOptions = {
...requestBody,
debug: now._debug,
};
if (!legacy) {
let buildSpinner = null;
let deploySpinner = null;
for await (const event of createDeployment(path0, opts, nowConfig)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
if (event.type === 'warning') {
warn(event.payload);
}
if (event.type === 'notice') {
note(event.payload);
}
if (event.type === 'file_count') {
debug(
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
);
if (!quiet) {
log(
`Synced ${pluralize(
'file',
event.payload.missing.length,
true
)} ${uploadStamp()}`
);
}
const missingSize = event.payload.missing
.map((sha: string) => event.payload.total.get(sha).data.length)
.reduce((a: number, b: number) => a + b, 0);
bar = new Progress(`${chalk.gray('>')} Upload [:bar] :percent :etas`, {
width: 20,
complete: '=',
incomplete: '',
total: missingSize,
clear: true,
});
}
if (event.type === 'file-uploaded') {
debug(
`Uploaded: ${event.payload.file.names.join(' ')} (${bytes(
event.payload.file.data.length
)})`
);
if (bar) {
bar.tick(event.payload.file.data.length);
}
}
if (event.type === 'created') {
now._host = event.payload.url;
if (!quiet) {
const version = legacy ? `${chalk.grey('[v1]')} ` : '';
log(`https://${event.payload.url} ${version}${deployStamp()}`);
} else {
process.stdout.write(`https://${event.payload.url}`);
}
}
if (event.type === 'build-state-changed') {
if (buildSpinner === null) {
buildSpinner = wait('Building...');
}
}
if (event.type === 'all-builds-completed') {
if (buildSpinner) {
buildSpinner();
}
deploySpinner = wait('Finalizing...');
}
// Handle error events
if (event.type === 'error') {
if (buildSpinner) {
buildSpinner();
}
if (deploySpinner) {
deploySpinner();
}
throw await now.handleDeploymentError(event.payload, { hashes, env });
}
// Handle ready event
if (event.type === 'ready') {
if (deploySpinner) {
deploySpinner();
}
return event.payload;
}
}
} else {
for await (const event of createLegacyDeployment(path0, opts, nowConfig)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
if (event.type === 'file_count') {
debug(
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
);
if (!quiet) {
log(
`Synced ${pluralize(
'file',
event.payload.missing.length,
true
)} ${uploadStamp()}`
);
}
const missingSize = event.payload.missing
.map((sha: string) => event.payload.total.get(sha).data.length)
.reduce((a: number, b: number) => a + b, 0);
bar = new Progress(`${chalk.gray('>')} Upload [:bar] :percent :etas`, {
width: 20,
complete: '=',
incomplete: '',
total: missingSize,
clear: true,
});
}
if (event.type === 'file-uploaded') {
debug(
`Uploaded: ${event.payload.file.names.join(' ')} (${bytes(
event.payload.file.data.length
)})`
);
if (bar) {
bar.tick(event.payload.file.data.length);
}
}
if (event.type === 'created') {
now._host = event.payload.url;
if (!quiet) {
const version = legacy ? `${chalk.grey('[v1]')} ` : '';
log(`${event.payload.url} ${version}${deployStamp()}`);
} else {
process.stdout.write(`https://${event.payload.url}`);
}
}
// Handle error events
if (event.type === 'error') {
throw await now.handleDeploymentError(event.payload, { hashes, env });
}
// Handle ready event
if (event.type === 'ready') {
log(`Build completed`);
return event.payload;
}
}
}
}

View File

@@ -0,0 +1,18 @@
import { homedir } from 'os';
import promptBool from '../input/prompt-bool';
import { Output } from '../output';
export default async function shouldDeployDir(argv0: string, output: Output) {
let yes = true;
if (argv0 === homedir()) {
if (
!(await promptBool(
'You are deploying your home directory. Do you want to continue?'
))
) {
output.log('Aborted');
yes = false;
}
}
return yes;
}

View File

@@ -8,6 +8,7 @@ import { createHash } from 'crypto';
import { createGunzip } from 'zlib'; import { createGunzip } from 'zlib';
import { join, resolve } from 'path'; import { join, resolve } from 'path';
import { funCacheDir } from '@zeit/fun'; import { funCacheDir } from '@zeit/fun';
import { PackageJson } from '@now/build-utils';
import XDGAppPaths from 'xdg-app-paths'; import XDGAppPaths from 'xdg-app-paths';
import { import {
createReadStream, createReadStream,
@@ -15,7 +16,7 @@ import {
readFile, readFile,
readJSON, readJSON,
writeFile, writeFile,
remove remove,
} from 'fs-extra'; } from 'fs-extra';
import pkg from '../../../package.json'; import pkg from '../../../package.json';
@@ -23,10 +24,10 @@ import { NoBuilderCacheError, BuilderCacheCleanError } from '../errors-ts';
import wait from '../output/wait'; import wait from '../output/wait';
import { Output } from '../output'; import { Output } from '../output';
import { getDistTag } from '../get-dist-tag'; import { getDistTag } from '../get-dist-tag';
import { devDependencies } from '../../../package.json';
import * as staticBuilder from './static-builder'; import * as staticBuilder from './static-builder';
import { BuilderWithPackage, Package } from './types'; import { BuilderWithPackage } from './types';
import { getBundledBuilders } from './get-bundled-builders';
const registryTypes = new Set(['version', 'tag', 'range']); const registryTypes = new Set(['version', 'tag', 'range']);
@@ -34,14 +35,10 @@ const localBuilders: { [key: string]: BuilderWithPackage } = {
'@now/static': { '@now/static': {
runInProcess: true, runInProcess: true,
builder: Object.freeze(staticBuilder), builder: Object.freeze(staticBuilder),
package: Object.freeze({ name: '@now/static', version: '' }) package: Object.freeze({ name: '@now/static', version: '' }),
} },
}; };
const bundledBuilders = Object.keys(devDependencies).filter(d =>
d.startsWith('@now/')
);
const distTag = getDistTag(pkg.version); const distTag = getDistTag(pkg.version);
export const cacheDirPromise = prepareCacheDir(); export const cacheDirPromise = prepareCacheDir();
@@ -117,7 +114,7 @@ export async function prepareBuilderDir() {
export async function prepareBuilderModulePath() { export async function prepareBuilderModulePath() {
const [builderDir, builderContents] = await Promise.all([ const [builderDir, builderContents] = await Promise.all([
builderDirPromise, builderDirPromise,
readFile(join(__dirname, 'builder-worker.js')) readFile(join(__dirname, 'builder-worker.js')),
]); ]);
let needsWrite = false; let needsWrite = false;
const builderSha = getSha(builderContents); const builderSha = getSha(builderContents);
@@ -179,7 +176,7 @@ export function getBuildUtils(packages: string[]): string {
export function filterPackage( export function filterPackage(
builderSpec: string, builderSpec: string,
distTag: string, distTag: string,
buildersPkg: Package buildersPkg: PackageJson
) { ) {
if (builderSpec in localBuilders) return false; if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec); const parsed = npa(builderSpec);
@@ -187,7 +184,7 @@ export function filterPackage(
parsed.name && parsed.name &&
parsed.type === 'tag' && parsed.type === 'tag' &&
parsed.fetchSpec === distTag && parsed.fetchSpec === distTag &&
bundledBuilders.includes(parsed.name) && getBundledBuilders().includes(parsed.name) &&
buildersPkg.dependencies buildersPkg.dependencies
) { ) {
const parsedInstalled = npa( const parsedInstalled = npa(
@@ -259,10 +256,10 @@ export async function installBuilders(
'--exact', '--exact',
'--no-lockfile', '--no-lockfile',
'--non-interactive', '--non-interactive',
...packagesToInstall ...packagesToInstall,
], ],
{ {
cwd: builderDir cwd: builderDir,
} }
); );
} finally { } finally {
@@ -294,10 +291,10 @@ export async function updateBuilders(
'--exact', '--exact',
'--no-lockfile', '--no-lockfile',
'--non-interactive', '--non-interactive',
...packages.filter(p => p !== '@now/static') ...packages.filter(p => p !== '@now/static'),
], ],
{ {
cwd: builderDir cwd: builderDir,
} }
); );
@@ -336,7 +333,7 @@ export async function getBuilder(
const pkg = require(join(dest, 'package.json')); const pkg = require(join(dest, 'package.json'));
builderWithPkg = { builderWithPkg = {
builder: Object.freeze(mod), builder: Object.freeze(mod),
package: Object.freeze(pkg) package: Object.freeze(pkg),
}; };
} catch (err) { } catch (err) {
if (err.code === 'MODULE_NOT_FOUND') { if (err.code === 'MODULE_NOT_FOUND') {
@@ -357,7 +354,7 @@ export async function getBuilder(
function getPackageName( function getPackageName(
parsed: npa.Result, parsed: npa.Result,
buildersPkg: Package buildersPkg: PackageJson
): string | null { ): string | null {
if (registryTypes.has(parsed.type)) { if (registryTypes.has(parsed.type)) {
return parsed.name; return parsed.name;
@@ -378,7 +375,7 @@ function getSha(buffer: Buffer): string {
} }
function hasBundledBuilders(dependencies: { [name: string]: string }): boolean { function hasBundledBuilders(dependencies: { [name: string]: string }): boolean {
for (const name of bundledBuilders) { for (const name of getBundledBuilders()) {
if (!(name in dependencies)) { if (!(name in dependencies)) {
return false; return false;
} }

View File

@@ -5,7 +5,7 @@ import bytes from 'bytes';
import { delimiter, dirname, join } from 'path'; import { delimiter, dirname, join } from 'path';
import { fork, ChildProcess } from 'child_process'; import { fork, ChildProcess } from 'child_process';
import { createFunction } from '@zeit/fun'; import { createFunction } from '@zeit/fun';
import { File, Lambda, FileBlob, FileFsRef } from '@now/build-utils'; import { Builder, File, Lambda, FileBlob, FileFsRef } from '@now/build-utils';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import chalk from 'chalk'; import chalk from 'chalk';
import which from 'which'; import which from 'which';
@@ -23,12 +23,11 @@ import { builderModulePathPromise, getBuilder } from './builder-cache';
import { import {
EnvConfig, EnvConfig,
NowConfig, NowConfig,
BuildConfig,
BuildMatch, BuildMatch,
BuildResult, BuildResult,
BuilderInputs, BuilderInputs,
BuilderOutput, BuilderOutput,
BuilderOutputs BuilderOutputs,
} from './types'; } from './types';
interface BuildMessage { interface BuildMessage {
@@ -69,7 +68,7 @@ async function createBuildProcess(
} }
const [execPath, modulePath] = await Promise.all([ const [execPath, modulePath] = await Promise.all([
nodeBinPromise, nodeBinPromise,
builderModulePathPromise builderModulePathPromise,
]); ]);
let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`; let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`;
if (yarnPath) { if (yarnPath) {
@@ -81,11 +80,11 @@ async function createBuildProcess(
...process.env, ...process.env,
PATH, PATH,
...buildEnv, ...buildEnv,
NOW_REGION: 'dev1' NOW_REGION: 'dev1',
}, },
execPath, execPath,
execArgv: [], execArgv: [],
stdio: ['ignore', 'pipe', 'pipe', 'ipc'] stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}); });
match.buildProcess = buildProcess; match.buildProcess = buildProcess;
@@ -122,7 +121,7 @@ export async function executeBuild(
filesRemoved?: string[] filesRemoved?: string[]
): Promise<void> { ): Promise<void> {
const { const {
builderWithPkg: { runInProcess, builder, package: pkg } builderWithPkg: { runInProcess, builder, package: pkg },
} = match; } = match;
const { src: entrypoint } = match; const { src: entrypoint } = match;
const { env, debug, buildEnv, yarnPath, cwd: workPath } = devServer; const { env, debug, buildEnv, yarnPath, cwd: workPath } = devServer;
@@ -165,8 +164,8 @@ export async function executeBuild(
filesChanged, filesChanged,
filesRemoved, filesRemoved,
env, env,
buildEnv buildEnv,
} },
}; };
let buildResultOrOutputs: BuilderOutputs | BuildResult; let buildResultOrOutputs: BuilderOutputs | BuildResult;
@@ -203,7 +202,7 @@ export async function executeBuild(
buildProcess.send({ buildProcess.send({
type: 'build', type: 'build',
builderName: pkg.name, builderName: pkg.name,
buildParams buildParams,
}); });
buildResultOrOutputs = await new Promise((resolve, reject) => { buildResultOrOutputs = await new Promise((resolve, reject) => {
@@ -260,7 +259,11 @@ export async function executeBuild(
result = { result = {
output: buildResultOrOutputs as BuilderOutputs, output: buildResultOrOutputs as BuilderOutputs,
routes: [], routes: [],
watch: [] watch: [],
distPath:
typeof buildResultOrOutputs.distPath === 'string'
? buildResultOrOutputs.distPath
: undefined,
}; };
} else { } else {
result = buildResultOrOutputs as BuildResult; result = buildResultOrOutputs as BuildResult;
@@ -346,9 +349,9 @@ export async function executeBuild(
...nowConfig.env, ...nowConfig.env,
...asset.environment, ...asset.environment,
...env, ...env,
NOW_REGION: 'dev1' NOW_REGION: 'dev1',
} },
} },
}); });
} }
@@ -382,7 +385,7 @@ export async function getBuildMatches(
return matches; return matches;
} }
const noMatches: BuildConfig[] = []; const noMatches: Builder[] = [];
const builds = nowConfig.builds || [{ src: '**', use: '@now/static' }]; const builds = nowConfig.builds || [{ src: '**', use: '@now/static' }];
for (const buildConfig of builds) { for (const buildConfig of builds) {
@@ -420,7 +423,7 @@ export async function getBuildMatches(
builderWithPkg, builderWithPkg,
buildOutput: {}, buildOutput: {},
buildResults: new Map(), buildResults: new Map(),
buildTimestamp: 0 buildTimestamp: 0,
}); });
} }
} }

View File

@@ -12,13 +12,13 @@ export const httpStatusDescriptionMap = new Map([
[502, 'BAD_GATEWAY'], [502, 'BAD_GATEWAY'],
[503, 'SERVICE_UNAVAILABLE'], [503, 'SERVICE_UNAVAILABLE'],
[504, 'GATEWAY_TIMEOUT'], [504, 'GATEWAY_TIMEOUT'],
[508, 'INFINITE_LOOP'] [508, 'INFINITE_LOOP'],
]); ]);
export const errorMessageMap = new Map([ export const errorMessageMap = new Map([
[400, 'Bad request'], [400, 'Bad request'],
[402, 'Payment required'], [402, 'Payment required'],
[403, 'You don\'t have the required permissions'], [403, "You don't have the required permissions"],
[404, 'The page could not be found'], [404, 'The page could not be found'],
[405, 'Method not allowed'], [405, 'Method not allowed'],
[410, 'The deployment has been removed'], [410, 'The deployment has been removed'],
@@ -28,7 +28,7 @@ export const errorMessageMap = new Map([
[501, 'Not implemented'], [501, 'Not implemented'],
[503, 'The deployment is currently unavailable'], [503, 'The deployment is currently unavailable'],
[504, 'An error occurred with your deployment'], [504, 'An error occurred with your deployment'],
[508, 'Infinite loop detected'] [508, 'Infinite loop detected'],
]); ]);
interface ErrorMessage { interface ErrorMessage {
@@ -40,20 +40,20 @@ interface ErrorMessage {
const appError = { const appError = {
title: 'An error occurred with this application.', title: 'An error occurred with this application.',
subtitle: 'This is an error with the application itself, not the platform.', subtitle: 'This is an error with the application itself, not the platform.',
app_error: true app_error: true,
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const infrastructureError = { const infrastructureError = {
title: 'An internal error occurred with ZEIT Now.', title: 'An internal error occurred with ZEIT Now.',
subtitle: 'This is an error with the platform itself, not the application.', subtitle: 'This is an error with the platform itself, not the application.',
app_error: false app_error: false,
}; };
const pageNotFoundError = { const pageNotFoundError = {
title: 'The page could not be found.', title: 'The page could not be found.',
subtitle: 'The page could not be found in the application.', subtitle: 'The page could not be found in the application.',
app_error: true app_error: true,
}; };
export function generateErrorMessage( export function generateErrorMessage(
@@ -68,7 +68,7 @@ export function generateErrorMessage(
} }
return { return {
title: errorMessageMap.get(statusCode) || 'Error occurred', title: errorMessageMap.get(statusCode) || 'Error occurred',
app_error: false app_error: false,
}; };
} }

View File

@@ -0,0 +1,11 @@
export function getBundledBuilders() {
return [
'@now/go',
'@now/next',
'@now/node',
'@now/ruby',
'@now/python',
'@now/static-build',
'@now/build-utils',
];
}

View File

@@ -98,7 +98,7 @@ export default async function(
headers: combinedHeaders, headers: combinedHeaders,
uri_args: query, uri_args: query,
matched_route: routeConfig, matched_route: routeConfig,
matched_route_idx: idx matched_route_idx: idx,
}; };
break; break;
} else { } else {
@@ -114,7 +114,7 @@ export default async function(
headers: combinedHeaders, headers: combinedHeaders,
uri_args: query, uri_args: query,
matched_route: routeConfig, matched_route: routeConfig,
matched_route_idx: idx matched_route_idx: idx,
}; };
break; break;
} }
@@ -127,7 +127,7 @@ export default async function(
found: false, found: false,
dest: reqPathname, dest: reqPathname,
uri_args: query, uri_args: query,
headers: combinedHeaders headers: combinedHeaders,
}; };
} }

View File

@@ -16,10 +16,11 @@ import { basename, dirname, extname, join } from 'path';
import directoryTemplate from 'serve-handler/src/directory'; import directoryTemplate from 'serve-handler/src/directory';
import { import {
Builder,
FileFsRef, FileFsRef,
PackageJson, PackageJson,
detectBuilders, detectBuilders,
detectRoutes detectRoutes,
} from '@now/build-utils'; } from '@now/build-utils';
import { once } from '../once'; import { once } from '../once';
@@ -33,7 +34,7 @@ import { version as cliVersion } from '../../../package.json';
import { import {
createIgnore, createIgnore,
staticFiles as getFiles, staticFiles as getFiles,
getAllProjectFiles getAllProjectFiles,
} from '../get-files'; } from '../get-files';
import { validateNowConfigBuilds, validateNowConfigRoutes } from './validate'; import { validateNowConfigBuilds, validateNowConfigRoutes } from './validate';
@@ -46,7 +47,7 @@ import { generateErrorMessage, generateHttpStatusDescription } from './errors';
import { import {
builderDirPromise, builderDirPromise,
installBuilders, installBuilders,
updateBuilders updateBuilders,
} from './builder-cache'; } from './builder-cache';
// HTML templates // HTML templates
@@ -60,7 +61,6 @@ import {
EnvConfig, EnvConfig,
NowConfig, NowConfig,
DevServerOptions, DevServerOptions,
BuildConfig,
BuildMatch, BuildMatch,
BuildResult, BuildResult,
BuilderInputs, BuilderInputs,
@@ -70,7 +70,7 @@ import {
InvokeResult, InvokeResult,
ListenSpec, ListenSpec,
RouteConfig, RouteConfig,
RouteResult RouteResult,
} from './types'; } from './types';
interface FSEvent { interface FSEvent {
@@ -87,7 +87,7 @@ interface NodeRequire {
declare const __non_webpack_require__: NodeRequire; declare const __non_webpack_require__: NodeRequire;
function sortBuilders(buildA: BuildConfig, buildB: BuildConfig) { function sortBuilders(buildA: Builder, buildB: Builder) {
if (buildA && buildA.use && buildA.use.startsWith('@now/static-build')) { if (buildA && buildA.use && buildA.use.startsWith('@now/static-build')) {
return 1; return 1;
} }
@@ -182,6 +182,20 @@ export default class DevServer {
const filesChanged: Set<string> = new Set(); const filesChanged: Set<string> = new Set();
const filesRemoved: Set<string> = new Set(); const filesRemoved: Set<string> = new Set();
const distPaths: string[] = [];
for (const buildMatch of this.buildMatches.values()) {
for (const buildResult of buildMatch.buildResults.values()) {
if (buildResult.distPath) {
distPaths.push(buildResult.distPath);
}
}
}
events = events.filter(event =>
distPaths.every(distPath => !event.path.startsWith(distPath))
);
// First, update the `files` mapping of source files // First, update the `files` mapping of source files
for (const event of events) { for (const event of events) {
if (event.type === 'add') { if (event.type === 'add') {
@@ -255,9 +269,7 @@ export default class DevServer {
}); });
} else { } else {
this.output.debug( this.output.debug(
`Not rebuilding because \`shouldServe()\` returned \`false\` for "${ `Not rebuilding because \`shouldServe()\` returned \`false\` for "${match.use}" request path "${requestPath}"`
match.use
}" request path "${requestPath}"`
); );
} }
} }
@@ -376,7 +388,7 @@ export default class DevServer {
// Sort build matches to make sure `@now/static-build` is always last // Sort build matches to make sure `@now/static-build` is always last
this.buildMatches = new Map( this.buildMatches = new Map(
[...this.buildMatches.entries()].sort((matchA, matchB) => { [...this.buildMatches.entries()].sort((matchA, matchB) => {
return sortBuilders(matchA[1] as BuildConfig, matchB[1] as BuildConfig); return sortBuilders(matchA[1] as Builder, matchB[1] as Builder);
}) })
); );
} }
@@ -413,10 +425,10 @@ export default class DevServer {
for (const buildMatch of this.buildMatches.values()) { for (const buildMatch of this.buildMatches.values()) {
const { const {
src, src,
builderWithPkg: { package: pkg } builderWithPkg: { package: pkg },
} = buildMatch; } = buildMatch;
if (pkg.name === '@now/static') continue; if (pkg.name === '@now/static') continue;
if (updatedBuilders.includes(pkg.name)) { if (pkg.name && updatedBuilders.includes(pkg.name)) {
this.buildMatches.delete(src); this.buildMatches.delete(src);
this.output.debug(`Invalidated build match for "${src}"`); this.output.debug(`Invalidated build match for "${src}"`);
} }
@@ -441,7 +453,7 @@ export default class DevServer {
} }
} }
try { try {
this.validateEnvConfig(fileName, base || {}, env); return this.validateEnvConfig(fileName, base || {}, env);
} catch (err) { } catch (err) {
if (err instanceof MissingDotenvVarsError) { if (err instanceof MissingDotenvVarsError) {
this.output.error(err.message); this.output.error(err.message);
@@ -450,7 +462,7 @@ export default class DevServer {
throw err; throw err;
} }
} }
return { ...base, ...env }; return {};
} }
async getNowConfig( async getNowConfig(
@@ -516,8 +528,8 @@ export default class DevServer {
`filtered out ${allFiles.length - files.length} files` `filtered out ${allFiles.length - files.length} files`
); );
const { builders, errors } = await detectBuilders(files, pkg, { const { builders, warnings, errors } = await detectBuilders(files, pkg, {
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest' tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
}); });
if (errors) { if (errors) {
@@ -525,6 +537,10 @@ export default class DevServer {
await this.exit(); await this.exit();
} }
if (warnings && warnings.length > 0) {
warnings.forEach(warning => this.output.warn(warning.message));
}
if (builders) { if (builders) {
const { defaultRoutes, error: routesError } = await detectRoutes( const { defaultRoutes, error: routesError } = await detectRoutes(
files, files,
@@ -608,7 +624,9 @@ export default class DevServer {
type: string, type: string,
env: EnvConfig = {}, env: EnvConfig = {},
localEnv: EnvConfig = {} localEnv: EnvConfig = {}
): void { ): EnvConfig {
// Validate if there are any missing env vars defined in `now.json`,
// but not in the `.env` / `.build.env` file
const missing: string[] = Object.entries(env) const missing: string[] = Object.entries(env)
.filter( .filter(
([name, value]) => ([name, value]) =>
@@ -617,9 +635,36 @@ export default class DevServer {
!hasOwnProperty(localEnv, name) !hasOwnProperty(localEnv, name)
) )
.map(([name]) => name); .map(([name]) => name);
if (missing.length >= 1) {
if (missing.length > 0) {
throw new MissingDotenvVarsError(type, missing); throw new MissingDotenvVarsError(type, missing);
} }
const merged: EnvConfig = { ...env, ...localEnv };
// Validate that the env var name matches what AWS Lambda allows:
// - https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html
let hasInvalidName = false;
for (const key of Object.keys(merged)) {
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) {
this.output.warn(
`Ignoring ${type
.split('.')
.slice(1)
.reverse()
.join(' ')} var ${JSON.stringify(key)} because name is invalid`
);
hasInvalidName = true;
delete merged[key];
}
}
if (hasInvalidName) {
this.output.log(
'Env var names must start with letters, and can only contain alphanumeric characters and underscores'
);
}
return merged;
} }
/** /**
@@ -652,7 +697,7 @@ export default class DevServer {
const nowConfigBuild = nowConfig.build || {}; const nowConfigBuild = nowConfig.build || {};
const [env, buildEnv] = await Promise.all([ const [env, buildEnv] = await Promise.all([
this.getLocalEnv('.env', nowConfig.env), this.getLocalEnv('.env', nowConfig.env),
this.getLocalEnv('.env.build', nowConfigBuild.env) this.getLocalEnv('.env.build', nowConfigBuild.env),
]); ]);
Object.assign(process.env, buildEnv); Object.assign(process.env, buildEnv);
this.env = env; this.env = env;
@@ -670,8 +715,8 @@ export default class DevServer {
const builders: Set<string> = new Set( const builders: Set<string> = new Set(
(nowConfig.builds || []) (nowConfig.builds || [])
.filter((b: BuildConfig) => b.use) .filter((b: Builder) => b.use)
.map((b: BuildConfig) => b.use as string) .map((b: Builder) => b.use as string)
); );
await installBuilders(builders, this.yarnPath, this.output); await installBuilders(builders, this.yarnPath, this.output);
@@ -716,7 +761,7 @@ export default class DevServer {
ignoreInitial: true, ignoreInitial: true,
useFsEvents: false, useFsEvents: false,
usePolling: false, usePolling: false,
persistent: true persistent: true,
}); });
this.watcher.on('add', (path: string) => { this.watcher.on('add', (path: string) => {
this.enqueueFsEvent('add', path); this.enqueueFsEvent('add', path);
@@ -856,8 +901,8 @@ export default class DevServer {
const json = JSON.stringify({ const json = JSON.stringify({
error: { error: {
code: statusCode, code: statusCode,
message: errorMessage.title message: errorMessage.title,
} },
}); });
body = `${json}\n`; body = `${json}\n`;
} else if (accept.includes('html')) { } else if (accept.includes('html')) {
@@ -870,7 +915,7 @@ export default class DevServer {
http_status_code: statusCode, http_status_code: statusCode,
http_status_description, http_status_description,
error_code, error_code,
now_id: nowRequestId now_id: nowRequestId,
}); });
} else if (statusCode === 502) { } else if (statusCode === 502) {
view = errorTemplate502({ view = errorTemplate502({
@@ -878,19 +923,19 @@ export default class DevServer {
http_status_code: statusCode, http_status_code: statusCode,
http_status_description, http_status_description,
error_code, error_code,
now_id: nowRequestId now_id: nowRequestId,
}); });
} else { } else {
view = errorTemplate({ view = errorTemplate({
http_status_code: statusCode, http_status_code: statusCode,
http_status_description, http_status_description,
now_id: nowRequestId now_id: nowRequestId,
}); });
} }
body = errorTemplateBase({ body = errorTemplateBase({
http_status_code: statusCode, http_status_code: statusCode,
http_status_description, http_status_description,
view view,
}); });
} else { } else {
res.setHeader('content-type', 'text/plain; charset=utf-8'); res.setHeader('content-type', 'text/plain; charset=utf-8');
@@ -917,7 +962,7 @@ export default class DevServer {
res.setHeader('content-type', 'application/json'); res.setHeader('content-type', 'application/json');
const json = JSON.stringify({ const json = JSON.stringify({
redirect: location, redirect: location,
status: String(statusCode) status: String(statusCode),
}); });
body = `${json}\n`; body = `${json}\n`;
} else if (accept.includes('html')) { } else if (accept.includes('html')) {
@@ -949,7 +994,7 @@ export default class DevServer {
server: 'now', server: 'now',
'x-now-trace': 'dev1', 'x-now-trace': 'dev1',
'x-now-id': nowRequestId, 'x-now-id': nowRequestId,
'x-now-cache': 'MISS' 'x-now-cache': 'MISS',
}; };
for (const [name, value] of Object.entries(allHeaders)) { for (const [name, value] of Object.entries(allHeaders)) {
res.setHeader(name, value); res.setHeader(name, value);
@@ -976,7 +1021,7 @@ export default class DevServer {
'x-now-deployment-url': host, 'x-now-deployment-url': host,
'x-now-id': nowRequestId, 'x-now-id': nowRequestId,
'x-now-log-id': nowRequestId.split('-')[2], 'x-now-log-id': nowRequestId.split('-')[2],
'x-zeit-co-forwarded-for': ip 'x-zeit-co-forwarded-for': ip,
}; };
} }
@@ -1183,9 +1228,7 @@ export default class DevServer {
Object.assign(origUrl.query, uri_args); Object.assign(origUrl.query, uri_args);
const newUrl = url.format(origUrl); const newUrl = url.format(origUrl);
this.output.debug( this.output.debug(
`Checking build result's ${ `Checking build result's ${buildResult.routes.length} \`routes\` to match ${newUrl}`
buildResult.routes.length
} \`routes\` to match ${newUrl}`
); );
const matchedRoute = await devRouter( const matchedRoute = await devRouter(
newUrl, newUrl,
@@ -1238,17 +1281,17 @@ export default class DevServer {
headers: [ headers: [
{ {
key: 'Content-Type', key: 'Content-Type',
value: getMimeType(assetKey) value: getMimeType(assetKey),
} },
] ],
} },
] ],
}); });
case 'FileBlob': case 'FileBlob':
const headers: http.OutgoingHttpHeaders = { const headers: http.OutgoingHttpHeaders = {
'Content-Length': asset.data.length, 'Content-Length': asset.data.length,
'Content-Type': getMimeType(assetKey) 'Content-Type': getMimeType(assetKey),
}; };
this.setResponseHeaders(res, nowRequestId, headers); this.setResponseHeaders(res, nowRequestId, headers);
res.end(asset.data); res.end(asset.data);
@@ -1273,7 +1316,7 @@ export default class DevServer {
Object.assign(parsed.query, uri_args); Object.assign(parsed.query, uri_args);
const path = url.format({ const path = url.format({
pathname: parsed.pathname, pathname: parsed.pathname,
query: parsed.query query: parsed.query,
}); });
const body = await rawBody(req); const body = await rawBody(req);
@@ -1283,7 +1326,7 @@ export default class DevServer {
path, path,
headers: this.getNowProxyHeaders(req, nowRequestId), headers: this.getNowProxyHeaders(req, nowRequestId),
encoding: 'base64', encoding: 'base64',
body: body.toString('base64') body: body.toString('base64'),
}; };
this.output.debug(`Invoking lambda: "${assetKey}" with ${path}`); this.output.debug(`Invoking lambda: "${assetKey}" with ${path}`);
@@ -1292,7 +1335,7 @@ export default class DevServer {
try { try {
result = await asset.fn<InvokeResult>({ result = await asset.fn<InvokeResult>({
Action: 'Invoke', Action: 'Invoke',
body: JSON.stringify(payload) body: JSON.stringify(payload),
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -1379,7 +1422,7 @@ export default class DevServer {
relative: href, relative: href,
ext, ext,
title: href, title: href,
base base,
}; };
}); });
@@ -1391,13 +1434,13 @@ export default class DevServer {
const paths = [ const paths = [
{ {
name: directory, name: directory,
url: requestPath url: requestPath,
} },
]; ];
const directoryHtml = directoryTemplate({ const directoryHtml = directoryTemplate({
files, files,
paths, paths,
directory directory,
}); });
this.setResponseHeaders(res, nowRequestId); this.setResponseHeaders(res, nowRequestId);
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8');
@@ -1459,7 +1502,7 @@ function proxyPass(
ws: true, ws: true,
xfwd: true, xfwd: true,
ignorePath: true, ignorePath: true,
target: dest target: dest,
}); });
proxy.on('error', (error: NodeJS.ErrnoException) => { proxy.on('error', (error: NodeJS.ErrnoException) => {
@@ -1490,7 +1533,7 @@ function serveStaticFile(
public: cwd, public: cwd,
cleanUrls: false, cleanUrls: false,
etag: true, etag: true,
...opts ...opts,
}); });
} }
@@ -1546,7 +1589,7 @@ async function shouldServe(
const { const {
src: entrypoint, src: entrypoint,
config, config,
builderWithPkg: { builder } builderWithPkg: { builder },
} = match; } = match;
if (typeof builder.shouldServe === 'function') { if (typeof builder.shouldServe === 'function') {
const shouldServe = await builder.shouldServe({ const shouldServe = await builder.shouldServe({
@@ -1554,7 +1597,7 @@ async function shouldServe(
files, files,
config, config,
requestPath, requestPath,
workPath: devServer.cwd workPath: devServer.cwd,
}); });
if (shouldServe) { if (shouldServe) {
return true; return true;

View File

@@ -5,7 +5,7 @@ export const version = 2;
export function build({ files, entrypoint }: BuilderParams): BuildResult { export function build({ files, entrypoint }: BuilderParams): BuildResult {
const output = { const output = {
[entrypoint]: files[entrypoint] [entrypoint]: files[entrypoint],
}; };
const watch = [entrypoint]; const watch = [entrypoint];
@@ -15,7 +15,7 @@ export function build({ files, entrypoint }: BuilderParams): BuildResult {
export function shouldServe({ export function shouldServe({
entrypoint, entrypoint,
files, files,
requestPath requestPath,
}: ShouldServeParams) { }: ShouldServeParams) {
if (isIndex(entrypoint)) { if (isIndex(entrypoint)) {
const indexPath = join(requestPath, basename(entrypoint)); const indexPath = join(requestPath, basename(entrypoint));

View File

@@ -1,7 +1,13 @@
import http from 'http'; import http from 'http';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { Lambda as FunLambda } from '@zeit/fun'; import { Lambda as FunLambda } from '@zeit/fun';
import { FileBlob, FileFsRef, Lambda } from '@now/build-utils'; import {
Builder as BuildConfig,
FileBlob,
FileFsRef,
Lambda,
PackageJson,
} from '@now/build-utils';
import { Output } from '../output'; import { Output } from '../output';
export interface DevServerOptions { export interface DevServerOptions {
@@ -13,12 +19,6 @@ export interface EnvConfig {
[name: string]: string | undefined; [name: string]: string | undefined;
} }
export interface BuildConfig {
src: string;
use?: string;
config?: object;
}
export interface BuildMatch extends BuildConfig { export interface BuildMatch extends BuildConfig {
builderWithPkg: BuilderWithPackage; builderWithPkg: BuilderWithPackage;
buildOutput: BuilderOutputs; buildOutput: BuilderOutputs;
@@ -119,6 +119,7 @@ export interface BuildResult {
output: BuilderOutputs; output: BuilderOutputs;
routes: RouteConfig[]; routes: RouteConfig[];
watch: string[]; watch: string[];
distPath?: string;
} }
export interface ShouldServeParams { export interface ShouldServeParams {
@@ -129,18 +130,10 @@ export interface ShouldServeParams {
workPath: string; workPath: string;
} }
export interface Package {
name: string;
version: string;
scripts?: { [key: string]: string };
dependencies?: { [name: string]: string };
devDependencies?: { [name: string]: string };
}
export interface BuilderWithPackage { export interface BuilderWithPackage {
runInProcess?: boolean; runInProcess?: boolean;
builder: Readonly<Builder>; builder: Readonly<Builder>;
package: Readonly<Package>; package: Readonly<PackageJson>;
} }
export interface HttpHeadersConfig { export interface HttpHeadersConfig {

View File

@@ -16,16 +16,16 @@ const buildsSchema = {
src: { src: {
type: 'string', type: 'string',
minLength: 1, minLength: 1,
maxLength: 4096 maxLength: 4096,
}, },
use: { use: {
type: 'string', type: 'string',
minLength: 3, minLength: 3,
maxLength: 256 maxLength: 256,
}, },
config: { type: 'object' } config: { type: 'object' },
} },
} },
}; };
const validateBuilds = ajv.compile(buildsSchema); const validateBuilds = ajv.compile(buildsSchema);

View File

@@ -5,7 +5,7 @@ import {
writeFile, writeFile,
statSync, statSync,
chmodSync, chmodSync,
createReadStream createReadStream,
} from 'fs-extra'; } from 'fs-extra';
import pipe from 'promisepipe'; import pipe from 'promisepipe';
import { join } from 'path'; import { join } from 'path';
@@ -63,7 +63,7 @@ async function installYarn(output: Output): Promise<string> {
output.debug(`Downloading ${YARN_URL}`); output.debug(`Downloading ${YARN_URL}`);
const response = await fetch(YARN_URL, { const response = await fetch(YARN_URL, {
compress: false, compress: false,
redirect: 'follow' redirect: 'follow',
}); });
if (response.status !== 200) { if (response.status !== 200) {
@@ -90,7 +90,7 @@ async function installYarn(output: Output): Promise<string> {
'@echo off', '@echo off',
'@SETLOCAL', '@SETLOCAL',
'@SET PATHEXT=%PATHEXT:;.JS;=;%', '@SET PATHEXT=%PATHEXT:;.JS;=;%',
'node "%~dp0\\yarn" %*' 'node "%~dp0\\yarn" %*',
].join('\r\n') ].join('\r\n')
); );
} }

View File

@@ -1,22 +1,24 @@
import chalk from 'chalk'; import chalk from 'chalk';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import { Response } from 'fetch-h2' import { Response } from 'node-fetch';
import { DomainNotFound, InvalidDomain } from '../errors-ts'; import { DomainNotFound, InvalidDomain } from '../errors-ts';
import Client from '../client'; import Client from '../client';
import wait from '../output/wait'; import wait from '../output/wait';
type JSONResponse = { type JSONResponse = {
recordIds: string[] recordIds: string[];
} };
export default async function importZonefile( export default async function importZonefile(
client: Client, client: Client,
contextName: string, contextName: string,
domain: string, domain: string,
zonefilePath: string, zonefilePath: string
) { ) {
const cancelWait = wait(`Importing Zone file for domain ${domain} under ${chalk.bold(contextName)}`); const cancelWait = wait(
`Importing Zone file for domain ${domain} under ${chalk.bold(contextName)}`
);
const zonefile = readFileSync(resolve(zonefilePath), 'utf8'); const zonefile = readFileSync(resolve(zonefilePath), 'utf8');
try { try {
@@ -27,7 +29,7 @@ export default async function importZonefile(
json: false, json: false,
}); });
const { recordIds } = await res.json() as JSONResponse; const { recordIds } = (await res.json()) as JSONResponse;
cancelWait(); cancelWait();
return recordIds; return recordIds;
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,5 @@
import bytes from 'bytes'; import bytes from 'bytes';
import { Response } from 'fetch-h2'; import { Response } from 'node-fetch';
import { NowError } from './now-error'; import { NowError } from './now-error';
import param from './output/param'; import param from './output/param';
import cmd from './output/cmd'; import cmd from './output/cmd';
@@ -53,7 +53,7 @@ export class TeamDeleted extends NowError<'TEAM_DELETED', {}> {
message: `Your team was deleted. You can switch to a different one using ${param( message: `Your team was deleted. You can switch to a different one using ${param(
'now switch' 'now switch'
)}.`, )}.`,
meta: {} meta: {},
}); });
} }
} }
@@ -67,7 +67,7 @@ export class InvalidToken extends NowError<'NOT_AUTHORIZED', {}> {
super({ super({
code: `NOT_AUTHORIZED`, code: `NOT_AUTHORIZED`,
message: `The specified token is not valid`, message: `The specified token is not valid`,
meta: {} meta: {},
}); });
} }
} }
@@ -81,7 +81,7 @@ export class MissingUser extends NowError<'MISSING_USER', {}> {
super({ super({
code: 'MISSING_USER', code: 'MISSING_USER',
message: `Not able to load user, missing from response`, message: `Not able to load user, missing from response`,
meta: {} meta: {},
}); });
} }
} }
@@ -98,7 +98,7 @@ export class DomainAlreadyExists extends NowError<
super({ super({
code: 'DOMAIN_ALREADY_EXISTS', code: 'DOMAIN_ALREADY_EXISTS',
meta: { domain }, meta: { domain },
message: `The domain ${domain} already exists under a different context.` message: `The domain ${domain} already exists under a different context.`,
}); });
} }
} }
@@ -115,7 +115,7 @@ export class DomainPermissionDenied extends NowError<
super({ super({
code: 'DOMAIN_PERMISSION_DENIED', code: 'DOMAIN_PERMISSION_DENIED',
meta: { domain, context }, meta: { domain, context },
message: `You don't have access to the domain ${domain} under ${context}.` message: `You don't have access to the domain ${domain} under ${context}.`,
}); });
} }
} }
@@ -128,7 +128,7 @@ export class DomainExternal extends NowError<
super({ super({
code: 'DOMAIN_EXTERNAL', code: 'DOMAIN_EXTERNAL',
meta: { domain }, meta: { domain },
message: `The domain ${domain} must point to zeit.world.` message: `The domain ${domain} must point to zeit.world.`,
}); });
} }
} }
@@ -143,7 +143,7 @@ export class SourceNotFound extends NowError<'SOURCE_NOT_FOUND', {}> {
meta: {}, meta: {},
message: `Not able to purchase. Please add a payment method using ${cmd( message: `Not able to purchase. Please add a payment method using ${cmd(
'now billing add' 'now billing add'
)}.` )}.`,
}); });
} }
} }
@@ -156,7 +156,7 @@ export class InvalidTransferAuthCode extends NowError<
super({ super({
code: 'INVALID_TRANSFER_AUTH_CODE', code: 'INVALID_TRANSFER_AUTH_CODE',
meta: { domain, authCode }, meta: { domain, authCode },
message: `The provided auth code does not match with the one expected by the current registar` message: `The provided auth code does not match with the one expected by the current registar`,
}); });
} }
} }
@@ -169,7 +169,7 @@ export class DomainRegistrationFailed extends NowError<
super({ super({
code: 'DOMAIN_REGISTRATION_FAILED', code: 'DOMAIN_REGISTRATION_FAILED',
meta: { domain }, meta: { domain },
message message,
}); });
} }
} }
@@ -185,7 +185,7 @@ export class DomainNotFound extends NowError<
super({ super({
code: 'DOMAIN_NOT_FOUND', code: 'DOMAIN_NOT_FOUND',
meta: { domain }, meta: { domain },
message: `The domain ${domain} can't be found.` message: `The domain ${domain} can't be found.`,
}); });
} }
} }
@@ -198,7 +198,7 @@ export class DomainNotVerified extends NowError<
super({ super({
code: 'DOMAIN_NOT_VERIFIED', code: 'DOMAIN_NOT_VERIFIED',
meta: { domain }, meta: { domain },
message: `The domain ${domain} is not verified.` message: `The domain ${domain} is not verified.`,
}); });
} }
} }
@@ -221,7 +221,7 @@ export class DomainVerificationFailed extends NowError<
domain, domain,
nsVerification, nsVerification,
txtVerification, txtVerification,
purchased = false purchased = false,
}: { }: {
domain: string; domain: string;
nsVerification: NSVerificationError; nsVerification: NSVerificationError;
@@ -231,7 +231,7 @@ export class DomainVerificationFailed extends NowError<
super({ super({
code: 'DOMAIN_VERIFICATION_FAILED', code: 'DOMAIN_VERIFICATION_FAILED',
meta: { domain, nsVerification, txtVerification, purchased }, meta: { domain, nsVerification, txtVerification, purchased },
message: `We can't verify the domain ${domain}. Both Name Servers and DNS TXT verifications failed.` message: `We can't verify the domain ${domain}. Both Name Servers and DNS TXT verifications failed.`,
}); });
} }
} }
@@ -264,7 +264,7 @@ export class DomainNsNotVerifiedForWildcard extends NowError<
> { > {
constructor({ constructor({
domain, domain,
nsVerification nsVerification,
}: { }: {
domain: string; domain: string;
nsVerification: NSVerificationError; nsVerification: NSVerificationError;
@@ -272,7 +272,7 @@ export class DomainNsNotVerifiedForWildcard extends NowError<
super({ super({
code: 'DOMAIN_NS_NOT_VERIFIED_FOR_WILDCARD', code: 'DOMAIN_NS_NOT_VERIFIED_FOR_WILDCARD',
meta: { domain, nsVerification }, meta: { domain, nsVerification },
message: `The domain ${domain} is not verified by nameservers for wildcard alias.` message: `The domain ${domain} is not verified by nameservers for wildcard alias.`,
}); });
} }
} }
@@ -289,7 +289,17 @@ export class InvalidDomain extends NowError<
super({ super({
code: 'INVALID_DOMAIN', code: 'INVALID_DOMAIN',
meta: { domain }, meta: { domain },
message: message || `The domain ${domain} is not valid.` message: message || `The domain ${domain} is not valid.`,
});
}
}
export class NotDomainOwner extends NowError<'NOT_DOMAIN_OWNER', {}> {
constructor(message: string) {
super({
code: 'NOT_DOMAIN_OWNER',
meta: {},
message,
}); });
} }
} }
@@ -302,7 +312,7 @@ export class InvalidDeploymentId extends NowError<
super({ super({
code: 'INVALID_DEPLOYMENT_ID', code: 'INVALID_DEPLOYMENT_ID',
meta: { id }, meta: { id },
message: `The deployment id "${id}" is not valid.` message: `The deployment id "${id}" is not valid.`,
}); });
} }
} }
@@ -319,7 +329,7 @@ export class UnsupportedTLD extends NowError<
super({ super({
code: 'UNSUPPORTED_TLD', code: 'UNSUPPORTED_TLD',
meta: { domain }, meta: { domain },
message: `The TLD for domain name ${domain} is not supported.` message: `The TLD for domain name ${domain} is not supported.`,
}); });
} }
} }
@@ -336,7 +346,7 @@ export class DomainNotAvailable extends NowError<
super({ super({
code: 'DOMAIN_NOT_AVAILABLE', code: 'DOMAIN_NOT_AVAILABLE',
meta: { domain }, meta: { domain },
message: `The domain ${domain} is not available to be purchased.` message: `The domain ${domain} is not available to be purchased.`,
}); });
} }
} }
@@ -353,7 +363,7 @@ export class DomainServiceNotAvailable extends NowError<
super({ super({
code: 'DOMAIN_SERVICE_NOT_AVAILABLE', code: 'DOMAIN_SERVICE_NOT_AVAILABLE',
meta: { domain }, meta: { domain },
message: `The domain purchase is unavailable, try again later.` message: `The domain purchase is unavailable, try again later.`,
}); });
} }
} }
@@ -370,7 +380,7 @@ export class DomainNotTransferable extends NowError<
super({ super({
code: 'DOMAIN_NOT_TRANSFERABLE', code: 'DOMAIN_NOT_TRANSFERABLE',
meta: { domain }, meta: { domain },
message: `The domain ${domain} is not available to be transferred.` message: `The domain ${domain} is not available to be transferred.`,
}); });
} }
} }
@@ -386,7 +396,7 @@ export class UnexpectedDomainPurchaseError extends NowError<
super({ super({
code: 'UNEXPECTED_DOMAIN_PURCHASE_ERROR', code: 'UNEXPECTED_DOMAIN_PURCHASE_ERROR',
meta: { domain }, meta: { domain },
message: `An unexpected error happened while purchasing.` message: `An unexpected error happened while purchasing.`,
}); });
} }
} }
@@ -399,7 +409,7 @@ export class DomainPaymentError extends NowError<'DOMAIN_PAYMENT_ERROR', {}> {
super({ super({
code: 'DOMAIN_PAYMENT_ERROR', code: 'DOMAIN_PAYMENT_ERROR',
meta: {}, meta: {},
message: `Your card was declined.` message: `Your card was declined.`,
}); });
} }
} }
@@ -416,7 +426,7 @@ export class DomainPurchasePending extends NowError<
super({ super({
code: 'DOMAIN_PURCHASE_PENDING', code: 'DOMAIN_PURCHASE_PENDING',
meta: { domain }, meta: { domain },
message: `The domain purchase for ${domain} is pending.` message: `The domain purchase for ${domain} is pending.`,
}); });
} }
} }
@@ -430,7 +440,7 @@ export class UserAborted extends NowError<'USER_ABORTED', {}> {
super({ super({
code: 'USER_ABORTED', code: 'USER_ABORTED',
meta: {}, meta: {},
message: `The user aborted the operation.` message: `The user aborted the operation.`,
}); });
} }
} }
@@ -440,7 +450,7 @@ export class CertNotFound extends NowError<'CERT_NOT_FOUND', { id: string }> {
super({ super({
code: 'CERT_NOT_FOUND', code: 'CERT_NOT_FOUND',
meta: { id }, meta: { id },
message: `The cert ${id} can't be found.` message: `The cert ${id} can't be found.`,
}); });
} }
} }
@@ -453,7 +463,7 @@ export class CertsPermissionDenied extends NowError<
super({ super({
code: 'CERTS_PERMISSION_DENIED', code: 'CERTS_PERMISSION_DENIED',
meta: { domain }, meta: { domain },
message: `You don't have access to ${domain}'s certs under ${context}.` message: `You don't have access to ${domain}'s certs under ${context}.`,
}); });
} }
} }
@@ -466,7 +476,7 @@ export class CertOrderNotFound extends NowError<
super({ super({
code: 'CERT_ORDER_NOT_FOUND', code: 'CERT_ORDER_NOT_FOUND',
meta: { cns }, meta: { cns },
message: `No cert order could be found for cns ${cns.join(' ,')}` message: `No cert order could be found for cns ${cns.join(' ,')}`,
}); });
} }
} }
@@ -484,7 +494,7 @@ export class TooManyRequests extends NowError<
super({ super({
code: 'TOO_MANY_REQUESTS', code: 'TOO_MANY_REQUESTS',
meta: { api, retryAfter }, meta: { api, retryAfter },
message: `Rate limited. Too many requests to the same endpoint.` message: `Rate limited. Too many requests to the same endpoint.`,
}); });
} }
} }
@@ -518,7 +528,7 @@ export class CertError extends NowError<
cns, cns,
code, code,
message, message,
helpUrl helpUrl,
}: { }: {
cns: string[]; cns: string[];
code: CertErrorCode; code: CertErrorCode;
@@ -528,7 +538,7 @@ export class CertError extends NowError<
super({ super({
code: `CERT_ERROR`, code: `CERT_ERROR`,
meta: { cns, code, helpUrl }, meta: { cns, code, helpUrl },
message message,
}); });
} }
} }
@@ -547,7 +557,7 @@ export class CertConfigurationError extends NowError<
message, message,
external, external,
type, type,
helpUrl helpUrl,
}: { }: {
cns: string[]; cns: string[];
message: string; message: string;
@@ -558,7 +568,7 @@ export class CertConfigurationError extends NowError<
super({ super({
code: `CERT_CONFIGURATION_ERROR`, code: `CERT_CONFIGURATION_ERROR`,
meta: { cns, helpUrl, external, type }, meta: { cns, helpUrl, external, type },
message message,
}); });
} }
} }
@@ -575,7 +585,7 @@ export class DeploymentNotFound extends NowError<
super({ super({
code: 'DEPLOYMENT_NOT_FOUND', code: 'DEPLOYMENT_NOT_FOUND',
meta: { id, context }, meta: { id, context },
message: `Can't find the deployment ${id} under the context ${context}` message: `Can't find the deployment ${id} under the context ${context}`,
}); });
} }
} }
@@ -592,7 +602,7 @@ export class DeploymentNotReady extends NowError<
super({ super({
code: 'DEPLOYMENT_NOT_READY', code: 'DEPLOYMENT_NOT_READY',
meta: { url }, meta: { url },
message: `The deployment https://${url} is not ready.` message: `The deployment https://${url} is not ready.`,
}); });
} }
} }
@@ -605,7 +615,7 @@ export class DeploymentFailedAliasImpossible extends NowError<
super({ super({
code: 'DEPLOYMENT_FAILED_ALIAS_IMPOSSIBLE', code: 'DEPLOYMENT_FAILED_ALIAS_IMPOSSIBLE',
meta: {}, meta: {},
message: `The deployment build has failed and cannot be aliased` message: `The deployment build has failed and cannot be aliased`,
}); });
} }
} }
@@ -622,7 +632,7 @@ export class DeploymentPermissionDenied extends NowError<
super({ super({
code: 'DEPLOYMENT_PERMISSION_DENIED', code: 'DEPLOYMENT_PERMISSION_DENIED',
meta: { id, context }, meta: { id, context },
message: `You don't have access to the deployment ${id} under ${context}.` message: `You don't have access to the deployment ${id} under ${context}.`,
}); });
} }
} }
@@ -635,7 +645,7 @@ export class DeploymentTypeUnsupported extends NowError<
super({ super({
code: 'DEPLOYMENT_TYPE_UNSUPPORTED', code: 'DEPLOYMENT_TYPE_UNSUPPORTED',
meta: {}, meta: {},
message: `This region only accepts Serverless Docker Deployments` message: `This region only accepts Serverless Docker Deployments`,
}); });
} }
} }
@@ -649,7 +659,7 @@ export class InvalidAlias extends NowError<'INVALID_ALIAS', { alias: string }> {
super({ super({
code: 'INVALID_ALIAS', code: 'INVALID_ALIAS',
meta: { alias }, meta: { alias },
message: `The given alias ${alias} is not valid` message: `The given alias ${alias} is not valid`,
}); });
} }
} }
@@ -663,7 +673,7 @@ export class AliasInUse extends NowError<'ALIAS_IN_USE', { alias: string }> {
super({ super({
code: 'ALIAS_IN_USE', code: 'ALIAS_IN_USE',
meta: { alias }, meta: { alias },
message: `The alias is already in use` message: `The alias is already in use`,
}); });
} }
} }
@@ -678,7 +688,7 @@ export class CertMissing extends NowError<'ALIAS_IN_USE', { domain: string }> {
super({ super({
code: 'ALIAS_IN_USE', code: 'ALIAS_IN_USE',
meta: { domain }, meta: { domain },
message: `The alias is already in use` message: `The alias is already in use`,
}); });
} }
} }
@@ -691,7 +701,7 @@ export class ForbiddenScaleMinInstances extends NowError<
super({ super({
code: 'FORBIDDEN_SCALE_MIN_INSTANCES', code: 'FORBIDDEN_SCALE_MIN_INSTANCES',
meta: { url, max }, meta: { url, max },
message: `You can't scale to more than ${max} min instances with your current plan.` message: `You can't scale to more than ${max} min instances with your current plan.`,
}); });
} }
} }
@@ -704,7 +714,7 @@ export class ForbiddenScaleMaxInstances extends NowError<
super({ super({
code: 'FORBIDDEN_SCALE_MAX_INSTANCES', code: 'FORBIDDEN_SCALE_MAX_INSTANCES',
meta: { url, max }, meta: { url, max },
message: `You can't scale to more than ${max} max instances with your current plan.` message: `You can't scale to more than ${max} max instances with your current plan.`,
}); });
} }
} }
@@ -717,7 +727,7 @@ export class InvalidScaleMinMaxRelation extends NowError<
super({ super({
code: 'INVALID_SCALE_MIN_MAX_RELATION', code: 'INVALID_SCALE_MIN_MAX_RELATION',
meta: { url }, meta: { url },
message: `Min number of instances can't be higher than max.` message: `Min number of instances can't be higher than max.`,
}); });
} }
} }
@@ -730,7 +740,7 @@ export class NotSupportedMinScaleSlots extends NowError<
super({ super({
code: 'NOT_SUPPORTED_MIN_SCALE_SLOTS', code: 'NOT_SUPPORTED_MIN_SCALE_SLOTS',
meta: { url }, meta: { url },
message: `Cloud v2 does not yet support setting a non-zero min scale setting.` message: `Cloud v2 does not yet support setting a non-zero min scale setting.`,
}); });
} }
} }
@@ -743,7 +753,7 @@ export class VerifyScaleTimeout extends NowError<
super({ super({
code: 'VERIFY_SCALE_TIMEOUT', code: 'VERIFY_SCALE_TIMEOUT',
meta: { timeout }, meta: { timeout },
message: `Instance verification timed out (${timeout}ms)` message: `Instance verification timed out (${timeout}ms)`,
}); });
} }
} }
@@ -756,7 +766,7 @@ export class CantParseJSONFile extends NowError<
super({ super({
code: 'CANT_PARSE_JSON_FILE', code: 'CANT_PARSE_JSON_FILE',
meta: { file }, meta: { file },
message: `Can't parse json file` message: `Can't parse json file`,
}); });
} }
} }
@@ -769,7 +779,7 @@ export class CantFindConfig extends NowError<
super({ super({
code: 'CANT_FIND_CONFIG', code: 'CANT_FIND_CONFIG',
meta: { paths }, meta: { paths },
message: `Can't find a configuration file in the given locations.` message: `Can't find a configuration file in the given locations.`,
}); });
} }
} }
@@ -779,7 +789,7 @@ export class FileNotFound extends NowError<'FILE_NOT_FOUND', { file: string }> {
super({ super({
code: 'FILE_NOT_FOUND', code: 'FILE_NOT_FOUND',
meta: { file }, meta: { file },
message: `Can't find a file in provided location '${file}'.` message: `Can't find a file in provided location '${file}'.`,
}); });
} }
} }
@@ -792,7 +802,7 @@ export class RulesFileValidationError extends NowError<
super({ super({
code: 'PATH_ALIAS_VALIDATION_ERROR', code: 'PATH_ALIAS_VALIDATION_ERROR',
meta: { location, message }, meta: { location, message },
message: `The provided rules format in file for path alias are invalid` message: `The provided rules format in file for path alias are invalid`,
}); });
} }
} }
@@ -802,7 +812,7 @@ export class NoAliasInConfig extends NowError<'NO_ALIAS_IN_CONFIG', {}> {
super({ super({
code: 'NO_ALIAS_IN_CONFIG', code: 'NO_ALIAS_IN_CONFIG',
meta: {}, meta: {},
message: `There is no alias set up in config file.` message: `There is no alias set up in config file.`,
}); });
} }
} }
@@ -815,7 +825,7 @@ export class InvalidAliasInConfig extends NowError<
super({ super({
code: 'INVALID_ALIAS_IN_CONFIG', code: 'INVALID_ALIAS_IN_CONFIG',
meta: { value }, meta: { value },
message: `Invalid alias option in configuration.` message: `Invalid alias option in configuration.`,
}); });
} }
} }
@@ -828,7 +838,7 @@ export class RuleValidationFailed extends NowError<
super({ super({
code: 'RULE_VALIDATION_FAILED', code: 'RULE_VALIDATION_FAILED',
meta: { message }, meta: { message },
message: `The server validation for rules failed` message: `The server validation for rules failed`,
}); });
} }
} }
@@ -841,7 +851,7 @@ export class InvalidMinForScale extends NowError<
super({ super({
code: 'INVALID_MIN_FOR_SCALE', code: 'INVALID_MIN_FOR_SCALE',
meta: { value }, meta: { value },
message: `Invalid <min> parameter "${value}". A number or "auto" were expected` message: `Invalid <min> parameter "${value}". A number or "auto" were expected`,
}); });
} }
} }
@@ -854,7 +864,7 @@ export class InvalidArgsForMinMaxScale extends NowError<
super({ super({
code: 'INVALID_ARGS_FOR_MIN_MAX_SCALE', code: 'INVALID_ARGS_FOR_MIN_MAX_SCALE',
meta: { min }, meta: { min },
message: `Invalid number of arguments: expected <min> ("${min}") and [max]` message: `Invalid number of arguments: expected <min> ("${min}") and [max]`,
}); });
} }
} }
@@ -867,7 +877,7 @@ export class InvalidMaxForScale extends NowError<
super({ super({
code: 'INVALID_MAX_FOR_SCALE', code: 'INVALID_MAX_FOR_SCALE',
meta: { value }, meta: { value },
message: `Invalid <max> parameter "${value}". A number or "auto" were expected` message: `Invalid <max> parameter "${value}". A number or "auto" were expected`,
}); });
} }
} }
@@ -877,7 +887,7 @@ export class InvalidCert extends NowError<'INVALID_CERT', {}> {
super({ super({
code: 'INVALID_CERT', code: 'INVALID_CERT',
meta: {}, meta: {},
message: `The provided custom certificate is invalid and couldn't be added` message: `The provided custom certificate is invalid and couldn't be added`,
}); });
} }
} }
@@ -890,7 +900,7 @@ export class DNSPermissionDenied extends NowError<
super({ super({
code: 'DNS_PERMISSION_DENIED', code: 'DNS_PERMISSION_DENIED',
meta: { domain }, meta: { domain },
message: `You don't have access to the DNS records of ${domain}.` message: `You don't have access to the DNS records of ${domain}.`,
}); });
} }
} }
@@ -900,7 +910,7 @@ export class DNSInvalidPort extends NowError<'DNS_INVALID_PORT', {}> {
super({ super({
code: 'DNS_INVALID_PORT', code: 'DNS_INVALID_PORT',
meta: {}, meta: {},
message: `Invalid <port> parameter. A number was expected` message: `Invalid <port> parameter. A number was expected`,
}); });
} }
} }
@@ -913,7 +923,7 @@ export class DNSInvalidType extends NowError<
super({ super({
code: 'DNS_INVALID_TYPE', code: 'DNS_INVALID_TYPE',
meta: { type }, meta: { type },
message: `Invalid <type> parameter "${type}". Expected one of A, AAAA, ALIAS, CAA, CNAME, MX, SRV, TXT` message: `Invalid <type> parameter "${type}". Expected one of A, AAAA, ALIAS, CAA, CNAME, MX, SRV, TXT`,
}); });
} }
} }
@@ -926,7 +936,7 @@ export class DNSConflictingRecord extends NowError<
super({ super({
code: 'DNS_CONFLICTING_RECORD', code: 'DNS_CONFLICTING_RECORD',
meta: { record }, meta: { record },
message: ` A conflicting record exists "${record}".` message: ` A conflicting record exists "${record}".`,
}); });
} }
} }
@@ -949,7 +959,7 @@ export class DomainRemovalConflict extends NowError<
pendingAsyncPurchase, pendingAsyncPurchase,
resolvable, resolvable,
suffix, suffix,
transferring transferring,
}: { }: {
aliases: string[]; aliases: string[];
certs: string[]; certs: string[];
@@ -967,9 +977,9 @@ export class DomainRemovalConflict extends NowError<
pendingAsyncPurchase, pendingAsyncPurchase,
suffix, suffix,
transferring, transferring,
resolvable resolvable,
}, },
message message,
}); });
} }
} }
@@ -982,7 +992,7 @@ export class DomainMoveConflict extends NowError<
message, message,
pendingAsyncPurchase, pendingAsyncPurchase,
resolvable, resolvable,
suffix suffix,
}: { }: {
message: string; message: string;
pendingAsyncPurchase: boolean; pendingAsyncPurchase: boolean;
@@ -994,9 +1004,9 @@ export class DomainMoveConflict extends NowError<
meta: { meta: {
pendingAsyncPurchase, pendingAsyncPurchase,
resolvable, resolvable,
suffix suffix,
}, },
message message,
}); });
} }
} }
@@ -1006,7 +1016,7 @@ export class InvalidEmail extends NowError<'INVALID_EMAIL', { email: string }> {
super({ super({
code: 'INVALID_EMAIL', code: 'INVALID_EMAIL',
message, message,
meta: { email } meta: { email },
}); });
} }
} }
@@ -1022,7 +1032,7 @@ export class AccountNotFound extends NowError<
super({ super({
code: 'ACCOUNT_NOT_FOUND', code: 'ACCOUNT_NOT_FOUND',
message, message,
meta: { email } meta: { email },
}); });
} }
} }
@@ -1035,7 +1045,7 @@ export class InvalidMoveDestination extends NowError<
super({ super({
code: 'INVALID_MOVE_DESTINATION', code: 'INVALID_MOVE_DESTINATION',
message: `Invalid move destination "${destination}"`, message: `Invalid move destination "${destination}"`,
meta: { destination } meta: { destination },
}); });
} }
} }
@@ -1048,7 +1058,7 @@ export class InvalidMoveToken extends NowError<
super({ super({
code: 'INVALID_MOVE_TOKEN', code: 'INVALID_MOVE_TOKEN',
message: `Invalid move token "${token}"`, message: `Invalid move token "${token}"`,
meta: { token } meta: { token },
}); });
} }
} }
@@ -1058,7 +1068,7 @@ export class NoBuilderCacheError extends NowError<'NO_BUILDER_CACHE', {}> {
super({ super({
code: 'NO_BUILDER_CACHE', code: 'NO_BUILDER_CACHE',
message: 'Could not find cache directory for now-builders.', message: 'Could not find cache directory for now-builders.',
meta: {} meta: {},
}); });
} }
} }
@@ -1071,7 +1081,7 @@ export class BuilderCacheCleanError extends NowError<
super({ super({
code: 'BUILDER_CACHE_CLEAN_FAILED', code: 'BUILDER_CACHE_CLEAN_FAILED',
message: `Error cleaning builder cache: ${message}`, message: `Error cleaning builder cache: ${message}`,
meta: { path } meta: { path },
}); });
} }
} }
@@ -1088,7 +1098,7 @@ export class LambdaSizeExceededError extends NowError<
).toLowerCase()}) exceeds the maximum size limit (${bytes( ).toLowerCase()}) exceeds the maximum size limit (${bytes(
maxLambdaSize maxLambdaSize
).toLowerCase()}). Learn more: https://zeit.co/docs/v2/deployments/concepts/lambdas/#maximum-bundle-size`, ).toLowerCase()}). Learn more: https://zeit.co/docs/v2/deployments/concepts/lambdas/#maximum-bundle-size`,
meta: { size, maxLambdaSize } meta: { size, maxLambdaSize },
}); });
} }
} }
@@ -1107,7 +1117,7 @@ export class MissingDotenvVarsError extends NowError<
} else { } else {
message = [ message = [
`The following env vars are not defined in ${code(type)} file:`, `The following env vars are not defined in ${code(type)} file:`,
...missing.map(name => ` - ${JSON.stringify(name)}`) ...missing.map(name => ` - ${JSON.stringify(name)}`),
].join('\n'); ].join('\n');
} }
@@ -1116,7 +1126,7 @@ export class MissingDotenvVarsError extends NowError<
super({ super({
code: 'MISSING_DOTENV_VARS', code: 'MISSING_DOTENV_VARS',
message, message,
meta: { type, missing } meta: { type, missing },
}); });
} }
} }
@@ -1129,7 +1139,7 @@ export class DeploymentsRateLimited extends NowError<
super({ super({
code: 'DEPLOYMENTS_RATE_LIMITED', code: 'DEPLOYMENTS_RATE_LIMITED',
meta: {}, meta: {},
message message,
}); });
} }
} }
@@ -1139,7 +1149,7 @@ export class BuildsRateLimited extends NowError<'BUILDS_RATE_LIMITED', {}> {
super({ super({
code: 'BUILDS_RATE_LIMITED', code: 'BUILDS_RATE_LIMITED',
meta: {}, meta: {},
message message,
}); });
} }
} }
@@ -1149,7 +1159,7 @@ export class ProjectNotFound extends NowError<'PROJECT_NOT_FOUND', {}> {
super({ super({
code: 'PROJECT_NOT_FOUND', code: 'PROJECT_NOT_FOUND',
meta: {}, meta: {},
message: `There is no project for "${nameOrId}"` message: `There is no project for "${nameOrId}"`,
}); });
} }
} }
@@ -1159,7 +1169,7 @@ export class AliasDomainConfigured extends NowError<'DOMAIN_CONFIGURED', {}> {
super({ super({
code: 'DOMAIN_CONFIGURED', code: 'DOMAIN_CONFIGURED',
meta: {}, meta: {},
message message,
}); });
} }
} }
@@ -1169,7 +1179,7 @@ export class MissingBuildScript extends NowError<'MISSING_BUILD_SCRIPT', {}> {
super({ super({
code: 'MISSING_BUILD_SCRIPT', code: 'MISSING_BUILD_SCRIPT',
meta: {}, meta: {},
message message,
}); });
} }
} }
@@ -1179,7 +1189,7 @@ export class ConflictingFilePath extends NowError<'CONFLICTING_FILE_PATH', {}> {
super({ super({
code: 'CONFLICTING_FILE_PATH', code: 'CONFLICTING_FILE_PATH',
meta: {}, meta: {},
message message,
}); });
} }
} }
@@ -1192,7 +1202,23 @@ export class ConflictingPathSegment extends NowError<
super({ super({
code: 'CONFLICTING_PATH_SEGMENT', code: 'CONFLICTING_PATH_SEGMENT',
meta: {}, meta: {},
message message,
});
}
}
export class BuildError extends NowError<'BUILD_ERROR', {}> {
constructor({
message,
meta,
}: {
message: string;
meta: { entrypoint: string };
}) {
super({
code: 'BUILD_ERROR',
meta,
message,
}); });
} }
} }

View File

@@ -15,7 +15,6 @@ export default async function getConfig(output: Output, configFile?: string) {
if (config) { if (config) {
return config; return config;
} }
// First try with the config supplied by the user via --local-config // First try with the config supplied by the user via --local-config
if (configFile) { if (configFile) {
const localFilePath = path.resolve(localPath, configFile); const localFilePath = path.resolve(localPath, configFile);
@@ -27,8 +26,7 @@ export default async function getConfig(output: Output, configFile?: string) {
return localConfig; return localConfig;
} }
if (localConfig !== null) { if (localConfig !== null) {
const castedConfig = localConfig; config = localConfig;
config = castedConfig;
return config; return config;
} }
} }

View File

@@ -18,7 +18,10 @@ export default function handleError(
if ((<APIError>error).status === 403) { if ((<APIError>error).status === 403) {
console.error( console.error(
errorOutput('Authentication error. Run `now login` to log-in again.') errorOutput(
error.message ||
'Authentication error. Run `now login` to log-in again.'
)
); );
} else if ((<APIError>error).status === 429) { } else if ((<APIError>error).status === 429) {
// Rate limited: display the message from the server-side, // Rate limited: display the message from the server-side,

View File

@@ -1,5 +1,5 @@
import { homedir } from 'os'; import { homedir } from 'os';
import { resolve as resolvePath, join, basename } from 'path'; import { resolve as resolvePath } from 'path';
import EventEmitter from 'events'; import EventEmitter from 'events';
import qs from 'querystring'; import qs from 'querystring';
import { parse as parseUrl } from 'url'; import { parse as parseUrl } from 'url';
@@ -7,24 +7,22 @@ import bytes from 'bytes';
import chalk from 'chalk'; import chalk from 'chalk';
import retry from 'async-retry'; import retry from 'async-retry';
import { parse as parseIni } from 'ini'; import { parse as parseIni } from 'ini';
import { createReadStream } from 'fs';
import fs from 'fs-extra'; import fs from 'fs-extra';
import ms from 'ms'; import ms from 'ms';
import fetch from 'node-fetch';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import { import {
staticFiles as getFiles, staticFiles as getFiles,
npm as getNpmFiles, npm as getNpmFiles,
docker as getDockerFiles docker as getDockerFiles,
} from './get-files'; } from './get-files';
import Agent from './agent.ts';
import ua from './ua.ts'; import ua from './ua.ts';
import hash from './hash'; import processDeployment from './deploy/process-deployment.ts';
import highlight from './output/highlight'; import highlight from './output/highlight';
import createOutput from './output'; import createOutput from './output';
import { responseError } from './error'; import { responseError } from './error';
import stamp from './output/stamp';
// How many concurrent HTTP/2 stream uploads import { BuildError } from './errors-ts';
const MAX_CONCURRENT = 50;
// Check if running windows // Check if running windows
const IS_WIN = process.platform.startsWith('win'); const IS_WIN = process.platform.startsWith('win');
@@ -39,14 +37,8 @@ export default class Now extends EventEmitter {
this._forceNew = forceNew; this._forceNew = forceNew;
this._output = createOutput({ debug }); this._output = createOutput({ debug });
this._apiUrl = apiUrl; this._apiUrl = apiUrl;
this._agent = new Agent(apiUrl, { debug });
this._onRetry = this._onRetry.bind(this); this._onRetry = this._onRetry.bind(this);
this.currentTeam = currentTeam; this.currentTeam = currentTeam;
const closeAgent = () => {
this._agent.close();
process.removeListener('nowExit', closeAgent);
};
process.on('nowExit', closeAgent);
} }
async create( async create(
@@ -61,7 +53,6 @@ export default class Now extends EventEmitter {
nowConfig = {}, nowConfig = {},
hasNowJson = false, hasNowJson = false,
sessionAffinity = 'random', sessionAffinity = 'random',
isFile = false,
atlas = false, atlas = false,
// Latest // Latest
@@ -73,361 +64,152 @@ export default class Now extends EventEmitter {
quiet = false, quiet = false,
env, env,
build, build,
followSymlinks = true,
forceNew = false, forceNew = false,
target = null target = null,
deployStamp,
} }
) { ) {
const { log, warn, time } = this._output; const opts = { output: this._output, hasNowJson };
const { log, warn, debug } = this._output;
const isBuilds = type === null; const isBuilds = type === null;
let files = []; let files = [];
let hashes = {};
const relatives = {}; const relatives = {};
let engines; let engines;
let deployment;
let requestBody = {};
await time('Getting files', async () => { if (isBuilds) {
const opts = { output: this._output, hasNowJson }; requestBody = {
token: this._token,
teamId: this.currentTeam,
env,
build,
public: wantsPublic || nowConfig.public,
name,
project,
meta,
regions,
force: forceNew,
};
if (type === 'npm') { if (target) {
files = await getNpmFiles(paths[0], pkg, nowConfig, opts); requestBody.target = target;
}
} else if (type === 'npm') {
files = await getNpmFiles(paths[0], pkg, nowConfig, opts);
// A `start` or `now-start` npm script, or a `server.js` file // A `start` or `now-start` npm script, or a `server.js` file
// in the root directory of the deployment are required // in the root directory of the deployment are required
if ( if (
!isBuilds && !isBuilds &&
!hasNpmStart(pkg) && !hasNpmStart(pkg) &&
!hasFile(paths[0], files, 'server.js') !hasFile(paths[0], files, 'server.js')
) { ) {
const err = new Error( const err = new Error(
'Missing `start` (or `now-start`) script in `package.json`. ' + 'Missing `start` (or `now-start`) script in `package.json`. ' +
'See: https://docs.npmjs.com/cli/start' 'See: https://docs.npmjs.com/cli/start'
); );
throw err; throw err;
}
engines = nowConfig.engines || pkg.engines;
forwardNpm = forwardNpm || nowConfig.forwardNpm;
} else if (type === 'static') {
if (paths.length === 1) {
files = await getFiles(paths[0], nowConfig, opts);
} else {
if (!files) {
files = [];
} }
engines = nowConfig.engines || pkg.engines; for (const path of paths) {
forwardNpm = forwardNpm || nowConfig.forwardNpm; const list = await getFiles(path, {}, opts);
} else if (type === 'static') { files = files.concat(list);
if (isFile) {
files = [resolvePath(paths[0])];
} else if (paths.length === 1) {
files = await getFiles(paths[0], nowConfig, opts);
} else {
if (!files) {
files = [];
}
for (const path of paths) { for (const file of list) {
const list = await getFiles(path, {}, opts); relatives[file] = path;
files = files.concat(list);
for (const file of list) {
relatives[file] = path;
}
}
}
} else if (type === 'docker') {
files = await getDockerFiles(paths[0], nowConfig, opts);
} else if (isBuilds) {
opts.isBuilds = isBuilds;
if (isFile) {
files = [resolvePath(paths[0])];
} else if (paths.length === 1) {
files = await getFiles(paths[0], {}, opts);
} else {
if (!files) {
files = [];
}
for (const path of paths) {
const list = await getFiles(path, {}, opts);
files = files.concat(list);
for (const file of list) {
relatives[file] = path;
}
} }
} }
} }
}); } else if (type === 'docker') {
files = await getDockerFiles(paths[0], nowConfig, opts);
// Read `registry.npmjs.org` authToken from .npmrc
let authToken;
if (type === 'npm' && forwardNpm) {
authToken =
(await readAuthToken(paths[0])) || (await readAuthToken(homedir()));
} }
const hashes = await time('Computing hashes', () => { const uploadStamp = stamp();
const pkgDetails = Object.assign({ name }, pkg);
return hash(files, pkgDetails);
});
this._files = hashes; if (isBuilds) {
deployment = await processDeployment({
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
nowConfig,
});
} else {
// Read `registry.npmjs.org` authToken from .npmrc
let authToken;
const deployment = await this.retry(async bail => { if (type === 'npm' && forwardNpm) {
// Flatten the array to contain files to sync where each nested input authToken =
// array has a group of files with the same sha but different path (await readAuthToken(paths[0])) || (await readAuthToken(homedir()));
const files = await time(
'Get files ready for deployment',
Promise.all(
Array.prototype.concat.apply(
[],
await Promise.all(
Array.from(this._files).map(async ([sha, { data, names }]) => {
const statFn = followSymlinks ? fs.stat : fs.lstat;
return names.map(async name => {
const getMode = async () => {
const st = await statFn(name);
return st.mode;
};
const mode = await getMode();
const multipleStatic = Object.keys(relatives).length !== 0;
let file;
if (isFile) {
file = basename(paths[0]);
} else if (multipleStatic) {
file = toRelative(name, join(relatives[name], '..'));
} else {
file = toRelative(name, paths[0]);
}
return {
sha,
size: data.length,
file,
mode
};
});
})
)
)
)
);
// This is a useful warning because it prevents people
// from getting confused about a deployment that renders 404.
if (
files.length === 0 ||
files.every(item => item.file.startsWith('.'))
) {
warn(
'There are no files (or only files starting with a dot) inside your deployment.'
);
} }
const queryProps = {}; requestBody = {
const requestBody = isBuilds token: this._token,
? { teamId: this.currentTeam,
version: 2, env,
env, build,
build, meta,
public: wantsPublic || nowConfig.public, public: wantsPublic || nowConfig.public,
name, forceNew,
project, name,
files, project,
meta, description,
regions deploymentType: type,
} registryAuthToken: authToken,
: { engines,
env, scale,
build, sessionAffinity,
meta, limits: nowConfig.limits,
public: wantsPublic || nowConfig.public, atlas,
forceNew, config: nowConfig,
name, };
project,
description,
deploymentType: type,
registryAuthToken: authToken,
files,
engines,
scale,
sessionAffinity,
limits: nowConfig.limits,
atlas
};
if (Object.keys(nowConfig).length > 0) { deployment = await processDeployment({
if (isBuilds) { legacy: true,
// These properties are only used inside Now CLI and now: this,
// are not supported on the API. output: this._output,
const exclude = ['github', 'scope']; hashes,
paths,
// Request properties that are made of a combination of requestBody,
// command flags and config properties were already set uploadStamp,
// earlier. Here, we are setting request properties that deployStamp,
// are purely made of their equally-named config property. quiet,
for (const key of Object.keys(nowConfig)) { env,
const value = nowConfig[key]; nowConfig,
});
if (!requestBody[key] && !exclude.includes(key)) { }
requestBody[key] = value;
}
}
} else {
requestBody.config = nowConfig;
}
}
if (isBuilds) {
if (forceNew) {
queryProps.forceNew = 1;
}
if (target) {
requestBody.target = target;
}
if (isFile) {
requestBody.routes = [
{
src: '/',
dest: `/${files[0].file}`
}
];
}
}
const query = qs.stringify(queryProps);
const version = isBuilds ? 'v9' : 'v4';
const res = await this._fetch(
`/${version}/now/deployments${query ? `?${query}` : ''}`,
{
method: 'POST',
body: requestBody
}
);
// No retry on 4xx
let body;
try {
body = await res.json();
} catch (err) {
throw new Error(
`Unexpected response error: ${err.message} (${
res.status
} status code)`
);
}
if (res.status === 429) {
if (body.error && body.error.code === 'builds_rate_limited') {
const err = new Error(body.error.message);
err.status = res.status;
err.retryAfter = 'never';
err.code = body.error.code;
return bail(err);
}
let msg = 'You have been creating deployments at a very fast pace. ';
if (body.error && body.error.limit && body.error.limit.reset) {
const { reset } = body.error.limit;
const difference = reset * 1000 - Date.now();
msg += `Please retry in ${ms(difference, { long: true })}.`;
} else {
msg += 'Please slow down.';
}
const err = new Error(msg);
err.status = res.status;
err.retryAfter = 'never';
return bail(err);
}
// If the deployment domain is missing a cert, bail with the error
if (
res.status === 400 &&
body.error &&
body.error.code === 'cert_missing'
) {
bail(await responseError(res, null, body));
}
if (
res.status === 400 &&
body.error &&
body.error.code === 'missing_files'
) {
return body;
}
if (res.status === 404 && body.error && body.error.code === 'not_found') {
return body;
}
if (res.status >= 400 && res.status < 500) {
const err = new Error();
if (body.error) {
const { code, unreferencedBuildSpecs } = body.error;
if (code === 'env_value_invalid_type') {
const { key } = body.error;
err.message =
`The env key ${key} has an invalid type: ${typeof env[key]}. ` +
'Please supply a String or a Number (https://err.sh/now/env-value-invalid-type)';
} else if (code === 'unreferenced_build_specifications') {
const count = unreferencedBuildSpecs.length;
const prefix = count === 1 ? 'build' : 'builds';
err.message =
`You defined ${count} ${prefix} that did not match any source files (please ensure they are NOT defined in ${highlight(
'.nowignore'
)}):` +
`\n- ${unreferencedBuildSpecs
.map(item => JSON.stringify(item))
.join('\n- ')}`;
} else {
Object.assign(err, body.error);
}
} else {
err.message = 'Not able to create deployment';
}
return bail(err);
}
if (res.status !== 200) {
throw new Error(body.error.message);
}
for (const [name, value] of res.headers.entries()) {
if (name.startsWith('x-now-warning-')) {
this._output.warn(value);
}
}
return body;
});
// We report about files whose sizes are too big // We report about files whose sizes are too big
let missingVersion = false; let missingVersion = false;
if (deployment.warnings) { if (deployment && deployment.warnings) {
let sizeExceeded = 0; let sizeExceeded = 0;
deployment.warnings.forEach(warning => { deployment.warnings.forEach(warning => {
if (warning.reason === 'size_limit_exceeded') { if (warning.reason === 'size_limit_exceeded') {
const { sha, limit } = warning; const { sha, limit } = warning;
const n = hashes.get(sha).names.pop(); const n = hashes[sha].names.pop();
warn(`Skipping file ${n} (size exceeded ${bytes(limit)}`); warn(`Skipping file ${n} (size exceeded ${bytes(limit)}`);
hashes.get(sha).names.unshift(n); // Move name (hack, if duplicate matches we report them in order) hashes[sha].names.unshift(n); // Move name (hack, if duplicate matches we report them in order)
sizeExceeded++; sizeExceeded++;
} else if (warning.reason === 'node_version_not_found') { } else if (warning.reason === 'node_version_not_found') {
warn(`Requested node version ${warning.wanted} is not available`); warn(`Requested node version ${warning.wanted} is not available`);
@@ -445,19 +227,10 @@ export default class Now extends EventEmitter {
} }
} }
if (deployment.error && deployment.error.code === 'missing_files') {
this._missing = deployment.error.missing || [];
this._fileCount = files.length;
return null;
}
if (!isBuilds && !quiet && type === 'npm' && deployment.nodeVersion) { if (!isBuilds && !quiet && type === 'npm' && deployment.nodeVersion) {
if (engines && engines.node && !missingVersion) { if (engines && engines.node && !missingVersion) {
log( log(
chalk`Using Node.js {bold ${ chalk`Using Node.js {bold ${deployment.nodeVersion}} (requested: {dim \`${engines.node}\`})`
deployment.nodeVersion
}} (requested: {dim \`${engines.node}\`})`
); );
} else { } else {
log(chalk`Using Node.js {bold ${deployment.nodeVersion}} (default)`); log(chalk`Using Node.js {bold ${deployment.nodeVersion}} (default)`);
@@ -472,81 +245,90 @@ export default class Now extends EventEmitter {
return deployment; return deployment;
} }
upload({ atlas = false, scale = {} } = {}) { async handleDeploymentError(error, { hashes, env }) {
const { debug, time } = this._output; if (error.status === 429) {
debug(`Will upload ${this._missing.length} files`); if (error.code === 'builds_rate_limited') {
const err = new Error(error.message);
err.status = error.status;
err.retryAfter = 'never';
err.code = error.code;
this._agent.setConcurrency({ return err;
maxStreams: MAX_CONCURRENT, }
capacity: this._missing.length
});
time( let msg = 'You have been creating deployments at a very fast pace. ';
'Uploading files',
Promise.all(
this._missing.map(sha =>
retry(
async bail => {
const file = this._files.get(sha);
const fPath = file.names[0];
const stream = createReadStream(fPath);
const { data } = file;
const fstreamPush = stream.push; if (error.limit && error.limit.reset) {
const { reset } = error.limit;
const difference = reset * 1000 - Date.now();
let uploadedSoFar = 0; msg += `Please retry in ${ms(difference, { long: true })}.`;
stream.push = chunk => { } else {
// If we're about to push the last chunk, then don't do it here msg += 'Please slow down.';
// But instead, we'll "hang" the progress bar and do it on 200 }
if (chunk && uploadedSoFar + chunk.length < data.length) {
this.emit('uploadProgress', chunk.length);
uploadedSoFar += chunk.length;
}
return fstreamPush.call(stream, chunk);
};
const url = atlas ? '/v1/now/images' : '/v2/now/files'; const err = new Error(msg);
const additionalHeaders = atlas
? {
'x-now-dcs': Object.keys(scale).join(',')
}
: {};
const res = await this._fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'x-now-digest': sha,
'x-now-size': data.length,
...additionalHeaders
},
body: stream
});
if (res.status === 200) { err.status = error.status;
// What we want err.retryAfter = 'never';
this.emit('uploadProgress', file.data.length - uploadedSoFar);
this.emit('upload', file); return err;
} else if (res.status > 200 && res.status < 500) { }
// If something is wrong with our request, we don't retry
return bail(await responseError(res, `Failed to upload file with status: ${res.status}`)); // If the deployment domain is missing a cert, bail with the error
} else { if (error.status === 400 && error.code === 'cert_missing') {
// If something is wrong with the server, we retry return responseError(error, null, error);
throw await responseError(res, 'Failed to upload file'); }
}
}, if (error.status === 400 && error.code === 'missing_files') {
{ this._missing = error.missing || [];
retries: 3, this._fileCount = hashes.length;
randomize: true,
onRetry: this._onRetry return error;
} }
)
) if (error.status === 404 && error.code === 'not_found') {
) return error;
) }
.then(() => {
this.emit('complete'); if (error.status >= 400 && error.status < 500) {
}) const err = new Error();
.catch(err => this.emit('error', err));
const { code, unreferencedBuildSpecs } = error;
if (code === 'env_value_invalid_type') {
const { key } = error;
err.message =
`The env key ${key} has an invalid type: ${typeof env[key]}. ` +
'Please supply a String or a Number (https://err.sh/now-cli/env-value-invalid-type)';
} else if (code === 'unreferenced_build_specifications') {
const count = unreferencedBuildSpecs.length;
const prefix = count === 1 ? 'build' : 'builds';
err.message =
`You defined ${count} ${prefix} that did not match any source files (please ensure they are NOT defined in ${highlight(
'.nowignore'
)}):` +
`\n- ${unreferencedBuildSpecs
.map(item => JSON.stringify(item))
.join('\n- ')}`;
} else {
Object.assign(err, error);
}
return err;
}
// Handle build errors
if (error.id && error.id.startsWith('bld_')) {
return new BuildError({
meta: {
entrypoint: error.entrypoint,
},
});
}
return new Error(error.message);
} }
async listSecrets() { async listSecrets() {
@@ -589,7 +371,7 @@ export default class Now extends EventEmitter {
{ {
retries: 3, retries: 3,
minTimeout: 2500, minTimeout: 2500,
onRetry: this._onRetry onRetry: this._onRetry,
} }
); );
}; };
@@ -597,7 +379,7 @@ export default class Now extends EventEmitter {
if (!app && !Object.keys(meta).length) { if (!app && !Object.keys(meta).length) {
// Get the 35 latest projects and their latest deployment // Get the 35 latest projects and their latest deployment
const query = new URLSearchParams({ limit: 35 }); const query = new URLSearchParams({ limit: 35 });
const projects = await fetchRetry(`/projects/list?${query}`); const projects = await fetchRetry(`/v2/projects/?${query}`);
const deployments = await Promise.all( const deployments = await Promise.all(
projects.map(async ({ id: projectId }) => { projects.map(async ({ id: projectId }) => {
@@ -647,7 +429,7 @@ export default class Now extends EventEmitter {
{ {
retries: 3, retries: 3,
minTimeout: 2500, minTimeout: 2500,
onRetry: this._onRetry onRetry: this._onRetry,
} }
); );
@@ -727,7 +509,7 @@ export default class Now extends EventEmitter {
await this.retry(async bail => { await this.retry(async bail => {
const res = await this._fetch(url, { const res = await this._fetch(url, {
method: 'DELETE' method: 'DELETE',
}); });
if (res.status === 200) { if (res.status === 200) {
@@ -748,7 +530,7 @@ export default class Now extends EventEmitter {
return retry(fn, { return retry(fn, {
retries, retries,
maxTimeout, maxTimeout,
onRetry: this._onRetry onRetry: this._onRetry,
}); });
} }
@@ -756,9 +538,7 @@ export default class Now extends EventEmitter {
this._output.debug(`Retrying: ${err}\n${err.stack}`); this._output.debug(`Retrying: ${err}\n${err.stack}`);
} }
close() { close() {}
this._agent.close();
}
get id() { get id() {
return this._id; return this._id;
@@ -802,14 +582,21 @@ export default class Now extends EventEmitter {
opts.headers = opts.headers || {}; opts.headers = opts.headers || {};
opts.headers.accept = 'application/json'; opts.headers.accept = 'application/json';
opts.headers.authorization = `Bearer ${this._token}`; opts.headers.Authorization = `Bearer ${this._token}`;
opts.headers['user-agent'] = ua; opts.headers['user-agent'] = ua;
if (
opts.body &&
typeof opts.body === 'object' &&
opts.body.constructor === Object
) {
opts.body = JSON.stringify(opts.body);
opts.headers['Content-Type'] = 'application/json';
}
return this._output.time( return this._output.time(
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${JSON.stringify( `${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
opts.body fetch(`${this._apiUrl}${_url}`, opts)
) || ''}`,
this._agent.fetch(_url, opts)
); );
} }
@@ -827,8 +614,8 @@ export default class Now extends EventEmitter {
opts = Object.assign({}, opts, { opts = Object.assign({}, opts, {
body: JSON.stringify(opts.body), body: JSON.stringify(opts.body),
headers: Object.assign({}, opts.headers, { headers: Object.assign({}, opts.headers, {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}) }),
}); });
} }
const res = await this._fetch(url, opts); const res = await this._fetch(url, opts);
@@ -875,6 +662,7 @@ function hasNpmStart(pkg) {
function hasFile(base, files, name) { function hasFile(base, files, name) {
const relative = files.map(file => toRelative(file, base)); const relative = files.map(file => toRelative(file, base));
console.log(731, relative);
return relative.indexOf(name) !== -1; return relative.indexOf(name) !== -1;
} }

View File

@@ -1,8 +1,9 @@
import { join } from 'path'; import { join } from 'path';
import { exists } from 'fs-extra'; import { exists } from 'fs-extra';
import { PackageJson } from '@now/build-utils';
import Client from './client'; import Client from './client';
import { Config } from '../types'; import { Config } from '../types';
import { Package } from './dev/types';
import { CantParseJSONFile, ProjectNotFound } from './errors-ts'; import { CantParseJSONFile, ProjectNotFound } from './errors-ts';
import getProjectByIdOrName from './projects/get-project-by-id-or-name'; import getProjectByIdOrName from './projects/get-project-by-id-or-name';
@@ -26,14 +27,14 @@ export default async function preferV2Deployment({
hasServerfile, hasServerfile,
pkg, pkg,
localConfig, localConfig,
projectName projectName,
}: { }: {
client?: Client, client?: Client;
hasDockerfile: boolean, hasDockerfile: boolean;
hasServerfile: boolean, hasServerfile: boolean;
pkg: Package | CantParseJSONFile | null, pkg: PackageJson | CantParseJSONFile | null;
localConfig: Config | undefined, localConfig: Config | undefined;
projectName?: string projectName?: string;
}): Promise<null | string> { }): Promise<null | string> {
if (localConfig && localConfig.version) { if (localConfig && localConfig.version) {
// We will prefer anything that is set here // We will prefer anything that is set here
@@ -52,10 +53,14 @@ export default async function preferV2Deployment({
const { scripts = {} } = pkg; const { scripts = {} } = pkg;
if (!scripts.start && !scripts['now-start']) { if (!scripts.start && !scripts['now-start']) {
return `Deploying to Now 2.0, because ${highlight('package.json')} is missing a ${cmd('start')} script. ${INFO}`; return `Deploying to Now 2.0, because ${highlight(
'package.json'
)} is missing a ${cmd('start')} script. ${INFO}`;
} }
} else if (!pkg && !hasDockerfile) { } else if (!pkg && !hasDockerfile) {
return `Deploying to Now 2.0, because no ${highlight('Dockerfile')} was found. ${INFO}`; return `Deploying to Now 2.0, because no ${highlight(
'Dockerfile'
)} was found. ${INFO}`;
} }
if (client && projectName) { if (client && projectName) {

View File

@@ -2,10 +2,10 @@ import path from 'path';
import { CantParseJSONFile } from './errors-ts'; import { CantParseJSONFile } from './errors-ts';
import readJSONFile from './read-json-file'; import readJSONFile from './read-json-file';
import { Config } from '../types'; import { Config } from '../types';
import { Package } from './dev/types'; import { PackageJson } from '@now/build-utils';
interface CustomPackage extends Package { interface CustomPackage extends PackageJson {
now?: Config now?: Config;
} }
export default async function readPackage(file?: string) { export default async function readPackage(file?: string) {
@@ -16,8 +16,8 @@ export default async function readPackage(file?: string) {
return result; return result;
} }
if (result){ if (result) {
return result as CustomPackage return result as CustomPackage;
} }
return null; return null;

View File

@@ -22,7 +22,7 @@ export default async (sentry, error, apiUrl, configFiles) => {
if (user) { if (user) {
const spec = { const spec = {
email: user.email, email: user.email,
id: user.uid id: user.uid,
}; };
if (user.username) { if (user.username) {
@@ -44,7 +44,7 @@ export default async (sentry, error, apiUrl, configFiles) => {
scope.setExtra('scopeError', { scope.setExtra('scopeError', {
name: scopeError.name, name: scopeError.name,
message: scopeError.message, message: scopeError.message,
stack: scopeError.stack stack: scopeError.stack,
}); });
} }
@@ -81,7 +81,8 @@ export default async (sentry, error, apiUrl, configFiles) => {
// Report information about the version of `node` being used // Report information about the version of `node` being used
scope.setExtra('node', { scope.setExtra('node', {
execPath: process.execPath, execPath: process.execPath,
version: process.version version: process.version,
platform: process.platform,
}); });
sentry.captureException(error); sentry.captureException(error);

View File

@@ -1,4 +1,4 @@
import { Response } from 'fetch-h2'; import { Response } from 'node-fetch';
import { APIError } from './errors-ts'; import { APIError } from './errors-ts';
export default async function responseError( export default async function responseError(

View File

@@ -14,37 +14,37 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~8.1.0", "@angular/animations": "8.1.0",
"@angular/common": "~8.1.0", "@angular/common": "8.1.0",
"@angular/compiler": "~8.1.0", "@angular/compiler": "8.1.0",
"@angular/core": "~8.1.0", "@angular/core": "8.1.0",
"@angular/forms": "~8.1.0", "@angular/forms": "8.1.0",
"@angular/platform-browser": "~8.1.0", "@angular/platform-browser": "8.1.0",
"@angular/platform-browser-dynamic": "~8.1.0", "@angular/platform-browser-dynamic": "8.1.0",
"@angular/router": "~8.1.0", "@angular/router": "8.1.0",
"rxjs": "~6.4.0", "rxjs": "6.4.0",
"tslib": "^1.9.0", "tslib": "1.9.0",
"zone.js": "~0.9.1" "zone.js": "0.9.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.801.0", "@angular-devkit/build-angular": "0.801.0",
"@angular/cli": "~8.1.0", "@angular/cli": "8.1.0",
"@angular/compiler-cli": "~8.1.0", "@angular/compiler-cli": "8.1.0",
"@angular/language-service": "~8.1.0", "@angular/language-service": "8.1.0",
"@types/node": "~8.9.4", "@types/node": "8.9.4",
"@types/jasmine": "~3.3.8", "@types/jasmine": "3.3.8",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "2.0.3",
"codelyzer": "^5.0.0", "codelyzer": "5.0.0",
"jasmine-core": "~3.4.0", "jasmine-core": "3.4.0",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "4.2.1",
"karma": "~4.1.0", "karma": "4.1.0",
"karma-chrome-launcher": "~2.2.0", "karma-chrome-launcher": "2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1", "karma-coverage-istanbul-reporter": "2.0.1",
"karma-jasmine": "~2.0.1", "karma-jasmine": "2.0.1",
"karma-jasmine-html-reporter": "^1.4.0", "karma-jasmine-html-reporter": "1.4.0",
"protractor": "~5.4.0", "protractor": "5.4.0",
"ts-node": "~7.0.0", "ts-node": "7.0.0",
"tslint": "~5.15.0", "tslint": "5.15.0",
"typescript": "~3.4.3" "typescript": "3.4.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
Invalid env var names test

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"env": {
"1": ""
},
"build": {
"env": {
"_a": ""
}
}
}

View File

@@ -0,0 +1 @@
now.json

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"build": "rm -Rf public && mkdir -p public && echo 'hello first' > public/index.html"
}
}

View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@@ -49,7 +49,7 @@ function validateResponseHeaders(t, res) {
async function exec(directory, args = []) { async function exec(directory, args = []) {
return execa(binaryPath, ['dev', directory, ...args], { return execa(binaryPath, ['dev', directory, ...args], {
reject: false reject: false,
}); });
} }
@@ -63,6 +63,21 @@ function formatOutput({ stderr, stdout }) {
return `Received:\n"${stderr}"\n"${stdout}"`; return `Received:\n"${stderr}"\n"${stdout}"`;
} }
async function getPackedBuilderPath(builderDirName) {
const packagePath = path.join(__dirname, '..', '..', '..', builderDirName);
const output = await execa('npm', ['pack'], {
cwd: packagePath,
});
if (output.code !== 0 || output.stdout.trim() === '') {
throw new Error(
`Failed to pack ${builderDirName}: ${formatOutput(output)}`
);
}
return path.join(packagePath, output.stdout.trim());
}
async function testFixture(directory, opts = {}, args = []) { async function testFixture(directory, opts = {}, args = []) {
await runNpmInstall(directory); await runNpmInstall(directory);
@@ -72,9 +87,9 @@ async function testFixture(directory, opts = {}, args = []) {
reject: false, reject: false,
detached: true, detached: true,
stdio: 'ignore', stdio: 'ignore',
...opts ...opts,
}), }),
port port,
}; };
} }
@@ -140,14 +155,43 @@ test('[now dev] validate routes', async t => {
); );
}); });
test('[now dev] 00-list-directory', async t => { test('[now dev] validate env var names', async t => {
const directory = fixture('00-list-directory'); const directory = fixture('invalid-env-var-name');
const { dev, port } = await testFixture(directory); const { dev } = await testFixture(directory, { stdio: 'pipe' });
try { try {
// start `now dev` detached in child_process // start `now dev` detached in child_process
dev.unref(); dev.unref();
let stderr = '';
dev.stderr.setEncoding('utf8');
await new Promise(resolve => {
dev.stderr.on('data', b => {
stderr += b;
if (
stderr.includes('Ignoring env var "1" because name is invalid') &&
stderr.includes(
'Ignoring build env var "_a" because name is invalid'
) &&
stderr.includes(
'Env var names must start with letters, and can only contain alphanumeric characters and underscores'
)
) {
resolve();
}
});
});
t.pass();
} finally {
dev.kill('SIGTERM');
}
});
test(
'[now dev] 00-list-directory',
testFixtureStdio('00-list-directory', async (t, port) => {
const result = await fetchWithRetry(`http://localhost:${port}`, 60); const result = await fetchWithRetry(`http://localhost:${port}`, 60);
const response = await result; const response = await result;
@@ -157,10 +201,8 @@ test('[now dev] 00-list-directory', async t => {
t.regex(body, /Files within/gm); t.regex(body, /Files within/gm);
t.regex(body, /test1.txt/gm); t.regex(body, /test1.txt/gm);
t.regex(body, /directory/gm); t.regex(body, /directory/gm);
} finally { })
dev.kill('SIGTERM'); );
}
});
test('[now dev] 01-node', async t => { test('[now dev] 01-node', async t => {
const directory = fixture('01-node'); const directory = fixture('01-node');
@@ -187,7 +229,7 @@ if (satisfies(process.version, '10.x')) {
test('[now dev] 02-angular-node', async t => { test('[now dev] 02-angular-node', async t => {
const directory = fixture('02-angular-node'); const directory = fixture('02-angular-node');
const { dev, port } = await testFixture(directory, { stdio: 'pipe' }, [ const { dev, port } = await testFixture(directory, { stdio: 'pipe' }, [
'--debug' '--debug',
]); ]);
let stderr = ''; let stderr = '';
@@ -273,14 +315,9 @@ test(
}) })
); );
test('[now dev] 07-hexo-node', async t => { test(
const directory = fixture('07-hexo-node'); '[now dev] 07-hexo-node',
const { dev, port } = await testFixture(directory); testFixtureStdio('07-hexo-node', async (t, port) => {
try {
// start `now dev` detached in child_process
dev.unref();
const result = await fetchWithRetry(`http://localhost:${port}`, 180); const result = await fetchWithRetry(`http://localhost:${port}`, 180);
const response = await result; const response = await result;
@@ -288,10 +325,8 @@ test('[now dev] 07-hexo-node', async t => {
const body = await response.text(); const body = await response.text();
t.regex(body, /Hexo \+ Node.js API/gm); t.regex(body, /Hexo \+ Node.js API/gm);
} finally { })
dev.kill('SIGTERM'); );
}
});
test( test(
'[now dev] 08-hugo', '[now dev] 08-hugo',
@@ -486,7 +521,7 @@ test('[now dev] double slashes redirect', async t => {
{ {
const res = await fetch(`http://localhost:${port}////?foo=bar`, { const res = await fetch(`http://localhost:${port}////?foo=bar`, {
redirect: 'manual' redirect: 'manual',
}); });
validateResponseHeaders(t, res); validateResponseHeaders(t, res);
@@ -500,7 +535,7 @@ test('[now dev] double slashes redirect', async t => {
{ {
const res = await fetch(`http://localhost:${port}///api////date.js`, { const res = await fetch(`http://localhost:${port}///api////date.js`, {
method: 'POST', method: 'POST',
redirect: 'manual' redirect: 'manual',
}); });
validateResponseHeaders(t, res); validateResponseHeaders(t, res);
@@ -667,7 +702,7 @@ test('[now dev] add a `package.json` to trigger `@now/static-build`', async t =>
const rnd = Math.random().toString(); const rnd = Math.random().toString();
const pkg = { const pkg = {
scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` } scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` },
}; };
await fs.writeFile( await fs.writeFile(
path.join(directory, 'package.json'), path.join(directory, 'package.json'),
@@ -691,7 +726,7 @@ test('[now dev] add a `package.json` to trigger `@now/static-build`', async t =>
test('[now dev] no build matches warning', async t => { test('[now dev] no build matches warning', async t => {
const directory = fixture('no-build-matches'); const directory = fixture('no-build-matches');
const { dev } = await testFixture(directory, { const { dev } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'] stdio: ['ignore', 'pipe', 'pipe'],
}); });
try { try {
@@ -746,7 +781,7 @@ if (satisfies(process.version, '^8.10.0 || ^10.13.0 || >=11.10.1')) {
test('[now dev] render warning for empty cwd dir', async t => { test('[now dev] render warning for empty cwd dir', async t => {
const directory = fixture('empty'); const directory = fixture('empty');
const { dev, port } = await testFixture(directory, { const { dev, port } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'] stdio: ['ignore', 'pipe', 'pipe'],
}); });
try { try {
@@ -775,3 +810,62 @@ test('[now dev] render warning for empty cwd dir', async t => {
dev.kill('SIGTERM'); dev.kill('SIGTERM');
} }
}); });
test('[now dev] do not rebuild for changes in the output directory', async t => {
const directory = fixture('output-is-source');
// Pack the builder and set it in the now.json
const builder = await getPackedBuilderPath('now-static-build');
await fs.writeFile(
path.join(directory, 'now.json'),
JSON.stringify({
builds: [
{
src: 'package.json',
use: `file://${builder}`,
config: { zeroConfig: true },
},
],
})
);
const { dev, port } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
dev.unref();
let stderr = [];
const start = Date.now();
dev.stderr.on('data', str => stderr.push(str));
while (stderr.join('').includes('Ready') === false) {
await sleep(ms('3s'));
if (Date.now() - start > ms('30s')) {
console.log('stderr:', stderr.join(''));
break;
}
}
const resp1 = await fetch(`http://localhost:${port}`);
const text1 = await resp1.text();
t.is(text1.trim(), 'hello first', stderr.join(''));
await fs.writeFile(
path.join(directory, 'public', 'index.html'),
'hello second'
);
await sleep(ms('3s'));
const resp2 = await fetch(`http://localhost:${port}`);
const text2 = await resp2.text();
t.is(text2.trim(), 'hello second', stderr.join(''));
} finally {
dev.kill('SIGTERM');
}
});

View File

@@ -1,6 +1,6 @@
// Native // Native
const { join } = require('path');
const { randomBytes } = require('crypto'); const { randomBytes } = require('crypto');
const { join, dirname } = require('path');
// Packages // Packages
const { imageSync: getImageFile } = require('qr-image'); const { imageSync: getImageFile } = require('qr-image');
@@ -85,14 +85,14 @@ const randomAliasSuffix = randomBytes(6).toString('hex');
const getRevertAliasConfigFile = () => { const getRevertAliasConfigFile = () => {
return JSON.stringify({ return JSON.stringify({
'version': 2, version: 2,
'name': `now-revert-alias-${randomAliasSuffix}`, name: `now-revert-alias-${randomAliasSuffix}`,
'builds': [ builds: [
{ {
'src': '*.json', src: '*.json',
'use': '@now/static' use: '@now/static',
} },
] ],
}); });
}; };
@@ -103,14 +103,14 @@ module.exports = async session => {
'package.json': getPackageFile(session), 'package.json': getPackageFile(session),
'now.json': getConfigFile(false), 'now.json': getConfigFile(false),
'first.png': getImageFile(session, { 'first.png': getImageFile(session, {
size: 30 size: 30,
}), }),
'second.png': getImageFile(session, { 'second.png': getImageFile(session, {
size: 20 size: 20,
}), }),
'now.json-builds': getConfigFile(true), 'now.json-builds': getConfigFile(true),
'index.html': getIndexHTMLFile(session), 'index.html': getIndexHTMLFile(session),
'contact.php': getContactFile(session) 'contact.php': getContactFile(session),
}; };
const spec = { const spec = {
@@ -120,26 +120,24 @@ module.exports = async session => {
'static-single-file': ['first.png', 'now.json'], 'static-single-file': ['first.png', 'now.json'],
'static-multiple-files': ['first.png', 'second.png', 'now.json'], 'static-multiple-files': ['first.png', 'second.png', 'now.json'],
'single-dotfile': { 'single-dotfile': {
'.testing': 'i am a dotfile' '.testing': 'i am a dotfile',
}, },
'config-alias-property': { 'config-alias-property': {
'now.json': 'now.json':
'{ "alias": "test.now.sh", "builds": [ { "src": "*.html", "use": "@now/static" } ] }', '{ "alias": "test.now.sh", "builds": [ { "src": "*.html", "use": "@now/static" } ] }',
'index.html': '<span>test alias</span' 'index.html': '<span>test alias</span',
}, },
'config-scope-property-email': { 'config-scope-property-email': {
'now.json': 'now.json': `{ "scope": "${session}@zeit.pub", "builds": [ { "src": "*.html", "use": "@now/static" } ], "version": 2 }`,
`{ "scope": "${session}@zeit.pub", "builds": [ { "src": "*.html", "use": "@now/static" } ], "version": 2 }`, 'index.html': '<span>test scope email</span',
'index.html': '<span>test scope email</span'
}, },
'config-scope-property-username': { 'config-scope-property-username': {
'now.json': 'now.json': `{ "scope": "${session}", "builds": [ { "src": "*.html", "use": "@now/static" } ] }`,
`{ "scope": "${session}", "builds": [ { "src": "*.html", "use": "@now/static" } ] }`, 'index.html': '<span>test scope username</span',
'index.html': '<span>test scope username</span'
}, },
'builds-wrong': { 'builds-wrong': {
'now.json': '{"builder": 1, "type": "static"}', 'now.json': '{"builder": 1, "type": "static"}',
'index.html': '<span>test</span' 'index.html': '<span>test</span',
}, },
'builds-no-list': { 'builds-no-list': {
'now.json': `{ 'now.json': `{
@@ -161,75 +159,222 @@ module.exports = async session => {
FROM alpine FROM alpine
RUN mkdir /public RUN mkdir /public
RUN echo hello > /public/index.html RUN echo hello > /public/index.html
` `,
}, },
'build-env': { 'build-env': {
'now.json': JSON.stringify({ 'now.json': JSON.stringify({
version: 1, version: 1,
type: 'static', type: 'static',
build: { build: {
env: { FOO: 'bar' } env: { FOO: 'bar' },
} },
}), }),
Dockerfile: ` Dockerfile: `
FROM alpine FROM alpine
ARG FOO ARG FOO
RUN mkdir /public RUN mkdir /public
RUN echo $FOO > /public/index.html RUN echo $FOO > /public/index.html
` `,
}, },
'build-env-arg': { 'build-env-arg': {
'now.json': JSON.stringify({ 'now.json': JSON.stringify({
version: 1, version: 1,
type: 'static' type: 'static',
}), }),
Dockerfile: ` Dockerfile: `
FROM alpine FROM alpine
ARG NONCE ARG NONCE
RUN mkdir /public RUN mkdir /public
RUN echo $NONCE > /public/index.html RUN echo $NONCE > /public/index.html
` `,
},
'build-env-debug': {
'now.json':
'{ "builds": [ { "src": "index.js", "use": "@now/node" } ], "version": 2 }',
'package.json': `
{
"scripts": {
"now-build": "node now-build.js"
}
}
`,
'now-build.js': `
const fs = require('fs');
fs.writeFileSync(
'index.js',
fs
.readFileSync('index.js', 'utf8')
.replace('BUILD_ENV_DEBUG', process.env.NOW_BUILDER_DEBUG),
);
`,
'index.js': `
module.exports = (req, res) => {
res.status(200).send('BUILD_ENV_DEBUG')
}
`,
}, },
'now-revert-alias-1': { 'now-revert-alias-1': {
'index.json': JSON.stringify({ name: 'now-revert-alias-1' }), 'index.json': JSON.stringify({ name: 'now-revert-alias-1' }),
'now.json': getRevertAliasConfigFile() 'now.json': getRevertAliasConfigFile(),
}, },
'now-revert-alias-2': { 'now-revert-alias-2': {
'index.json': JSON.stringify({ name: 'now-revert-alias-2' }), 'index.json': JSON.stringify({ name: 'now-revert-alias-2' }),
'now.json': getRevertAliasConfigFile() 'now.json': getRevertAliasConfigFile(),
}, },
'now-dev-fail-dev-script': { 'now-dev-fail-dev-script': {
'package.json': JSON.stringify({ 'package.json': JSON.stringify(
scripts: { {
dev: 'now dev' scripts: {
} dev: 'now dev',
}, null, 2) },
},
null,
2
),
}, },
'v1-warning-link': { 'v1-warning-link': {
'now.json': JSON.stringify({ 'now.json': JSON.stringify({
version: 1 version: 1,
}), }),
'package.json': JSON.stringify({ 'package.json': JSON.stringify({
dependencies: { dependencies: {
next: '9.0.0' next: '9.0.0',
} },
}) }),
}, },
'static-deployment': { 'static-deployment': {
'index.txt': 'Hello World' 'index.txt': 'Hello World',
},
nowignore: {
'index.txt': 'Hello World',
'ignore.txt': 'Should be ignored',
'.nowignore': 'ignore.txt',
},
'nowignore-allowlist': {
'index.txt': 'Hello World',
'ignore.txt': 'Should be ignored',
'.nowignore': '*\n!index.txt',
}, },
'failing-build': { 'failing-build': {
'package.json': JSON.stringify({ 'package.json': JSON.stringify({
scripts: { scripts: {
build: 'echo hello && exit 1' build: 'echo hello && exit 1',
} },
}) }),
} },
'failing-alias': {
'now.json': JSON.stringify(
Object.assign(JSON.parse(getConfigFile(true)), { alias: 'zeit.co' })
),
},
'local-config-cloud-v1': {
'.gitignore': '*.html',
'index.js': `
const { createServer } = require('http');
const { readFileSync } = require('fs');
const svr = createServer((req, res) => {
const { url = '/' } = req;
const file = '.' + url;
console.log('reading file ' + file);
try {
let contents = readFileSync(file, 'utf8');
res.end(contents || '');
} catch (e) {
res.statusCode = 404;
res.end('Not found');
}
});
svr.listen(3000);`,
'main.html': '<h1>hello main</h1>',
'test.html': '<h1>hello test</h1>',
'folder/file1.txt': 'file1',
'folder/sub/file2.txt': 'file2',
Dockerfile: `FROM mhart/alpine-node:latest
LABEL name "now-cli-dockerfile-${session}"
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN yarn
EXPOSE 3000
CMD ["node", "index.js"]`,
'now.json': JSON.stringify({
version: 1,
type: 'docker',
features: {
cloud: 'v1',
},
files: ['.gitignore', 'folder', 'index.js', 'main.html'],
}),
'now-test.json': JSON.stringify({
version: 1,
type: 'docker',
features: {
cloud: 'v1',
},
files: ['.gitignore', 'folder', 'index.js', 'test.html'],
}),
},
'local-config-v2': {
[`main-${session}.html`]: '<h1>hello main</h1>',
[`test-${session}.html`]: '<h1>hello test</h1>',
'now.json': JSON.stringify({
version: 2,
builds: [{ src: `main-${session}.html`, use: '@now/static' }],
routes: [{ src: '/another-main', dest: `/main-${session}.html` }],
}),
'now-test.json': JSON.stringify({
version: 2,
builds: [{ src: `test-${session}.html`, use: '@now/static' }],
routes: [{ src: '/another-test', dest: `/test-${session}.html` }],
}),
},
'alias-rules': {
'rules.json': JSON.stringify({
rules: [
// for example:
// { pathname: '/', dest: '' },
// { pathname: '/', dest: '', method: 'GET' }
// Will be generated by the actual test
],
}),
'invalid-rules.json': JSON.stringify({
what: { what: 0 },
}),
'invalid-type-rules.json': JSON.stringify({
rules: { what: 0 },
}),
'invalid-json-rules.json': '==ok',
},
'zero-config-next-js': {
'pages/index.js':
'export default () => <div><h1>Now CLI test</h1><p>Zero-config + Next.js</p></div>',
'package.json': JSON.stringify({
name: 'zero-config-next-js-test',
scripts: {
dev: 'next',
start: 'next start',
build: 'next build',
},
dependencies: {
next: 'latest',
react: 'latest',
'react-dom': 'latest',
},
}),
},
}; };
for (const typeName of Object.keys(spec)) { for (const typeName of Object.keys(spec)) {
const needed = spec[typeName]; const needed = spec[typeName];
const directory = join(__dirname, '..', 'fixtures', 'integration', typeName); const directory = join(
__dirname,
'..',
'fixtures',
'integration',
typeName
);
await mkdirp(directory); await mkdirp(directory);
if (Array.isArray(needed)) { if (Array.isArray(needed)) {
@@ -237,6 +382,7 @@ RUN echo $NONCE > /public/index.html
for (const name of needed) { for (const name of needed) {
const file = join(directory, name); const file = join(directory, name);
const content = files[name]; const content = files[name];
await mkdirp(dirname(file));
await writeFile(file.replace('-builds', ''), content); await writeFile(file.replace('-builds', ''), content);
} }
} else { } else {
@@ -245,6 +391,7 @@ RUN echo $NONCE > /public/index.html
for (const name of names) { for (const name of names) {
const file = join(directory, name); const file = join(directory, name);
const content = needed[name]; const content = needed[name];
await mkdirp(dirname(file));
await writeFile(file.replace('-builds', ''), content); await writeFile(file.replace('-builds', ''), content);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{ {
"name": "now-client", "name": "now-client",
"version": "5.1.2", "version": "5.2.0",
"main": "lib/index.js", "main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "ncc build ./src/index.ts -o ./lib --source-map", "build": "tsc",
"prepare": "npm run build", "prepare": "npm run build",
"test-integration-once": "jest --verbose --forceExit", "test-integration-once": "jest --verbose --forceExit",
"test-lint": "eslint . --ext .js,.ts --ignore-path ../../.eslintignore" "test-lint": "eslint . --ext .js,.ts --ignore-path ../../.eslintignore"
@@ -34,11 +35,10 @@
"@zeit/fetch": "5.1.0", "@zeit/fetch": "5.1.0",
"async-retry": "1.2.3", "async-retry": "1.2.3",
"async-sema": "3.0.0", "async-sema": "3.0.0",
"fetch-h2": "2.2.0",
"fs-extra": "8.0.1", "fs-extra": "8.0.1",
"ignore": "4.0.6",
"ms": "2.1.2", "ms": "2.1.2",
"node-fetch": "2.6.0", "node-fetch": "2.6.0",
"now-client": "4.1.2",
"querystring": "^0.2.0", "querystring": "^0.2.0",
"recursive-readdir": "2.2.2", "recursive-readdir": "2.2.2",
"sleep-promise": "8.0.1" "sleep-promise": "8.0.1"

View File

@@ -1,81 +1,199 @@
import { readdir as readRootFolder, lstatSync } from 'fs-extra' import { readdir as readRootFolder, lstatSync } from 'fs-extra';
import readdir from 'recursive-readdir' import readdir from 'recursive-readdir';
import hashes, { mapToObject } from './utils/hashes' import { relative, join } from 'path';
import uploadAndDeploy from './upload' import hashes, { mapToObject } from './utils/hashes';
import { getNowIgnore } from './utils' import uploadAndDeploy from './upload';
import { DeploymentError } from './errors' import { getNowIgnore, createDebug, parseNowJSON } from './utils';
import { DeploymentError } from './errors';
import {
CreateDeploymentFunction,
DeploymentOptions,
NowJsonOptions,
} from './types';
export { EVENTS } from './utils' export { EVENTS } from './utils';
export default function buildCreateDeployment(version: number): CreateDeploymentFunction { export default function buildCreateDeployment(
version: number
): CreateDeploymentFunction {
return async function* createDeployment( return async function* createDeployment(
path: string | string[], path: string | string[],
options: DeploymentOptions = {} options: DeploymentOptions = {},
nowConfig?: NowJsonOptions
): AsyncIterableIterator<any> { ): AsyncIterableIterator<any> {
const debug = createDebug(options.debug);
const cwd = process.cwd();
debug('Creating deployment...');
if (typeof path !== 'string' && !Array.isArray(path)) { if (typeof path !== 'string' && !Array.isArray(path)) {
debug(
`Error: 'path' is expected to be a string or an array. Received ${typeof path}`
);
throw new DeploymentError({ throw new DeploymentError({
code: 'missing_path', code: 'missing_path',
message: 'Path not provided' message: 'Path not provided',
}) });
} }
if (typeof options.token !== 'string') { if (typeof options.token !== 'string') {
debug(
`Error: 'token' is expected to be a string. Received ${typeof options.token}`
);
throw new DeploymentError({ throw new DeploymentError({
code: 'token_not_provided', code: 'token_not_provided',
message: 'Options object must include a `token`' message: 'Options object must include a `token`',
}) });
} }
const isDirectory = !Array.isArray(path) && lstatSync(path).isDirectory() const isDirectory = !Array.isArray(path) && lstatSync(path).isDirectory();
// Get .nowignore let rootFiles: string[];
let rootFiles
if (isDirectory && !Array.isArray(path)) { if (isDirectory && !Array.isArray(path)) {
rootFiles = await readRootFolder(path) debug(`Provided 'path' is a directory. Reading subpaths... `);
rootFiles = await readRootFolder(path);
debug(`Read ${rootFiles.length} subpaths`);
} else if (Array.isArray(path)) { } else if (Array.isArray(path)) {
rootFiles = path debug(`Provided 'path' is an array of file paths`);
rootFiles = path;
} else { } else {
rootFiles = [path] debug(`Provided 'path' is a single file`);
rootFiles = [path];
} }
let ignores: string[] = await getNowIgnore(rootFiles, path) // Get .nowignore
let ig = await getNowIgnore(path);
let fileList debug(`Found ${ig.ignores.length} rules in .nowignore`);
let fileList: string[];
debug('Building file tree...');
if (isDirectory && !Array.isArray(path)) { if (isDirectory && !Array.isArray(path)) {
// Directory path // Directory path
fileList = await readdir(path, ignores) const dirContents = await readdir(path);
const relativeFileList = dirContents.map(filePath =>
relative(process.cwd(), filePath)
);
fileList = ig
.filter(relativeFileList)
.map((relativePath: string) => join(process.cwd(), relativePath));
debug(`Read ${fileList.length} files in the specified directory`);
} else if (Array.isArray(path)) { } else if (Array.isArray(path)) {
// Array of file paths // Array of file paths
fileList = path fileList = path;
debug(`Assigned ${fileList.length} files provided explicitly`);
} else { } else {
// Single file // Single file
fileList = [path] fileList = [path];
debug(`Deploying the provided path as single file`);
} }
const files = await hashes(fileList) if (!nowConfig) {
// If the user did not provide a nowConfig,
// then use the now.json file in the root.
const fileName = 'now.json';
const absolutePath = fileList.find(f => relative(cwd, f) === fileName);
debug(absolutePath ? `Found ${fileName}` : `Missing ${fileName}`);
nowConfig = await parseNowJSON(absolutePath);
}
yield { type: 'hashes-calculated', payload: mapToObject(files) } if (
version === 1 &&
nowConfig &&
Array.isArray(nowConfig.files) &&
nowConfig.files.length > 0
) {
// See the docs: https://zeit.co/docs/v1/features/configuration/#files-(array)
debug('Filtering file list based on `files` key in now.json');
const allowedFiles = new Set<string>(['Dockerfile']);
const allowedDirs = new Set<string>();
nowConfig.files.forEach(relPath => {
if (lstatSync(relPath).isDirectory()) {
allowedDirs.add(relPath);
} else {
allowedFiles.add(relPath);
}
});
fileList = fileList.filter(absPath => {
const relPath = relative(cwd, absPath);
if (allowedFiles.has(relPath)) {
return true;
}
for (let dir of allowedDirs) {
if (relPath.startsWith(dir + '/')) {
return true;
}
}
return false;
});
debug(`Found ${fileList.length} files: ${JSON.stringify(fileList)}`);
}
const { token, teamId, force, defaultName, ...metadata } = options // This is a useful warning because it prevents people
// from getting confused about a deployment that renders 404.
if (
fileList.length === 0 ||
fileList.every((item): boolean => {
if (!item) {
return true;
}
metadata.version = version const segments = item.split('/');
return segments[segments.length - 1].startsWith('.');
})
) {
debug(
`Deployment path has no files (or only dotfiles). Yielding a warning event`
);
yield {
type: 'warning',
payload:
'There are no files (or only files starting with a dot) inside your deployment.',
};
}
const files = await hashes(fileList);
debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`);
yield { type: 'hashes-calculated', payload: mapToObject(files) };
const {
token,
teamId,
force,
defaultName,
debug: debug_,
...metadata
} = options;
debug(`Setting platform version to ${version}`);
metadata.version = version;
const deploymentOpts = { const deploymentOpts = {
debug: debug_,
totalFiles: files.size, totalFiles: files.size,
nowConfig,
token, token,
isDirectory, isDirectory,
path, path,
teamId, teamId,
force, force,
defaultName, defaultName,
metadata metadata,
} };
debug(`Creating the deployment and starting upload...`);
for await (const event of uploadAndDeploy(files, deploymentOpts)) { for await (const event of uploadAndDeploy(files, deploymentOpts)) {
yield event debug(`Yielding a '${event.type}' event`);
yield event;
} }
} };
} }

View File

@@ -1,13 +1,14 @@
import { DeploymentFile } from './utils/hashes' import { DeploymentFile } from './utils/hashes';
import { import {
parseNowJSON,
fetch, fetch,
API_DEPLOYMENTS, API_DEPLOYMENTS,
prepareFiles, prepareFiles,
API_DEPLOYMENTS_LEGACY API_DEPLOYMENTS_LEGACY,
} from './utils' createDebug,
import checkDeploymentStatus from './deployment-status' } from './utils';
import { generateQueryString } from './utils/query-string' import checkDeploymentStatus from './deployment-status';
import { generateQueryString } from './utils/query-string';
import { Deployment, DeploymentOptions, NowJsonOptions } from './types';
export interface Options { export interface Options {
metadata: DeploymentOptions; metadata: DeploymentOptions;
@@ -19,17 +20,22 @@ export interface Options {
isDirectory?: boolean; isDirectory?: boolean;
defaultName?: string; defaultName?: string;
preflight?: boolean; preflight?: boolean;
debug?: boolean;
nowConfig?: NowJsonOptions;
} }
async function* createDeployment( async function* createDeployment(
metadata: DeploymentOptions, metadata: DeploymentOptions,
files: Map<string, DeploymentFile>, files: Map<string, DeploymentFile>,
options: Options options: Options,
debug: Function
): AsyncIterableIterator<{ type: string; payload: any }> { ): AsyncIterableIterator<{ type: string; payload: any }> {
const preparedFiles = prepareFiles(files, options) const preparedFiles = prepareFiles(files, options);
let apiDeployments = let apiDeployments =
metadata.version === 2 ? API_DEPLOYMENTS : API_DEPLOYMENTS_LEGACY metadata.version === 2 ? API_DEPLOYMENTS : API_DEPLOYMENTS_LEGACY;
debug('Sending deployment creation API request');
try { try {
const dpl = await fetch( const dpl = await fetch(
`${apiDeployments}${generateQueryString(options)}`, `${apiDeployments}${generateQueryString(options)}`,
@@ -38,113 +44,174 @@ async function* createDeployment(
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${options.token}` Authorization: `Bearer ${options.token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
...metadata, ...metadata,
files: preparedFiles files: preparedFiles,
}) }),
} }
) );
const json = await dpl.json() const json = await dpl.json();
debug('Deployment response:', JSON.stringify(json));
if (!dpl.ok || json.error) { if (!dpl.ok || json.error) {
debug('Error: Deployment request status is', dpl.status);
// Return error object // Return error object
return yield { type: 'error', payload: json.error ? { ...json.error, status: dpl.status } : { ...json, status: dpl.status } } return yield {
type: 'error',
payload: json.error
? { ...json.error, status: dpl.status }
: { ...json, status: dpl.status },
};
} }
yield { type: 'created', payload: json } for (const [name, value] of dpl.headers.entries()) {
if (name.startsWith('x-now-warning-')) {
debug('Deployment created with a warning:', value);
yield { type: 'warning', payload: value };
}
if (name.startsWith('x-now-notice-')) {
debug('Deployment created with a notice:', value);
yield { type: 'notice', payload: value };
}
}
yield { type: 'created', payload: json };
} catch (e) { } catch (e) {
return yield { type: 'error', payload: e } return yield { type: 'error', payload: e };
} }
} }
const getDefaultName = ( const getDefaultName = (
path: string | string[] | undefined, path: string | string[] | undefined,
isDirectory: boolean | undefined, isDirectory: boolean | undefined,
files: Map<string, DeploymentFile> files: Map<string, DeploymentFile>,
debug: Function
): string => { ): string => {
if (isDirectory && typeof path === 'string') { if (isDirectory && typeof path === 'string') {
const segments = path.split('/') debug('Provided path is a directory. Using last segment as default name');
const segments = path.split('/');
return segments[segments.length - 1] return segments[segments.length - 1];
} else { } else {
const filePath = Array.from(files.values())[0].names[0] debug(
const segments = filePath.split('/') 'Provided path is not a directory. Using last segment of the first file as default name'
);
const filePath = Array.from(files.values())[0].names[0];
const segments = filePath.split('/');
return segments[segments.length - 1] return segments[segments.length - 1];
} }
} };
export default async function* deploy( export default async function* deploy(
files: Map<string, DeploymentFile>, files: Map<string, DeploymentFile>,
options: Options options: Options
): AsyncIterableIterator<{ type: string; payload: any }> { ): AsyncIterableIterator<{ type: string; payload: any }> {
const nowJson: DeploymentFile | undefined = Array.from(files.values()).find( const debug = createDebug(options.debug);
(file: DeploymentFile): boolean => { const nowJsonMetadata = options.nowConfig || {};
return Boolean( delete nowJsonMetadata.github;
file.names.find((name: string): boolean => name.includes('now.json')) delete nowJsonMetadata.scope;
)
}
)
const nowJsonMetadata: NowJsonOptions = parseNowJSON(nowJson)
const meta = options.metadata || {} const meta = options.metadata || {};
const metadata = { ...nowJsonMetadata, ...meta } const metadata = { ...nowJsonMetadata, ...meta };
// Check if we should default to a static deployment // Check if we should default to a static deployment
if (!metadata.version && !metadata.name) { if (!metadata.version && !metadata.name) {
metadata.version = 2 metadata.version = 2;
metadata.name = metadata.name =
options.totalFiles === 1 options.totalFiles === 1
? 'file' ? 'file'
: getDefaultName(options.path, options.isDirectory, files) : getDefaultName(options.path, options.isDirectory, files, debug);
if (metadata.name === 'file') {
debug('Setting deployment name to "file" for single-file deployment');
}
}
if (options.totalFiles === 1 && !metadata.builds && !metadata.routes) {
debug(`Assigning '/' route for single file deployment`);
const filePath = Array.from(files.values())[0].names[0];
const segments = filePath.split('/');
metadata.routes = [
{
src: '/',
dest: `/${segments[segments.length - 1]}`,
},
];
} }
if (!metadata.name) { if (!metadata.name) {
metadata.name = metadata.name =
options.defaultName || options.defaultName ||
getDefaultName(options.path, options.isDirectory, files) getDefaultName(options.path, options.isDirectory, files, debug);
debug('No name provided. Defaulting to', metadata.name);
} }
if (metadata.version === 1 && !metadata.deploymentType) { if (metadata.version === 1 && !metadata.deploymentType) {
metadata.deploymentType = nowJsonMetadata.type debug(`Setting 'type' for 1.0 deployment to '${nowJsonMetadata.type}'`);
metadata.deploymentType = nowJsonMetadata.type;
} }
delete metadata.github if (metadata.version === 1) {
delete metadata.scope debug(`Writing 'config' values for 1.0 deployment`);
const nowConfig = { ...nowJsonMetadata };
delete nowConfig.version;
let deployment: Deployment | undefined metadata.config = {
...nowConfig,
...metadata.config,
};
}
let deployment: Deployment | undefined;
try { try {
for await (const event of createDeployment(metadata, files, options)) { debug('Creating deployment');
for await (const event of createDeployment(
metadata,
files,
options,
debug
)) {
if (event.type === 'created') { if (event.type === 'created') {
deployment = event.payload debug('Deployment created');
deployment = event.payload;
} }
yield event yield event;
} }
} catch (e) { } catch (e) {
return yield { type: 'error', payload: e } debug('An unexpected error occurred when creating the deployment');
return yield { type: 'error', payload: e };
} }
if (deployment) { if (deployment) {
if (deployment.readyState === 'READY') { if (deployment.readyState === 'READY') {
return yield { type: 'ready', payload: deployment } debug('Deployment is READY. Not performing additional polling');
return yield { type: 'ready', payload: deployment };
} }
try { try {
debug('Waiting for deployment to be ready...');
for await (const event of checkDeploymentStatus( for await (const event of checkDeploymentStatus(
deployment, deployment,
options.token, options.token,
metadata.version, metadata.version,
options.teamId options.teamId,
debug
)) { )) {
yield event yield event;
} }
} catch (e) { } catch (e) {
return yield { type: 'error', payload: e } debug(
'An unexpected error occurred while waiting for deployment to be ready'
);
return yield { type: 'error', payload: e };
} }
} }
} }

View File

@@ -1,7 +1,8 @@
import sleep from 'sleep-promise' import sleep from 'sleep-promise';
import ms from 'ms' import ms from 'ms';
import { fetch, API_DEPLOYMENTS, API_DEPLOYMENTS_LEGACY } from './utils' import { fetch, API_DEPLOYMENTS, API_DEPLOYMENTS_LEGACY } from './utils';
import { isDone, isReady, isFailed } from './utils/ready-state' import { isDone, isReady, isFailed } from './utils/ready-state';
import { Deployment, DeploymentBuild } from './types';
interface DeploymentStatus { interface DeploymentStatus {
type: string; type: string;
@@ -13,19 +14,24 @@ export default async function* checkDeploymentStatus(
deployment: Deployment, deployment: Deployment,
token: string, token: string,
version: number | undefined, version: number | undefined,
teamId?: string teamId: string | undefined,
debug: Function
): AsyncIterableIterator<DeploymentStatus> { ): AsyncIterableIterator<DeploymentStatus> {
let deploymentState = deployment; let deploymentState = deployment;
let allBuildsCompleted = false; let allBuildsCompleted = false;
const buildsState: { [key: string]: DeploymentBuild } = {}; const buildsState: { [key: string]: DeploymentBuild } = {};
let apiDeployments = version === 2 ? API_DEPLOYMENTS : API_DEPLOYMENTS_LEGACY; let apiDeployments = version === 2 ? API_DEPLOYMENTS : API_DEPLOYMENTS_LEGACY;
debug(`Using ${version ? `${version}.0` : '2.0'} API for status checks`);
// If the deployment is ready, we don't want any of this to run // If the deployment is ready, we don't want any of this to run
if (isDone(deploymentState)) { if (isDone(deploymentState)) {
debug(`Deployment is already READY. Not running status checks`);
return; return;
} }
// Build polling // Build polling
debug('Waiting for builds and the deployment to complete...');
while (true) { while (true) {
if (!allBuildsCompleted) { if (!allBuildsCompleted) {
const buildsData = await fetch( const buildsData = await fetch(
@@ -34,6 +40,7 @@ export default async function* checkDeploymentStatus(
}`, }`,
token token
); );
const data = await buildsData.json(); const data = await buildsData.json();
const { builds = [] } = data; const { builds = [] } = data;
@@ -41,10 +48,14 @@ export default async function* checkDeploymentStatus(
const prevState = buildsState[build.id]; const prevState = buildsState[build.id];
if (!prevState || prevState.readyState !== build.readyState) { if (!prevState || prevState.readyState !== build.readyState) {
debug(
`Build state for '${build.entrypoint}' changed to ${build.readyState}`
);
yield { type: 'build-state-changed', payload: build }; yield { type: 'build-state-changed', payload: build };
} }
if (build.readyState.includes('ERROR')) { if (build.readyState.includes('ERROR')) {
debug(`Build '${build.entrypoint}' has errorred`);
return yield { type: 'error', payload: build }; return yield { type: 'error', payload: build };
} }
@@ -54,6 +65,7 @@ export default async function* checkDeploymentStatus(
const readyBuilds = builds.filter((b: DeploymentBuild) => isDone(b)); const readyBuilds = builds.filter((b: DeploymentBuild) => isDone(b));
if (readyBuilds.length === builds.length) { if (readyBuilds.length === builds.length) {
debug('All builds completed');
allBuildsCompleted = true; allBuildsCompleted = true;
yield { type: 'all-builds-completed', payload: readyBuilds }; yield { type: 'all-builds-completed', payload: readyBuilds };
} }
@@ -68,15 +80,21 @@ export default async function* checkDeploymentStatus(
const deploymentUpdate = await deploymentData.json(); const deploymentUpdate = await deploymentData.json();
if (deploymentUpdate.error) { if (deploymentUpdate.error) {
return yield { type: 'error', payload: deploymentUpdate.error } debug('Deployment status check has errorred');
return yield { type: 'error', payload: deploymentUpdate.error };
} }
if (isReady(deploymentUpdate)) { if (isReady(deploymentUpdate)) {
debug('Deployment state changed to READY');
return yield { type: 'ready', payload: deploymentUpdate }; return yield { type: 'ready', payload: deploymentUpdate };
} }
if (isFailed(deploymentUpdate)) { if (isFailed(deploymentUpdate)) {
return yield { type: 'error', payload: deploymentUpdate.error || deploymentUpdate }; debug('Deployment has failed');
return yield {
type: 'error',
payload: deploymentUpdate.error || deploymentUpdate,
};
} }
} }

View File

@@ -1,8 +1,8 @@
export class DeploymentError extends Error { export class DeploymentError extends Error {
constructor(err: { code: string; message: string }) { constructor(err: { code: string; message: string }) {
super(err.message) super(err.message);
this.code = err.code this.code = err.code;
this.name = 'DeploymentError' this.name = 'DeploymentError';
} }
code: string; code: string;

View File

@@ -1,11 +1,12 @@
// Polyfill Node 8 and below // Polyfill Node 8 and below
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#the-for-await-of-statement // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#the-for-await-of-statement
if (!Symbol.asyncIterator) { if (!Symbol.asyncIterator) {
(Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator') (Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator');
} }
import buildCreateDeployment from './create-deployment' import buildCreateDeployment from './create-deployment';
export const createDeployment = buildCreateDeployment(2) export const createDeployment = buildCreateDeployment(2);
export const createLegacyDeployment = buildCreateDeployment(1) export const createLegacyDeployment = buildCreateDeployment(1);
export * from './errors' export * from './errors';
export * from './types';

View File

@@ -1,4 +1,4 @@
declare interface Route { export interface Route {
src: string; src: string;
dest: string; dest: string;
headers?: { headers?: {
@@ -8,12 +8,12 @@ declare interface Route {
methods?: string[]; methods?: string[];
} }
declare interface Build { export interface Build {
src: string; src: string;
use: string; use: string;
} }
declare interface Deployment { export interface Deployment {
id: string; id: string;
deploymentId?: string; deploymentId?: string;
url: string; url: string;
@@ -29,19 +29,19 @@ declare interface Deployment {
public: boolean; public: boolean;
ownerId: string; ownerId: string;
readyState: readyState:
| 'INITIALIZING' | 'INITIALIZING'
| 'ANALYZING' | 'ANALYZING'
| 'BUILDING' | 'BUILDING'
| 'DEPLOYING' | 'DEPLOYING'
| 'READY' | 'READY'
| 'ERROR'; | 'ERROR';
state?: state?:
| 'INITIALIZING' | 'INITIALIZING'
| 'ANALYZING' | 'ANALYZING'
| 'BUILDING' | 'BUILDING'
| 'DEPLOYING' | 'DEPLOYING'
| 'READY' | 'READY'
| 'ERROR'; | 'ERROR';
createdAt: string; createdAt: string;
createdIn: string; createdIn: string;
env: { env: {
@@ -56,30 +56,37 @@ declare interface Deployment {
alias: string[]; alias: string[];
} }
declare interface DeploymentBuild { export interface DeploymentBuild {
id: string; id: string;
use: string; use: string;
createdIn: string; createdIn: string;
deployedTo: string; deployedTo: string;
readyState: readyState:
| 'INITIALIZING' | 'INITIALIZING'
| 'ANALYZING' | 'ANALYZING'
| 'BUILDING' | 'BUILDING'
| 'DEPLOYING' | 'DEPLOYING'
| 'READY' | 'READY'
| 'ERROR'; | 'ERROR';
state?: state?:
| 'INITIALIZING' | 'INITIALIZING'
| 'ANALYZING' | 'ANALYZING'
| 'BUILDING' | 'BUILDING'
| 'DEPLOYING' | 'DEPLOYING'
| 'READY' | 'READY'
| 'ERROR'; | 'ERROR';
readyStateAt: string; readyStateAt: string;
path: string; path: string;
} }
declare interface DeploymentOptions { export interface DeploymentGithubData {
enabled: boolean;
autoAlias: boolean;
silent: boolean;
autoJobCancelation: boolean;
}
export interface DeploymentOptions {
version?: number; version?: number;
regions?: string[]; regions?: string[];
routes?: Route[]; routes?: Route[];
@@ -100,7 +107,7 @@ declare interface DeploymentOptions {
defaultName?: string; defaultName?: string;
isDirectory?: boolean; isDirectory?: boolean;
path?: string | string[]; path?: string | string[];
github?: any; github?: DeploymentGithubData;
scope?: string; scope?: string;
public?: boolean; public?: boolean;
forceNew?: boolean; forceNew?: boolean;
@@ -109,11 +116,19 @@ declare interface DeploymentOptions {
engines?: { [key: string]: string }; engines?: { [key: string]: string };
sessionAffinity?: 'ip' | 'random'; sessionAffinity?: 'ip' | 'random';
config?: { [key: string]: any }; config?: { [key: string]: any };
debug?: boolean;
} }
declare interface NowJsonOptions { export interface NowJsonOptions {
github?: DeploymentGithubData;
scope?: string;
type?: 'NPM' | 'STATIC' | 'DOCKER'; type?: 'NPM' | 'STATIC' | 'DOCKER';
version?: number; version?: number;
files?: string[];
} }
declare type CreateDeploymentFunction = (path: string | string[], options?: DeploymentOptions) => AsyncIterableIterator<any>; export type CreateDeploymentFunction = (
path: string | string[],
options?: DeploymentOptions,
nowConfig?: NowJsonOptions
) => AsyncIterableIterator<any>;

View File

@@ -1,148 +1,198 @@
import { createReadStream } from 'fs' import { createReadStream } from 'fs';
import retry from 'async-retry' import { Agent } from 'https';
import { DeploymentFile } from './utils/hashes' import retry from 'async-retry';
import { fetch, API_FILES } from './utils' import { Sema } from 'async-sema';
import { DeploymentError } from '.' import { DeploymentFile } from './utils/hashes';
import deploy, { Options } from './deploy' import { fetch, API_FILES, createDebug } from './utils';
import { DeploymentError } from '.';
import deploy, { Options } from './deploy';
export default async function* upload(files: Map<string, DeploymentFile>, options: Options): AsyncIterableIterator<any> { const isClientNetworkError = (err: Error | DeploymentError) => {
const { token, teamId } = options if (err.message) {
// These are common network errors that may happen occasionally and we should retry if we encounter these
if (!files && !token && !teamId) { return (
return err.message.includes('ETIMEDOUT') ||
err.message.includes('ECONNREFUSED') ||
err.message.includes('ENOTFOUND') ||
err.message.includes('ECONNRESET') ||
err.message.includes('EAI_FAIL') ||
err.message.includes('socket hang up') ||
err.message.includes('network socket disconnected')
);
} }
let missingFiles = [] return false;
};
for await(const event of deploy(files, options)) { export default async function* upload(
files: Map<string, DeploymentFile>,
options: Options,
): AsyncIterableIterator<any> {
const { token, teamId, debug: isDebug } = options;
const debug = createDebug(isDebug);
if (!files && !token && !teamId) {
debug(`Neither 'files', 'token' nor 'teamId are present. Exiting`);
return;
}
let missingFiles = [];
debug('Determining necessary files for upload...');
for await (const event of deploy(files, options)) {
if (event.type === 'error') { if (event.type === 'error') {
if (event.payload.code === 'missing_files') { if (event.payload.code === 'missing_files') {
missingFiles = event.payload.missing missingFiles = event.payload.missing;
debug(`${missingFiles.length} files are required to upload`);
} else { } else {
return yield event return yield event;
} }
} else { } else {
// If the deployment has succeeded here, don't continue // If the deployment has succeeded here, don't continue
if (event.type === 'ready') { if (event.type === 'ready') {
return yield event debug('Deployment succeeded on file check');
return yield event;
} }
yield event yield event;
} }
} }
const shas = missingFiles const shas = missingFiles;
const uploadList: { [key: string]: Promise<any> } = {}
yield { type: 'file_count', payload: { total: files, missing: shas } };
const uploadList: { [key: string]: Promise<any> } = {};
debug('Building an upload list...');
const semaphore = new Sema(700, { capacity: 700 });
shas.map((sha: string): void => { shas.map((sha: string): void => {
uploadList[sha] = retry(async (bail): Promise<any> => { uploadList[sha] = retry(
const file = files.get(sha) async (bail): Promise<any> => {
const file = files.get(sha);
if (!file) { if (!file) {
return bail(new Error(`File ${sha} is undefined`)) debug(`File ${sha} is undefined. Bailing`);
} return bail(new Error(`File ${sha} is undefined`));
const fPath = file.names[0]
const stream = createReadStream(fPath)
const { data } = file
const fstreamPush = stream.push
let uploadedSoFar = 0
// let lastEvent = 0
stream.push = (chunk: any): boolean => {
// If we're about to push the last chunk, then don't do it here
// But instead, we'll "hang" the progress bar and do it on 200
if (chunk && uploadedSoFar + chunk.length < data.length) {
uploadedSoFar += chunk.length
// semaphore.release()
} }
return fstreamPush.call(stream, chunk) await semaphore.acquire();
}
// while (uploadedSoFar !== file.data.length) { const fPath = file.names[0];
// await semaphore.acquire() const stream = createReadStream(fPath);
const { data } = file;
// lastEvent = uploadedSoFar; let err;
// yield uploadedSoFar; let result;
// }
let err try {
let result const res = await fetch(
API_FILES,
token,
{
agent: new Agent({ keepAlive: true }),
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'x-now-digest': sha,
'x-now-length': data.length,
},
body: stream,
teamId,
},
isDebug
);
try { if (res.status === 200) {
const res = await fetch(API_FILES, token, { debug(
method: 'POST', `File ${sha} (${file.names[0]}${
headers: { file.names.length > 1 ? ` +${file.names.length}` : ''
'Content-Type': 'application/octet-stream', }) uploaded`
'x-now-digest': sha, );
'x-now-length': data.length, result = {
}, type: 'file-uploaded',
body: stream, payload: { sha, file },
teamId };
}) } else if (res.status > 200 && res.status < 500) {
// If something is wrong with our request, we don't retry
debug(
`An internal error occurred in upload request. Not retrying...`
);
const { error } = await res.json();
if (res.status === 200) { err = new DeploymentError(error);
result = { } else {
type: 'file-uploaded', // If something is wrong with the server, we retry
payload: { sha, file } debug(`A server error occurred in upload request. Retrying...`);
const { error } = await res.json();
throw new DeploymentError(error);
} }
} else if (res.status > 200 && res.status < 500) { } catch (e) {
// If something is wrong with our request, we don't retry debug(`An unexpected error occurred in upload promise:\n${e}`);
const { error } = await res.json() err = new Error(e);
} finally {
err = new DeploymentError(error) stream.close();
} else { stream.destroy();
// If something is wrong with the server, we retry
const { error } = await res.json()
throw new DeploymentError(error)
} }
} catch (e) {
err = new Error(e)
} finally {
stream.close()
stream.destroy()
}
if (err) { semaphore.release();
return bail(err)
}
return result if (err) {
}, if (isClientNetworkError(err)) {
{ debug('Network error, retrying: ' + err.message);
retries: 6, // If it's a network error, we retry
randomize: true throw err;
} } else {
) debug('Other error, bailing: ' + err.message);
}) // Otherwise we bail
return bail(err);
}
}
return result;
},
{
retries: 3,
factor: 2,
}
);
});
debug('Starting upload');
while (Object.keys(uploadList).length > 0) { while (Object.keys(uploadList).length > 0) {
try { try {
const event = await Promise.race(Object.keys(uploadList).map((key): Promise<any> => uploadList[key])) const event = await Promise.race(
Object.keys(uploadList).map((key): Promise<any> => uploadList[key])
);
delete uploadList[event.payload.sha] delete uploadList[event.payload.sha];
yield event yield event;
} catch (e) { } catch (e) {
return yield { type: 'error', payload: e } return yield { type: 'error', payload: e };
} }
} }
yield { type: 'all-files-uploaded', payload: files } debug('All files uploaded');
yield { type: 'all-files-uploaded', payload: files };
try { try {
for await(const event of deploy(files, options)) { debug('Starting deployment creation');
for await (const event of deploy(files, options)) {
if (event.type === 'ready') { if (event.type === 'ready') {
return yield event debug('Deployment is ready');
return yield event;
} }
yield event yield event;
} }
} catch (e) { } catch (e) {
yield { type: 'error', payload: e } debug('An unexpected error occurred when starting deployment creation');
yield { type: 'error', payload: e };
} }
} }

View File

@@ -1,5 +1,6 @@
import { createHash } from 'crypto' import { createHash } from 'crypto';
import fs from 'fs-extra' import fs from 'fs-extra';
import { Sema } from 'async-sema';
export interface DeploymentFile { export interface DeploymentFile {
names: string[]; names: string[];
@@ -15,7 +16,7 @@ export interface DeploymentFile {
function hash(buf: Buffer): string { function hash(buf: Buffer): string {
return createHash('sha1') return createHash('sha1')
.update(buf) .update(buf)
.digest('hex') .digest('hex');
} }
/** /**
@@ -23,14 +24,16 @@ function hash(buf: Buffer): string {
* @param map with hashed files * @param map with hashed files
* @return {object} * @return {object}
*/ */
export const mapToObject = (map: Map<string, DeploymentFile>): { [key: string]: any } => { export const mapToObject = (
const obj: { [key: string]: any } = {} map: Map<string, DeploymentFile>
): { [key: string]: DeploymentFile } => {
const obj: { [key: string]: DeploymentFile } = {};
for (const [key, value] of map) { for (const [key, value] of map) {
obj[key] = value obj[key] = value;
} }
return obj return obj;
} };
/** /**
* Computes hashes for the contents of each file given. * Computes hashes for the contents of each file given.
@@ -39,25 +42,29 @@ export const mapToObject = (map: Map<string, DeploymentFile>): { [key: string]:
* @return {Map} * @return {Map}
*/ */
async function hashes(files: string[]): Promise<Map<string, DeploymentFile>> { async function hashes(files: string[]): Promise<Map<string, DeploymentFile>> {
const map = new Map() const map = new Map();
const semaphore = new Sema(100);
await Promise.all( await Promise.all(
files.map( files.map(
async (name: string): Promise<void> => { async (name: string): Promise<void> => {
const data = await fs.readFile(name) await semaphore.acquire();
const data = await fs.readFile(name);
const h = hash(data) const h = hash(data);
const entry = map.get(h) const entry = map.get(h);
if (entry) { if (entry) {
entry.names.push(name) entry.names.push(name);
} else { } else {
map.set(h, { names: [name], data }) map.set(h, { names: [name], data });
} }
},
), semaphore.release();
) }
return map )
);
return map;
} }
export default hashes export default hashes;

View File

@@ -1,52 +1,63 @@
import { DeploymentFile } from './hashes' import { DeploymentFile } from './hashes';
import { parse as parseUrl } from 'url' import { parse as parseUrl } from 'url';
import { fetch as fetch_ } from 'fetch-h2' import fetch_ from 'node-fetch';
import { readFile } from 'fs-extra' import { join, sep } from 'path';
import { join } from 'path' import qs from 'querystring';
import qs from 'querystring' import ignore from 'ignore';
import pkg from '../../package.json' import pkg from '../../package.json';
import { Options } from '../deploy' import { Options } from '../deploy';
import { NowJsonOptions } from '../types';
import { Sema } from 'async-sema';
import { readFile } from 'fs-extra';
const semaphore = new Sema(10);
export const API_FILES = 'https://api.zeit.co/v2/now/files' export const API_FILES = 'https://api.zeit.co/v2/now/files';
export const API_DEPLOYMENTS = 'https://api.zeit.co/v9/now/deployments' export const API_DEPLOYMENTS = 'https://api.zeit.co/v9/now/deployments';
export const API_DEPLOYMENTS_LEGACY = 'https://api.zeit.co/v3/now/deployments' export const API_DEPLOYMENTS_LEGACY = 'https://api.zeit.co/v3/now/deployments';
export const API_DELETE_DEPLOYMENTS_LEGACY = export const API_DELETE_DEPLOYMENTS_LEGACY =
'https://api.zeit.co/v2/now/deployments' 'https://api.zeit.co/v2/now/deployments';
export const EVENTS = new Set([ export const EVENTS = new Set([
// File events // File events
'hashes-calculated', 'hashes-calculated',
'file_count',
'file-uploaded', 'file-uploaded',
'all-files-uploaded', 'all-files-uploaded',
// Deployment events // Deployment events
'created', 'created',
'ready', 'ready',
'warning',
'error', 'error',
// Build events // Build events
'build-state-changed' 'build-state-changed',
]) ]);
export function parseNowJSON(file?: DeploymentFile): NowJsonOptions { export async function parseNowJSON(filePath?: string): Promise<NowJsonOptions> {
if (!file) { if (!filePath) {
return {} return {};
} }
try { try {
const jsonString = file.data.toString() const jsonString = await readFile(filePath, 'utf8');
return JSON.parse(jsonString) return JSON.parse(jsonString);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(e) console.error(e);
return {} return {};
} }
} }
export async function getNowIgnore( const maybeRead = async function<T>(path: string, default_: T) {
files: string[], try {
path: string | string[] return await readFile(path, 'utf8');
): Promise<string[]> { } catch (err) {
return default_;
}
};
export async function getNowIgnore(path: string | string[]): Promise<any> {
let ignores: string[] = [ let ignores: string[] = [
'.hg', '.hg',
'.git', '.git',
@@ -70,55 +81,57 @@ export async function getNowIgnore(
'node_modules', 'node_modules',
'__pycache__/', '__pycache__/',
'venv/', 'venv/',
'CVS' 'CVS',
] ];
await Promise.all( const nowIgnore = Array.isArray(path)
files.map( ? await maybeRead(
async (file: string): Promise<void> => { join(
if (file.includes('.nowignore')) { path.find(fileName => fileName.includes('.nowignore'), '') || '',
const filePath = Array.isArray(path) '.nowignore'
? file ),
: file.includes(path) ''
? file )
: join(path, file) : await maybeRead(join(path, '.nowignore'), '');
const nowIgnore = await readFile(filePath)
nowIgnore const ig = ignore().add(`${ignores.join('\n')}\n${nowIgnore}`);
.toString()
.split('\n')
.filter((s: string): boolean => s.length > 0)
.forEach((entry: string): number => ignores.push(entry))
}
}
)
)
return ignores return ig;
} }
export const fetch = ( export const fetch = async (
url: string, url: string,
token: string, token: string,
opts: any = {} opts: any = {},
debugEnabled?: boolean
): Promise<any> => { ): Promise<any> => {
if (opts.teamId) { semaphore.acquire();
const parsedUrl = parseUrl(url, true) const debug = createDebug(debugEnabled);
const query = parsedUrl.query let time: number;
query.teamId = opts.teamId if (opts.teamId) {
url = `${parsedUrl.href}?${qs.encode(query)}` const parsedUrl = parseUrl(url, true);
delete opts.teamId const query = parsedUrl.query;
query.teamId = opts.teamId;
url = `${parsedUrl.href}?${qs.encode(query)}`;
delete opts.teamId;
} }
opts.headers = opts.headers || {} opts.headers = opts.headers || {};
// @ts-ignore // @ts-ignore
opts.headers.authorization = `Bearer ${token}` opts.headers.Authorization = `Bearer ${token}`;
// @ts-ignore // @ts-ignore
opts.headers['user-agent'] = `now-client-v${pkg.version}` opts.headers['user-agent'] = `now-client-v${pkg.version}`;
return fetch_(url, opts) debug(`${opts.method || 'GET'} ${url}`);
} time = Date.now();
const res = await fetch_(url, opts);
debug(`DONE in ${Date.now() - time}ms: ${opts.method || 'GET'} ${url}`);
semaphore.release();
return res;
};
export interface PreparedFile { export interface PreparedFile {
file: string; file: string;
@@ -126,39 +139,58 @@ export interface PreparedFile {
size: number; size: number;
} }
const isWin = process.platform.includes('win');
export const prepareFiles = ( export const prepareFiles = (
files: Map<string, DeploymentFile>, files: Map<string, DeploymentFile>,
options: Options options: Options
): PreparedFile[] => { ): PreparedFile[] => {
const preparedFiles = [...files.keys()].reduce( const preparedFiles = [...files.keys()].reduce(
(acc: PreparedFile[], sha: string): PreparedFile[] => { (acc: PreparedFile[], sha: string): PreparedFile[] => {
const next = [...acc] const next = [...acc];
const file = files.get(sha) as DeploymentFile const file = files.get(sha) as DeploymentFile;
for (const name of file.names) { for (const name of file.names) {
let fileName let fileName;
if (options.isDirectory) { if (options.isDirectory) {
// Directory // Directory
fileName = options.path ? name.replace(`${options.path}/`, '') : name fileName = options.path
? name.substring(options.path.length + 1)
: name;
} else { } else {
// Array of files or single file // Array of files or single file
const segments = name.split('/') const segments = name.split(sep);
fileName = segments[segments.length - 1] fileName = segments[segments.length - 1];
} }
next.push({ next.push({
file: fileName, file: isWin ? fileName.replace(/\\/g, '/') : fileName,
size: file.data.byteLength || file.data.length, size: file.data.byteLength || file.data.length,
sha sha,
}) });
} }
return next return next;
}, },
[] []
) );
return preparedFiles return preparedFiles;
};
export function createDebug(debug?: boolean) {
const isDebug = debug || process.env.NOW_CLIENT_DEBUG;
if (isDebug) {
return (...logs: string[]) => {
process.stderr.write(
[`[now-client-debug] ${new Date().toISOString()}`, ...logs].join(' ') +
'\n'
);
};
}
return () => {};
} }

View File

@@ -1,13 +1,13 @@
import { Options } from "../deploy" import { Options } from '../deploy';
export const generateQueryString = (options: Options): string => { export const generateQueryString = (options: Options): string => {
if (options.force && options.teamId) { if (options.force && options.teamId) {
return `?teamId=${options.teamId}&forceNew=1` return `?teamId=${options.teamId}&forceNew=1`;
} else if (options.teamId) { } else if (options.teamId) {
return `?teamId=${options.teamId}` return `?teamId=${options.teamId}`;
} else if (options.force) { } else if (options.force) {
return `?forceNew=1` return `?forceNew=1`;
} }
return '' return '';
} };

View File

@@ -1,3 +1,16 @@
export const isReady = ({ readyState, state }: Deployment | DeploymentBuild): boolean => readyState === 'READY' || state === 'READY' import { Deployment, DeploymentBuild } from '../types';
export const isFailed = ({ readyState, state }: Deployment | DeploymentBuild): boolean => readyState ? (readyState.endsWith('_ERROR') || readyState === 'ERROR') : (state && state.endsWith('_ERROR') || state === 'ERROR') export const isReady = ({
export const isDone = (buildOrDeployment: Deployment | DeploymentBuild): boolean => isReady(buildOrDeployment) || isFailed(buildOrDeployment) readyState,
state,
}: Deployment | DeploymentBuild): boolean =>
readyState === 'READY' || state === 'READY';
export const isFailed = ({
readyState,
state,
}: Deployment | DeploymentBuild): boolean =>
readyState
? readyState.endsWith('_ERROR') || readyState === 'ERROR'
: (state && state.endsWith('_ERROR')) || state === 'ERROR';
export const isDone = (
buildOrDeployment: Deployment | DeploymentBuild
): boolean => isReady(buildOrDeployment) || isFailed(buildOrDeployment);

View File

@@ -1,11 +1,11 @@
import path from 'path' import path from 'path';
import { TOKEN } from './constants' import { TOKEN } from './constants';
import { fetch, API_DEPLOYMENTS } from '../src/utils' import { fetch, API_DEPLOYMENTS } from '../src/utils';
import { Deployment } from './types' import { Deployment } from './types';
import { createDeployment } from '../src/index' import { createDeployment } from '../src/index';
describe('create v2 deployment', () => { describe('create v2 deployment', () => {
let deployment: Deployment let deployment: Deployment;
afterEach(async () => { afterEach(async () => {
if (deployment) { if (deployment) {
@@ -15,10 +15,48 @@ describe('create v2 deployment', () => {
{ {
method: 'DELETE' method: 'DELETE'
} }
) );
expect(response.status).toEqual(200) expect(response.status).toEqual(200);
} }
}) });
it('will display an empty deployment warning', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token: TOKEN,
name: 'now-client-tests-v2'
}
)) {
if (event.type === 'warning') {
expect(event.payload).toEqual('READY');
}
if (event.type === 'ready') {
deployment = event.payload;
break;
}
}
});
it('will report correct file count event', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token: TOKEN,
name: 'now-client-tests-v2'
}
)) {
if (event.type === 'file_count') {
expect(event.payload.total).toEqual(0);
}
if (event.type === 'ready') {
deployment = event.payload;
break;
}
}
});
it('will create a v2 deployment', async () => { it('will create a v2 deployment', async () => {
for await (const event of createDeployment( for await (const event of createDeployment(
@@ -29,10 +67,10 @@ describe('create v2 deployment', () => {
} }
)) { )) {
if (event.type === 'ready') { if (event.type === 'ready') {
deployment = event.payload deployment = event.payload;
expect(deployment.readyState).toEqual('READY') expect(deployment.readyState).toEqual('READY');
break break;
} }
} }
}) });
}) });

View File

@@ -5,7 +5,7 @@
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./lib", "outDir": "./dist",
"resolveJsonModule": true, "resolveJsonModule": true,
"strictNullChecks": true, "strictNullChecks": true,
"noImplicitAny": true, "noImplicitAny": true,
@@ -14,5 +14,6 @@
"strict": true, "strict": true,
"target": "ES2015", "target": "ES2015",
"downlevelIteration": true "downlevelIteration": true
} },
} "include": ["./src", "./types"]
}

View File

@@ -11,7 +11,7 @@ import {
BuildOptions, BuildOptions,
shouldServe, shouldServe,
Files, Files,
debug debug,
} from '@now/build-utils'; } from '@now/build-utils';
import { createGo, getAnalyzedEntrypoint } from './go-helpers'; import { createGo, getAnalyzedEntrypoint } from './go-helpers';
@@ -38,7 +38,7 @@ async function initPrivateGit(credentials: string) {
'config', 'config',
'--global', '--global',
'credential.helper', 'credential.helper',
`store --file ${join(homedir(), '.git-credentials')}` `store --file ${join(homedir(), '.git-credentials')}`,
]); ]);
await writeFile(join(homedir(), '.git-credentials'), credentials); await writeFile(join(homedir(), '.git-credentials'), credentials);
@@ -51,7 +51,7 @@ export async function build({
entrypoint, entrypoint,
config, config,
workPath, workPath,
meta = {} as BuildParamsMeta meta = {} as BuildParamsMeta,
}: BuildParamsType) { }: BuildParamsType) {
if (process.env.GIT_CREDENTIALS && !meta.isDev) { if (process.env.GIT_CREDENTIALS && !meta.isDev) {
debug('Initialize Git credentials...'); debug('Initialize Git credentials...');
@@ -76,7 +76,7 @@ Learn more: https://github.com/golang/go/wiki/Modules
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [goPath, outDir] = await Promise.all([ let [goPath, outDir] = await Promise.all([
getWriteableDirectory(), getWriteableDirectory(),
getWriteableDirectory() getWriteableDirectory(),
]); ]);
const srcPath = join(goPath, 'src', 'lambda'); const srcPath = join(goPath, 'src', 'lambda');
@@ -194,7 +194,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
process.platform, process.platform,
process.arch, process.arch,
{ {
cwd: entrypointDirname cwd: entrypointDirname,
}, },
true true
); );
@@ -204,7 +204,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
await writeFile(join(entrypointDirname, 'go.mod'), defaultGoModContent); await writeFile(join(entrypointDirname, 'go.mod'), defaultGoModContent);
} catch (err) { } catch (err) {
console.log(`failed to create default go.mod for ${packageName}`); console.log(`Failed to create default go.mod for ${packageName}`);
throw err; throw err;
} }
} }
@@ -277,11 +277,11 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
!isGoModExist !isGoModExist
) { ) {
await move(downloadedFiles[entrypoint].fsPath, finalDestination, { await move(downloadedFiles[entrypoint].fsPath, finalDestination, {
overwrite: forceMove overwrite: forceMove,
}); });
} }
} catch (err) { } catch (err) {
console.log('failed to move entry to package folder'); console.log('Failed to move entry to package folder');
throw err; throw err;
} }
@@ -354,7 +354,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
process.platform, process.platform,
process.arch, process.arch,
{ {
cwd: entrypointDirname cwd: entrypointDirname,
}, },
false false
); );
@@ -381,7 +381,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
try { try {
await go.get(); await go.get();
} catch (err) { } catch (err) {
console.log('failed to `go get`'); console.log('Failed to `go get`');
throw err; throw err;
} }
@@ -390,7 +390,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
try { try {
const src = [ const src = [
join(entrypointDirname, mainGoFileName), join(entrypointDirname, mainGoFileName),
downloadedFiles[entrypoint].fsPath downloadedFiles[entrypoint].fsPath,
]; ];
await go.build(src, destPath); await go.build(src, destPath);
} catch (err) { } catch (err) {
@@ -403,10 +403,10 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
files: { ...(await glob('**', outDir)), ...includedFiles }, files: { ...(await glob('**', outDir)), ...includedFiles },
handler: 'handler', handler: 'handler',
runtime: 'go1.x', runtime: 'go1.x',
environment: {} environment: {},
}); });
const output = { const output = {
[entrypoint]: lambda [entrypoint]: lambda,
}; };
const watch = parsedAnalyzed.watch; const watch = parsedAnalyzed.watch;
@@ -420,7 +420,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
return { return {
output, output,
watch: watch.concat(watchSub) watch: watch.concat(watchSub),
}; };
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@now/go", "name": "@now/go",
"version": "0.5.12", "version": "0.6.0",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/go-now-go", "homepage": "https://zeit.co/docs/v2/deployments/official-builders/go-now-go",

View File

@@ -0,0 +1,5 @@
// +build first
package custom
const Random = "first:RANDOMNESS_PLACEHOLDER"

View File

@@ -0,0 +1,5 @@
// +build second
package custom
const Random = "second:RANDOMNESS_PLACEHOLDER"

View File

@@ -0,0 +1,3 @@
module custom-flag
go 1.12

View File

@@ -3,9 +3,11 @@ package handler
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"custom-flag/custom"
) )
// Index func // Index func
func Index(w http.ResponseWriter, req *http.Request) { func Index(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "RANDOMNESS_PLACEHOLDER") fmt.Fprintf(w, custom.Random)
} }

View File

@@ -1,6 +1,6 @@
{ {
"version": 2, "version": 2,
"builds": [{ "src": "index.go", "use": "@now/go" }], "builds": [{ "src": "index.go", "use": "@now/go" }],
"build": { "env": { "GO_BUILD_FLAGS": "-tags netgo -ldflags '-s -w'" }}, "build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } },
"probes": [{ "path": "/", "mustContain": "RANDOMNESS_PLACEHOLDER" }] "probes": [{ "path": "/", "mustContain": "first:RANDOMNESS_PLACEHOLDER" }]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@now/next", "name": "@now/next",
"version": "1.0.0", "version": "1.0.3",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/next-js-now-next", "homepage": "https://zeit.co/docs/v2/deployments/official-builders/next-js-now-next",
@@ -25,7 +25,7 @@
"@types/resolve-from": "5.0.1", "@types/resolve-from": "5.0.1",
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@types/yazl": "2.4.1", "@types/yazl": "2.4.1",
"@zeit/node-file-trace": "0.2.14", "@zeit/node-file-trace": "0.3.1",
"async-sema": "3.0.1", "async-sema": "3.0.1",
"buffer-crc32": "0.2.13", "buffer-crc32": "0.2.13",
"execa": "2.0.4", "execa": "2.0.4",

View File

@@ -42,6 +42,7 @@ function getDefaultData(target: string) {
export default async function createServerlessConfig( export default async function createServerlessConfig(
workPath: string, workPath: string,
entryPath: string,
nextVersion: string | undefined nextVersion: string | undefined
) { ) {
let target = 'serverless'; let target = 'serverless';
@@ -50,12 +51,35 @@ export default async function createServerlessConfig(
if (semver.gte(nextVersion, ExperimentalTraceVersion)) { if (semver.gte(nextVersion, ExperimentalTraceVersion)) {
target = 'experimental-serverless-trace'; target = 'experimental-serverless-trace';
} }
} catch (_ignored) {} } catch (
_ignored
// eslint-disable-next-line
) {}
} }
const configPath = path.join(workPath, 'next.config.js'); const primaryConfigPath = path.join(entryPath, 'next.config.js');
const secondaryConfigPath = path.join(workPath, 'next.config.js');
const backupConfigName = `next.config.original.${Date.now()}.js`; const backupConfigName = `next.config.original.${Date.now()}.js`;
const backupConfigPath = path.join(workPath, backupConfigName);
const hasPrimaryConfig = fs.existsSync(primaryConfigPath);
const hasSecondaryConfig = fs.existsSync(secondaryConfigPath);
let configPath: string;
let backupConfigPath: string;
if (hasPrimaryConfig) {
// Prefer primary path
configPath = primaryConfigPath;
backupConfigPath = path.join(entryPath, backupConfigName);
} else if (hasSecondaryConfig) {
// Work with secondary path (some monorepo setups)
configPath = secondaryConfigPath;
backupConfigPath = path.join(workPath, backupConfigName);
} else {
// Default to primary path for creation
configPath = primaryConfigPath;
backupConfigPath = path.join(entryPath, backupConfigName);
}
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
await fs.rename(configPath, backupConfigPath); await fs.rename(configPath, backupConfigPath);

View File

@@ -37,12 +37,10 @@ import {
EnvConfig, EnvConfig,
excludeFiles, excludeFiles,
ExperimentalTraceVersion, ExperimentalTraceVersion,
filesFromDirectory,
getDynamicRoutes, getDynamicRoutes,
getNextConfig, getNextConfig,
getPathsInside, getPathsInside,
getRoutes, getRoutes,
includeOnlyEntryDirectory,
isDynamicRoute, isDynamicRoute,
normalizePackageJson, normalizePackageJson,
normalizePage, normalizePage,
@@ -242,6 +240,7 @@ export const build = async ({
} }
const isLegacy = isLegacyNext(nextVersion); const isLegacy = isLegacyNext(nextVersion);
let shouldRunScript = 'now-build';
debug(`MODE: ${isLegacy ? 'legacy' : 'serverless'}`); debug(`MODE: ${isLegacy ? 'legacy' : 'serverless'}`);
@@ -262,10 +261,16 @@ export const build = async ({
"WARNING: your application is being deployed in @now/next's legacy mode. http://err.sh/zeit/now/now-next-legacy-mode" "WARNING: your application is being deployed in @now/next's legacy mode. http://err.sh/zeit/now/now-next-legacy-mode"
); );
debug('normalizing package.json'); debug('Normalizing package.json');
const packageJson = normalizePackageJson(pkg); const packageJson = normalizePackageJson(pkg);
debug('normalized package.json result: ', packageJson); debug('Normalized package.json result: ', packageJson);
await writePackageJson(entryPath, packageJson); await writePackageJson(entryPath, packageJson);
} else if (pkg.scripts && pkg.scripts['now-build']) {
debug('Found user `now-build` script');
shouldRunScript = 'now-build';
} else if (pkg.scripts && pkg.scripts['build']) {
debug('Found user `build` script');
shouldRunScript = 'build';
} else if (!pkg.scripts || !pkg.scripts['now-build']) { } else if (!pkg.scripts || !pkg.scripts['now-build']) {
debug( debug(
'Your application is being built using `next build`. ' + 'Your application is being built using `next build`. ' +
@@ -276,15 +281,16 @@ export const build = async ({
'now-build': 'next build', 'now-build': 'next build',
...(pkg.scripts || {}), ...(pkg.scripts || {}),
}; };
shouldRunScript = 'now-build';
await writePackageJson(entryPath, pkg); await writePackageJson(entryPath, pkg);
} }
if (process.env.NPM_AUTH_TOKEN) { if (process.env.NPM_AUTH_TOKEN) {
debug('found NPM_AUTH_TOKEN in environment, creating .npmrc'); debug('Found NPM_AUTH_TOKEN in environment, creating .npmrc');
await writeNpmRc(entryPath, process.env.NPM_AUTH_TOKEN); await writeNpmRc(entryPath, process.env.NPM_AUTH_TOKEN);
} }
debug('installing dependencies...'); console.log('Installing dependencies...');
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts, meta); await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts, meta);
let realNextVersion: string | undefined; let realNextVersion: string | undefined;
@@ -292,23 +298,23 @@ export const build = async ({
realNextVersion = require(resolveFrom(entryPath, 'next/package.json')) realNextVersion = require(resolveFrom(entryPath, 'next/package.json'))
.version; .version;
debug(`detected Next.js version: ${realNextVersion}`); debug(`Detected Next.js version: ${realNextVersion}`);
} catch (_ignored) { } catch (_ignored) {
debug(`could not identify real Next.js version, that's OK!`); debug(`Could not identify real Next.js version, that's OK!`);
} }
if (!isLegacy) { if (!isLegacy) {
await createServerlessConfig(workPath, realNextVersion); await createServerlessConfig(workPath, entryPath, realNextVersion);
} }
debug('running user script...'); debug('Running user script...');
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128; const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
const env: { [key: string]: string | undefined } = { ...spawnOpts.env }; const env: { [key: string]: string | undefined } = { ...spawnOpts.env };
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`; env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
await runPackageJsonScript(entryPath, 'now-build', { ...spawnOpts, env }); await runPackageJsonScript(entryPath, shouldRunScript, { ...spawnOpts, env });
if (isLegacy) { if (isLegacy) {
debug('running npm install --production...'); debug('Running npm install --production...');
await runNpmInstall( await runNpmInstall(
entryPath, entryPath,
['--prefer-offline', '--production'], ['--prefer-offline', '--production'],
@@ -329,7 +335,7 @@ export const build = async ({
if (isLegacy) { if (isLegacy) {
const filesAfterBuild = await glob('**', entryPath); const filesAfterBuild = await glob('**', entryPath);
debug('preparing lambda files...'); debug('Preparing serverless function files...');
let buildId: string; let buildId: string;
try { try {
buildId = await readFile( buildId = await readFile(
@@ -397,7 +403,7 @@ export const build = async ({
], ],
}; };
debug(`Creating lambda for page: "${page}"...`); debug(`Creating serverless function for page: "${page}"...`);
lambdas[path.join(entryDirectory, pathname)] = await createLambda({ lambdas[path.join(entryDirectory, pathname)] = await createLambda({
files: { files: {
...nextFiles, ...nextFiles,
@@ -407,11 +413,11 @@ export const build = async ({
handler: 'now__launcher.launcher', handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime, runtime: nodeVersion.runtime,
}); });
debug(`Created lambda for page: "${page}"`); debug(`Created serverless function for page: "${page}"`);
}) })
); );
} else { } else {
debug('preparing lambda files...'); debug('Preparing serverless function files...');
const pagesDir = path.join(entryPath, '.next', 'serverless', 'pages'); const pagesDir = path.join(entryPath, '.next', 'serverless', 'pages');
const pages = await glob('**/*.js', pagesDir); const pages = await glob('**/*.js', pagesDir);
@@ -488,7 +494,8 @@ export const build = async ({
} = {}; } = {};
if (requiresTracing) { if (requiresTracing) {
const tracingLabel = 'Tracing Next.js lambdas for external files ...'; const tracingLabel =
'Tracing Next.js serverless functions for external files ...';
console.time(tracingLabel); console.time(tracingLabel);
const apiPages: string[] = []; const apiPages: string[] = [];
@@ -534,7 +541,7 @@ export const build = async ({
apiFileList.forEach(collectTracedFiles(apiReasons, apiTracedFiles)); apiFileList.forEach(collectTracedFiles(apiReasons, apiTracedFiles));
console.timeEnd(tracingLabel); console.timeEnd(tracingLabel);
const zippingLabel = 'Compressing shared lambda files'; const zippingLabel = 'Compressing shared serverless function files';
console.time(zippingLabel); console.time(zippingLabel);
pseudoLayers.push(await createPseudoLayer(tracedFiles)); pseudoLayers.push(await createPseudoLayer(tracedFiles));
@@ -552,7 +559,9 @@ export const build = async ({
const assetKeys = Object.keys(assets); const assetKeys = Object.keys(assets);
if (assetKeys.length > 0) { if (assetKeys.length > 0) {
debug('detected (legacy) assets to be bundled with lambda:'); debug(
'detected (legacy) assets to be bundled with serverless function:'
);
assetKeys.forEach(assetFile => debug(`\t${assetFile}`)); assetKeys.forEach(assetFile => debug(`\t${assetFile}`));
debug( debug(
'\nPlease upgrade to Next.js 9.1 to leverage modern asset handling.' '\nPlease upgrade to Next.js 9.1 to leverage modern asset handling.'
@@ -562,7 +571,7 @@ export const build = async ({
const launcherPath = path.join(__dirname, 'templated-launcher.js'); const launcherPath = path.join(__dirname, 'templated-launcher.js');
const launcherData = await readFile(launcherPath, 'utf8'); const launcherData = await readFile(launcherPath, 'utf8');
const allLambdasLabel = `All lambdas created`; const allLambdasLabel = `All serverless functions created`;
console.time(allLambdasLabel); console.time(allLambdasLabel);
await Promise.all( await Promise.all(
@@ -578,7 +587,7 @@ export const build = async ({
dynamicPages.push(normalizePage(pathname)); dynamicPages.push(normalizePage(pathname));
} }
const label = `Creating lambda for page: "${page}"...`; const label = `Creating serverless function for page: "${page}"...`;
console.time(label); console.time(label);
const pageFileName = path.normalize( const pageFileName = path.normalize(
@@ -629,6 +638,9 @@ export const build = async ({
'**', '**',
path.join(entryPath, '.next', 'static') path.join(entryPath, '.next', 'static')
); );
const staticFolderFiles = await glob('**', path.join(entryPath, 'static'));
const publicFolderFiles = await glob('**', path.join(entryPath, 'public'));
const staticFiles = Object.keys(nextStaticFiles).reduce( const staticFiles = Object.keys(nextStaticFiles).reduce(
(mappedFiles, file) => ({ (mappedFiles, file) => ({
...mappedFiles, ...mappedFiles,
@@ -638,23 +650,24 @@ export const build = async ({
}), }),
{} {}
); );
const staticDirectoryFiles = Object.keys(staticFolderFiles).reduce(
const entryDirectoryFiles = includeOnlyEntryDirectory(files, entryDirectory);
const staticDirectoryFiles = filesFromDirectory(
entryDirectoryFiles,
path.join(entryDirectory, 'static')
);
const publicDirectoryFiles = filesFromDirectory(
entryDirectoryFiles,
path.join(entryDirectory, 'public')
);
const publicFiles = Object.keys(publicDirectoryFiles).reduce(
(mappedFiles, file) => ({ (mappedFiles, file) => ({
...mappedFiles, ...mappedFiles,
[file.replace(/public[/\\]+/, '')]: publicDirectoryFiles[file], [path.join(entryDirectory, 'static', file)]: staticFolderFiles[file],
}), }),
{} {}
); );
const publicDirectoryFiles = Object.keys(publicFolderFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
[path.join(
entryDirectory,
file.replace(/public[/\\]+/, '')
)]: publicFolderFiles[file],
}),
{}
);
let dynamicPrefix = path.join('/', entryDirectory); let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix; dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
@@ -674,7 +687,7 @@ export const build = async ({
return { return {
output: { output: {
...publicFiles, ...publicDirectoryFiles,
...lambdas, ...lambdas,
...staticPages, ...staticPages,
...staticFiles, ...staticFiles,
@@ -717,7 +730,7 @@ export const prepareCache = async ({
workPath, workPath,
entrypoint, entrypoint,
}: PrepareCacheOptions) => { }: PrepareCacheOptions) => {
debug('preparing cache ...'); debug('Preparing cache...');
const entryDirectory = path.dirname(entrypoint); const entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory); const entryPath = path.join(workPath, entryDirectory);
@@ -731,7 +744,7 @@ export const prepareCache = async ({
return {}; return {};
} }
debug('producing cache file manifest ...'); debug('Producing cache file manifest...');
const cacheEntrypoint = path.relative(workPath, entryPath); const cacheEntrypoint = path.relative(workPath, entryPath);
const cache = { const cache = {
...(await glob(path.join(cacheEntrypoint, 'node_modules/**'), workPath)), ...(await glob(path.join(cacheEntrypoint, 'node_modules/**'), workPath)),
@@ -739,6 +752,6 @@ export const prepareCache = async ({
...(await glob(path.join(cacheEntrypoint, 'package-lock.json'), workPath)), ...(await glob(path.join(cacheEntrypoint, 'package-lock.json'), workPath)),
...(await glob(path.join(cacheEntrypoint, 'yarn.lock'), workPath)), ...(await glob(path.join(cacheEntrypoint, 'yarn.lock'), workPath)),
}; };
debug('cache file manifest produced'); debug('Cache file manifest produced');
return cache; return cache;
}; };

View File

@@ -55,29 +55,11 @@ function excludeFiles(
} }
return { return {
...newFiles, ...newFiles,
[filePath]: files[filePath] [filePath]: files[filePath],
}; };
}, {}); }, {});
} }
/**
* Creates a new Files object holding only the entrypoint files
*/
function includeOnlyEntryDirectory(
files: Files,
entryDirectory: string
): Files {
if (entryDirectory === '.') {
return files;
}
function matcher(filePath: string) {
return !filePath.startsWith(entryDirectory);
}
return excludeFiles(files, matcher);
}
/** /**
* Exclude package manager lockfiles from files * Exclude package manager lockfiles from files
*/ */
@@ -92,17 +74,6 @@ function excludeLockFiles(files: Files): Files {
return files; return files;
} }
/**
* Include only the files from a selected directory
*/
function filesFromDirectory(files: Files, dir: string): Files {
function matcher(filePath: string) {
return !filePath.startsWith(dir.replace(/\\/g, '/'));
}
return excludeFiles(files, matcher);
}
/** /**
* Enforce specific package.json configuration for smallest possible lambda * Enforce specific package.json configuration for smallest possible lambda
*/ */
@@ -116,7 +87,7 @@ function normalizePackageJson(
const dependencies: stringMap = {}; const dependencies: stringMap = {};
const devDependencies: stringMap = { const devDependencies: stringMap = {
...defaultPackageJson.dependencies, ...defaultPackageJson.dependencies,
...defaultPackageJson.devDependencies ...defaultPackageJson.devDependencies,
}; };
if (devDependencies.react) { if (devDependencies.react) {
@@ -139,17 +110,18 @@ function normalizePackageJson(
'react-dom': 'latest', 'react-dom': 'latest',
...dependencies, // override react if user provided it ...dependencies, // override react if user provided it
// next-server is forced to canary // next-server is forced to canary
'next-server': 'v7.0.2-canary.49' 'next-server': 'v7.0.2-canary.49',
}, },
devDependencies: { devDependencies: {
...devDependencies, ...devDependencies,
// next is forced to canary // next is forced to canary
next: 'v7.0.2-canary.49' next: 'v7.0.2-canary.49',
}, },
scripts: { scripts: {
...defaultPackageJson.scripts, ...defaultPackageJson.scripts,
'now-build': 'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas' 'now-build':
} 'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
},
}; };
} }
@@ -218,12 +190,12 @@ function getRoutes(
const routes: Route[] = [ const routes: Route[] = [
{ {
src: `${prefix}_next/(.*)`, src: `${prefix}_next/(.*)`,
dest: `${url}/_next/$1` dest: `${url}/_next/$1`,
}, },
{ {
src: `${prefix}static/(.*)`, src: `${prefix}static/(.*)`,
dest: `${url}/static/$1` dest: `${url}/static/$1`,
} },
]; ];
const filePaths = Object.keys(filesInside); const filePaths = Object.keys(filesInside);
const dynamicPages = []; const dynamicPages = [];
@@ -251,7 +223,7 @@ function getRoutes(
routes.push({ routes.push({
src: `${prefix}${pageName}`, src: `${prefix}${pageName}`,
dest: `${url}/${pageName}` dest: `${url}/${pageName}`,
}); });
if (pageName.endsWith('index')) { if (pageName.endsWith('index')) {
@@ -259,7 +231,7 @@ function getRoutes(
routes.push({ routes.push({
src: `${prefix}${resolvedIndex}`, src: `${prefix}${resolvedIndex}`,
dest: `${url}/${resolvedIndex}` dest: `${url}/${resolvedIndex}`,
}); });
} }
} }
@@ -288,7 +260,7 @@ function getRoutes(
const fileName = path.relative('public', relativePath); const fileName = path.relative('public', relativePath);
const route = { const route = {
src: `${prefix}${fileName}`, src: `${prefix}${fileName}`,
dest: `${url}/${fileName}` dest: `${url}/${fileName}`,
}; };
// Only add the route if a page is not already using it // Only add the route if a page is not already using it
@@ -326,6 +298,18 @@ export function getDynamicRoutes(
} }
} catch (_) {} // eslint-disable-line no-empty } catch (_) {} // eslint-disable-line no-empty
if (!getRouteRegex || !getSortedRoutes) {
try {
({ getRouteRegex, getSortedRoutes } = require(resolveFrom(
entryPath,
'next/dist/next-server/lib/router/utils'
)));
if (typeof getRouteRegex !== 'function') {
getRouteRegex = undefined;
}
} catch (_) {} // eslint-disable-line no-empty
}
if (!getRouteRegex || !getSortedRoutes) { if (!getRouteRegex || !getSortedRoutes) {
throw new Error( throw new Error(
'Found usage of dynamic routes but not on a new enough version of Next.js.' 'Found usage of dynamic routes but not on a new enough version of Next.js.'
@@ -334,7 +318,7 @@ export function getDynamicRoutes(
const pageMatchers = getSortedRoutes(dynamicPages).map(pageName => ({ const pageMatchers = getSortedRoutes(dynamicPages).map(pageName => ({
pageName, pageName,
matcher: getRouteRegex && getRouteRegex(pageName).re matcher: getRouteRegex && getRouteRegex(pageName).re,
})); }));
const routes: { src: string; dest: string }[] = []; const routes: { src: string; dest: string }[] = [];
@@ -347,7 +331,7 @@ export function getDynamicRoutes(
if (pageMatcher && pageMatcher.matcher) { if (pageMatcher && pageMatcher.matcher) {
routes.push({ routes.push({
src: pageMatcher.matcher.source, src: pageMatcher.matcher.source,
dest dest,
}); });
} }
}); });
@@ -403,7 +387,7 @@ export async function createPseudoLayer(files: {
pseudoLayer[fileName] = { pseudoLayer[fileName] = {
compBuffer, compBuffer,
crc32: crc32.unsigned(origBuffer), crc32: crc32.unsigned(origBuffer),
uncompressedSize: origBuffer.byteLength uncompressedSize: origBuffer.byteLength,
}; };
} }
@@ -427,7 +411,7 @@ export async function createLambdaFromPseudoLayers({
layers, layers,
handler, handler,
runtime, runtime,
environment = {} environment = {},
}: CreateLambdaFromPseudoLayersOptions) { }: CreateLambdaFromPseudoLayersOptions) {
await createLambdaSema.acquire(); await createLambdaSema.acquire();
const zipFile = new ZipFile(); const zipFile = new ZipFile();
@@ -441,7 +425,7 @@ export async function createLambdaFromPseudoLayers({
// @ts-ignore: `addDeflatedBuffer` is a valid function, but missing on the type // @ts-ignore: `addDeflatedBuffer` is a valid function, but missing on the type
zipFile.addDeflatedBuffer(compBuffer, seedKey, { zipFile.addDeflatedBuffer(compBuffer, seedKey, {
crc32, crc32,
uncompressedSize uncompressedSize,
}); });
addedFiles.add(seedKey); addedFiles.add(seedKey);
@@ -464,22 +448,20 @@ export async function createLambdaFromPseudoLayers({
handler, handler,
runtime, runtime,
zipBuffer, zipBuffer,
environment environment,
}); });
} }
export { export {
excludeFiles, excludeFiles,
validateEntrypoint, validateEntrypoint,
includeOnlyEntryDirectory,
excludeLockFiles, excludeLockFiles,
normalizePackageJson, normalizePackageJson,
filesFromDirectory,
getNextConfig, getNextConfig,
getPathsInside, getPathsInside,
getRoutes, getRoutes,
stringMap, stringMap,
syncEnvVars, syncEnvVars,
normalizePage, normalizePage,
isDynamicRoute isDynamicRoute,
}; };

View File

@@ -8,7 +8,7 @@ it(
'Should build the standard example', 'Should build the standard example',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'standard')); } = await runBuildLambda(path.join(__dirname, 'standard'));
expect(output['index.html']).toBeDefined(); expect(output['index.html']).toBeDefined();
expect(output.goodbye).toBeDefined(); expect(output.goodbye).toBeDefined();
@@ -33,7 +33,7 @@ it(
'Should build the monorepo example', 'Should build the monorepo example',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'monorepo')); } = await runBuildLambda(path.join(__dirname, 'monorepo'));
expect(output['www/index']).toBeDefined(); expect(output['www/index']).toBeDefined();
expect(output['www/static/test.txt']).toBeDefined(); expect(output['www/static/test.txt']).toBeDefined();
@@ -55,7 +55,7 @@ it(
'Should build the legacy standard example', 'Should build the legacy standard example',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'legacy-standard')); } = await runBuildLambda(path.join(__dirname, 'legacy-standard'));
expect(output.index).toBeDefined(); expect(output.index).toBeDefined();
const filePaths = Object.keys(output); const filePaths = Object.keys(output);
@@ -75,7 +75,7 @@ it(
'Should build the legacy custom dependency test', 'Should build the legacy custom dependency test',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'legacy-custom-dependency')); } = await runBuildLambda(path.join(__dirname, 'legacy-custom-dependency'));
expect(output.index).toBeDefined(); expect(output.index).toBeDefined();
}, },
@@ -97,7 +97,7 @@ it(
'Should build the static-files test on legacy', 'Should build the static-files test on legacy',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'legacy-static-files')); } = await runBuildLambda(path.join(__dirname, 'legacy-static-files'));
expect(output['static/test.txt']).toBeDefined(); expect(output['static/test.txt']).toBeDefined();
}, },
@@ -108,7 +108,7 @@ it(
'Should build the static-files test', 'Should build the static-files test',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'static-files')); } = await runBuildLambda(path.join(__dirname, 'static-files'));
expect(output['static/test.txt']).toBeDefined(); expect(output['static/test.txt']).toBeDefined();
}, },
@@ -119,9 +119,10 @@ it(
'Should build the public-files test', 'Should build the public-files test',
async () => { async () => {
const { const {
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'public-files')); } = await runBuildLambda(path.join(__dirname, 'public-files'));
expect(output['robots.txt']).toBeDefined(); expect(output['robots.txt']).toBeDefined();
expect(output['generated.txt']).toBeDefined();
}, },
FOUR_MINUTES FOUR_MINUTES
); );
@@ -131,7 +132,7 @@ it(
async () => { async () => {
const { const {
workPath, workPath,
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-config')); } = await runBuildLambda(path.join(__dirname, 'serverless-config'));
expect(output.index).toBeDefined(); expect(output.index).toBeDefined();
@@ -160,6 +161,75 @@ it(
FOUR_MINUTES FOUR_MINUTES
); );
it(
'Should build the serverless-config-monorepo-missing example',
async () => {
const {
workPath,
buildResult: { output },
} = await runBuildLambda(
path.join(__dirname, 'serverless-config-monorepo-missing')
);
expect(output['nested/index']).toBeDefined();
expect(output['nested/goodbye']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
expect(serverlessError).toBeTruthy();
const contents = await fs.readdir(path.join(workPath, 'nested'));
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
},
FOUR_MINUTES
);
it(
'Should build the serverless-config-monorepo-present example',
async () => {
const {
workPath,
buildResult: { output },
} = await runBuildLambda(
path.join(__dirname, 'serverless-config-monorepo-present')
);
expect(output['nested/index']).toBeDefined();
expect(output['nested/goodbye']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
expect(serverlessError).toBeTruthy();
const contents = await fs.readdir(path.join(workPath, 'nested'));
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
).toBeTruthy();
},
FOUR_MINUTES
);
it( it(
'Should not build the serverless-config-async example', 'Should not build the serverless-config-async example',
async () => { async () => {
@@ -197,7 +267,7 @@ it(
async () => { async () => {
const { const {
workPath, workPath,
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-config-object')); } = await runBuildLambda(path.join(__dirname, 'serverless-config-object'));
expect(output['index.html']).toBeDefined(); expect(output['index.html']).toBeDefined();
@@ -231,7 +301,7 @@ it(
async () => { async () => {
const { const {
workPath, workPath,
buildResult: { output } buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-no-config')); } = await runBuildLambda(path.join(__dirname, 'serverless-no-config'));
expect(output['index.html']).toBeDefined(); expect(output['index.html']).toBeDefined();
@@ -259,3 +329,45 @@ it(
}, },
FOUR_MINUTES FOUR_MINUTES
); );
it(
'Should invoke build command with serverless-no-config',
async () => {
const {
workPath,
buildResult: { output },
} = await runBuildLambda(
path.join(__dirname, 'serverless-no-config-build')
);
expect(output['index.html']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
);
const hasBuildFile = await fs.pathExists(
path.join(__dirname, 'serverless-no-config-build'),
'.next',
'world.txt'
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
expect(serverlessError).toBeTruthy();
expect(hasBuildFile).toBeTruthy();
const contents = await fs.readdir(workPath);
expect(contents.some(name => name === 'next.config.js')).toBeTruthy();
expect(
contents.some(name => name.includes('next.config.original.'))
).toBeFalsy();
},
FOUR_MINUTES
);

View File

@@ -0,0 +1,4 @@
const fs = require('fs');
// Adds a new file to the public folder at build time
fs.writeFileSync('public/generated.txt', 'Generated');

View File

@@ -1,9 +1,9 @@
{ {
"scripts": { "scripts": {
"now-build": "next build" "now-build": "node create-public-file.js && next build"
}, },
"dependencies": { "dependencies": {
"next": "8", "next": "9",
"react": "16", "react": "16",
"react-dom": "16" "react-dom": "16"
} }

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "8.1.0",
"react": "latest",
"react-dom": "latest"
}
}

View File

@@ -0,0 +1,3 @@
const F = () => 'Goodbye World!';
F.getInitialProps = async () => ({});
export default F;

View File

@@ -0,0 +1 @@
export default () => 'Hello World!';

View File

@@ -0,0 +1,4 @@
{
"version": 2,
"builds": [{ "src": "nested/package.json", "use": "@now/next" }]
}

View File

@@ -0,0 +1 @@
module.exports = () => ({});

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "8.1.0",
"react": "latest",
"react-dom": "latest"
}
}

Some files were not shown because too many files have changed in this diff Show More