Compare commits

..

241 Commits

Author SHA1 Message Date
Andy Bitz
b7f8f7c366 Publish
- gatsby-plugin-now@1.2.1-canary.4
 - @now/build-utils@1.0.0-canary.20
 - now@16.1.4-canary.35
 - now-client@5.1.1-canary.15
 - @now/go@0.5.11-canary.7
 - @now/next@1.0.0-canary.20
 - @now/node@1.0.0-canary.12
 - @now/python@0.2.17-canary.9
 - @now/ruby@0.1.5-canary.5
 - @now/static-build@0.9.9-canary.19
2019-11-07 23:28:56 +01:00
Andy
9adbb10fca [now-cli][now-client] Use v11 endpoint for functions (#3266)
* [now-cli][now-client] Use v11 endpoint for functions

* Adjust now-client

* Change version check

* Fix typescript in test
2019-11-07 23:28:14 +01:00
Andy
6646b97a65 Revert "[now-client] ignores is missing ignores added from .nowignore (#3186)" (#3267)
This reverts commit 2a39f61f34.
2019-11-07 22:54:31 +01:00
Michel Moreau
939c3a11d7 [docs] Update domain-verification error message (#3263)
Closes #3263.
2019-11-07 13:04:56 -08:00
Steven
363c3ea39f [all] Update homepage in package.json (#3265) 2019-11-07 14:31:07 -05:00
luc
935fca6986 Publish
- now-client@5.1.1-canary.14
 - @now/static-build@0.9.9-canary.18
2019-11-08 02:08:12 +08:00
Luc
7e20c585a1 [now-static-build] Cache .cache folder for gatsby deployments (#3260)
Fix https://github.com/zeit/now/issues/3247.

Adds `.cache` folder to the Now cache for Gatsby deployments.

Also adds a generic optional `cachePattern` property to the frameworks array so we can optimize cache paths for other frameworks in the future.
2019-11-07 17:58:28 +00:00
Max
7ecdfbb043 [now-client] Add .nowignore test (#3264)
This adds a test for https://github.com/zeit/now/pull/3186
2019-11-07 17:23:11 +00:00
Gerhard Sletten
2a39f61f34 [now-client] ignores is missing ignores added from .nowignore (#3186)
After upgrading to macOS Catalina I get this error on some now-deploys:

```
> Error! ENOENT: no such file or directory, stat '<file that not exist>'
```

and the file is below a folder that should be ignored by `.nowignore`, think this is due to some fs-bug in my node version, but did also discover that the ignore-array returned from `getNowIgnore` did not include the ignores from my `.nowignore`, but only the default defined in the top of the function.

Is this a bug?
2019-11-07 16:30:13 +00:00
Steven
6e0182af02 Publish
- now@16.1.4-canary.34
 - @now/routing-utils@1.2.3-canary.11
2019-11-07 10:48:41 -05:00
Steven
1fd630460c [now-dev][now-routing-utils] Fix trailingSlash (#3250)
Fixes `trailingSlash: true` in now dev
2019-11-07 15:27:53 +00:00
Andy Bitz
4583880377 Publish
- @now/build-utils@1.0.0-canary.19
 - @now/cgi@0.1.5-canary.2
 - now@16.1.4-canary.33
 - @now/go@0.5.11-canary.6
 - @now/next@1.0.0-canary.19
 - @now/node@1.0.0-canary.11
 - @now/python@0.2.17-canary.8
 - @now/ruby@0.1.5-canary.4
 - @now/static-build@0.9.9-canary.17
2019-11-07 14:32:35 +01:00
Andy
abacaf8d40 [all] Fix functions property and update Lambda (#3252)
* Allow functions + next.js

* Don't allow empty or invalid functions

* Make sure runtimes match a source

* Update now-dev to use the functions property

* Functions must match a source file

* Split up functions

* Make sure @now/next does not receive any unused functions

* Allow memory and maxDuration properties on Lambdas

* Add lambda options to @now/node

* Add lambda options to @now/go

* Add lambda options to @now/python

* Add lambda options to @now/ruby

* Update lambda options on @now/node

* Add lambda optiosn to @now/cgi

* Make options optional

* Add lambda options to @now/next

* Fix assertion

* Add test

* Fix tests

* Skip 06-ruby test

* Skip correct tests

* Fix options and cache src check

* Adjust memory test
2019-11-07 14:31:51 +01:00
Steven
de8829ccfe [now-cli] Fix integration test for maxDuration (#3259)
This test started failing because the error message changed.
2019-11-07 12:26:23 +00:00
Cam Smith
ba18a3a0cb [now-cli] Add note for Windows environment variable syntax (#3243) 2019-11-06 17:34:07 -08:00
Leo Lamprecht
0a0f13994f [docs] Rename "Builders" to "Runtimes" (#3256)
* Renamed "Builders" to "Runtimes"

* Brought html-minifier back

* Adjusted examples
2019-11-06 21:20:01 +01:00
Matthew Sweeney
eff29463e7 Update Public Directory Link (#3221)
This PR updates the link to the missing build script FAQ, pending a change to the structure of the documentation.

This PR should be merged only when the documentation PR has been - https://github.com/zeit/docs/pull/1423
2019-11-05 21:39:32 +00:00
Connor Davis
9c62b74122 Publish
- @now/next@1.0.0-canary.18
2019-11-05 11:26:00 -05:00
Connor Davis
5f19e63409 [now-next] 404 Known Static Files Before Dynamic Routes (#3238)
If someone uses a wildcard in the root of pages, it could collide with internal `_next` files:
```
pages/
  [slug]/
    [slug]/
      [slug]/
        [slug].js
```

For example:
`/_next/static/runtime/webpack-hash.js` exists and has no problem routing
`/_next/static/runtime/webpack-old-hash.js` doesn't exist on the server any more but would route to `/[slug].js` with status 200

This could cause strange edge cases related to loading a new version of a Next.js site along with the possible cost of execution if it's a lambda plus the cost of however large the response is.

# Side Notes

Should this:
92d9f2d809/packages/now-next/src/index.ts (L839)

Instead be this?
```
 src: path.join('/', entryDirectory, '_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'),
```

If this is the case, I need to adjust my PR to do the same
2019-11-05 14:53:57 +00:00
Sophearak Tha
1fd4c3a278 Publish
- now@16.1.4-canary.32
2019-11-05 09:35:53 +07:00
Sophearak Tha
281b385188 [now-cli] Adjust now ls according to new project overview (#3229)
Fix: [PRODUCT-493]

[PRODUCT-493]: https://zeit.atlassian.net/browse/PRODUCT-493
2019-11-05 02:25:01 +00:00
Steven
4e4b672af9 [now dev] Add support for cleanUrls (#3240)
This PR implements `cleanUrls` for now dev which is similar to the implementation in fmeta-util.

I also added an integration test to ensure correctness.
2019-11-04 23:42:21 +00:00
Steven
b70ac1ca1c [now-cli] Fix test invalid-builder-routes (#3249) 2019-11-04 17:31:20 -05:00
Sophearak Tha
36fc64d0ed [now-cli] Remove now upgrade command (#3228)
Fix: [PRODUCT-489]

[PRODUCT-489]: https://zeit.atlassian.net/browse/PRODUCT-489
2019-11-04 15:24:49 +00:00
Steven
2dee810e74 [now-dev] Fix route normalization (#3225)
This PR fixes a regression introduced in #3174 when removing the `^` and `$` normalization.

The previous PR was normalizing user-defined routes but forgot to normalize builder routes.

This PR normalizes builder routes 👍
2019-11-01 20:57:17 +00:00
Andy Bitz
4fa468299f Publish
- @now/build-utils@1.0.0-canary.18
 - now@16.1.4-canary.31
2019-11-01 19:21:53 +01:00
Andy
90eac2cf54 [now-build-utils] Fix custom function runtimes (#3239)
* [now-build-utils] Fix custom runtimes for functions

* [now-cli] Change order
2019-11-01 19:20:56 +01:00
Andy Bitz
63ad08074c Publish
- now@16.1.4-canary.30
 - now-client@5.1.1-canary.13
2019-11-01 17:49:59 +01:00
Andy
31fcc56dbd [now-cli][now-client] Add support for functions property (#3231)
* [now-cli][now-client] Add support for `functions` property

* Fix typo

* Update yarn.lock for test

* Update all yarn.lock files for now dev

* Log fixture that failed

* Use catch instead

* Run dev tests not serial

* Revert "Run dev tests not serial"

This reverts commit bfcd83642bcd2275daaac129b2c8b233f582eaae.

* Do not throw

* Skip nextjs tests on node 8

* Remove only flag

* Ignore 19-mithril

* Revert "Ignore 19-mithril"

This reverts commit d438c40c26a8ef2227a0a0dd7caba8600503d585.

* Revert "Remove only flag"

This reverts commit caff05ad82a184706eb48b6b39df550f8d17bb1d.

* Revert "Skip nextjs tests on node 8"

This reverts commit 3b45ca33b969a56da0bccce4c95cae3b34af98e0.

* Revert "Do not throw"

This reverts commit 55624b9193d7751e1dc16cbee9005fe23ff19662.

* Revert "Revert "Run dev tests not serial""

This reverts commit cd5260a205b6478cb7a44c9ea982b99f26f2f2e9.

* Revert "Run dev tests not serial"

This reverts commit bfcd83642bcd2275daaac129b2c8b233f582eaae.

* Revert "Use catch instead"

This reverts commit fe652995c08e1e8b2ba581aaf7304b5432718161.

* Revert "Log fixture that failed"

This reverts commit feb0e7b393626ce1c117ef95f9e95f7a148e3dab.

* Revert "Update all yarn.lock files for now dev"

This reverts commit 36be4dd98ca6d65850843dc958727952dd7461ee.

* Update yarn.lock for 14-svelte-node

* Update es-abstract in all yarn.lock files

* Skip node test

* Revert "Skip node test"

This reverts commit c9c45ec8d6fcef13cbd300db410699b167d76ed2.

* Remove test.only

* Only execute node test in now-dev

* Revert "Only execute node test in now-dev"

This reverts commit 8ab7a88d696c1faa5fe0fbcca9dcfb0dd375925f.

* Clean cache on macos node8

* Fix query

* Use --skip-integrity-check

* Add --skip-integrity-check to different yarn call

* Add --network-concurrency 1

* Add retry
2019-11-01 17:47:50 +01:00
Max Rovensky
958b067303 Publish
- now@16.1.4-canary.29
2019-11-01 21:32:49 +08:00
Max
6af183c7f1 Fix racing spinners (#3232) 2019-11-01 21:30:25 +08:00
Steven
92d9f2d809 Publish
- @now/build-utils@1.0.0-canary.17
 - @now/routing-utils@1.2.3-canary.10
2019-10-31 13:13:40 -04:00
Steven
4d7aaff8d0 [now-routing-utils] Fix newline in error message (#3234)
The newlines were not being applied properly since the entry array was stringified.

This PR fixes the newlines for route errors.
2019-10-31 16:49:05 +00:00
Steven
68b19c8122 [now-build-utils] Fix zero config trailing slash (#3233)
This fixes the scenario where the user defines `trailingSlash: true` and creates a file `/api/users.js`. They would expect to be able to visit `/api/users/` and it should run that function.
2019-10-31 16:27:29 +00:00
Steven
5b600fea44 Publish
- @now/routing-utils@1.2.3-canary.9
2019-10-31 11:24:13 -04:00
Leo Lamprecht
2c867295e6 [now-routing-utils] Improve error message for invalid regexes (#3230)
Currently, this is what you see if you enter invalid regexes with any of the new routing properties:

![image](https://user-images.githubusercontent.com/6170607/67955351-c0fd6480-fbf2-11e9-999c-b36509b5c405.png)

Instead of printing JSON, we want to print a list of all error messages.
2019-10-31 15:22:34 +00:00
Joe Haddad
996c0ffffc Publish
- @now/next@1.0.0-canary.17
2019-10-30 17:47:02 -04:00
Joe Haddad
2344357113 [now-next] Use Past Tense (#3226) 2019-10-30 21:20:51 +00:00
Joe Haddad
e919c591fe Publish
- @now/next@1.0.0-canary.16
2019-10-30 16:45:47 -04:00
Joe Haddad
88516137de [now-next] Reduce Build Noise (#3224)
This PR removes the individual lambda zip prints since they're not very useful. We've already printed this information above and will time them as a group!
2019-10-30 20:43:48 +00:00
Steven
f515138074 Publish
- @now/routing-utils@1.2.3-canary.8
2019-10-30 14:29:11 -04:00
Steven
6d04016054 [now-routing-utils] Fix redirects when cleanUrls and trailingSlash (#3223)
This fixes the scenario when both `{ cleanUrls: true, and trailingSlash: true }` so that only one redirect occurs when visiting `/file.html`.

Previously, this would have redirected twice from `/file.html` => `/file` => `/file/`.

Now it will redirect once from `/file.html` => `/file/`.
2019-10-30 18:27:07 +00:00
Steven
eb968d813c Publish
- @now/routing-utils@1.2.3-canary.7
2019-10-30 10:54:16 -04:00
Steven
37881e0830 [now-routing-utils] Return null routes when provided null (#3222)
This PR ensures that null routes are returned as null instead of the empty array.
2019-10-30 14:51:50 +00:00
luc
e3dd13ba9e Publish
- gatsby-plugin-now@1.2.1-canary.3
 - @now/routing-utils@1.2.3-canary.6
 - @now/static-build@0.9.9-canary.16
2019-10-30 12:14:09 +08:00
Luc
9054b7f2f5 [now-static-build] Improve Gatsby default routes (#3212)
This PR does 3 things:
- add default caching headers 
- redirect all missed requests to Gatsby's own 404.html page
- add deprecation message to `gatsby-plugin-now` since we're auto injecting it and it's not needed anymore

Fixes #2859
2019-10-30 04:12:06 +00:00
Steven
17cd17ac96 [now-routing-utils] Enhance superstatic validation (#3219)
This PR refactors a few things:

- enhance validation so that invalid regex is caught before transforming to routes
- remove `filePaths` input parameter since it is no longer used

After this PR is merged and released, we'll be able to perform validation earlier in the build pipeline and report the errors to the user.
2019-10-29 23:03:39 +00:00
Andy Bitz
d7c2d071c1 Publish
- @now/build-utils@1.0.0-canary.16
2019-10-29 21:07:06 +01:00
Andy
483a117a6a [now-build-utils] Validate functions and allow them as config (#3218)
* [now-build-utils] Validate functions and allow them as config

* Apply suggestions from code review

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

* Change memory check

* Adjust test
2019-10-29 21:03:04 +01:00
Steven
3c630343d0 Publish
- @now/routing-utils@1.2.3-canary.5
2019-10-29 10:38:40 -04:00
Steven
fe61b31197 [now-routing-utils] Fix html redirects for cleanUrls (#3217)
We used to read the output files and create a route for each redirect when `cleanUrls: true`.

Instead, this PR will add 2 redirects for `cleanUrls: true` no matter how many files are in the outputs.
2019-10-29 14:23:23 +00:00
Joe Haddad
af14d0af4b Publish
- @now/build-utils@1.0.0-canary.15
 - now@16.1.4-canary.28
 - @now/next@1.0.0-canary.15
2019-10-28 21:28:24 -04:00
JJ Kasper
e04e6bb188 [now-next] Add Beta iSSG Support (#3067)
Adds support for new SPRv2 handling in Next.js.

x-ref: #3021
2019-10-29 01:26:42 +00:00
Andy
e36f3d355e [now-build-utils] Add functions property (#3213)
This updates `@now/build-utils` to add support for the function property including types and tests.

Related: [PRODUCT-27]

[PRODUCT-27]: https://zeit.atlassian.net/browse/PRODUCT-27
2019-10-28 23:47:45 +00:00
Nathan Rajlich
24891e3ce8 [now-cli] Remove now dev cleanCacheDir() function (#3214)
Dead code as of #2097.
2019-10-28 16:26:13 -07:00
Steven
0d29e37dcb Publish
- now@16.1.4-canary.27
 - @now/routing-utils@1.2.3-canary.4
2019-10-28 10:40:03 -04:00
Steven
089b29c2ca [now-routing-utils] Fix validation for routes/cleanUrls (#3211) 2019-10-28 10:38:58 -04:00
Leo Lamprecht
af513c249c Improve test stability (#3208)
This pull request aims to make our test suite more stable.
2019-10-26 17:58:21 +02:00
Max Rovensky
6151f11657 Publish
- now@16.1.4-canary.26
2019-10-26 05:50:49 +08:00
Max
c0d3847aab Separate out queued into it's own spinner (#3206) 2019-10-26 05:50:15 +08:00
luc
c78af57d76 Publish
- gatsby-plugin-now@1.2.1-canary.2
 - now@16.1.4-canary.25
 - @now/next@1.0.0-canary.14
 - @now/node@1.0.0-canary.10
 - @now/static-build@0.9.9-canary.15
2019-10-25 22:01:19 +02:00
Max
172605d77d Add Queued spinner (#3205)
* Add `Queued` indication before builds begin

* Fix second spinner condition
2019-10-25 21:59:51 +02:00
Michael Zetterberg fd. Lopez
d7dc5a0c45 [gatsby-plugin-now] Fix logic error for redirect check (#3203)
Currently `__now_routes_g4t5bY.json` always gets created, regardless of existing redirects in Gatsby.

This PR fixes that.

PS. According to https://github.com/zeit/now/blob/canary/CONTRIBUTING.md I should run `yarn lint` and `yarn test` but both fail on a clean clone. Not sure how to handle that.

gist for errors:
- [yarn lint](https://gist.github.com/michaellopez/ae187969b64da017de295967cb0d7539)
- [yarn test](https://gist.github.com/michaellopez/024c484bedca8018e933d61b3a2c13ee)
2019-10-25 19:42:20 +00:00
Steven
75edd0bab5 [now-node][now-next] Bump node-file-trace to 0.4.0 (#3204)
Bump `@zeit/node-file-trace` to version [0.4.0](https://github.com/zeit/node-file-trace/releases/tag/0.4.0).
2019-10-25 18:56:31 +00:00
Luc
ac60b3660c [now-static-build] Inject gatsby-plugin-now in Gatsby deployments (#3182)
This PR is an attempt to automatically inject `gatsby-plugin-now` in Gatsby projects, as an optimisation.

- [x] avoid conflicts with existing files
- [x] refactor
2019-10-25 12:54:14 +00:00
Sophearak Tha
6e22c07ecc Publish
- now@16.1.4-canary.24
 - now-client@5.1.1-canary.12
2019-10-25 19:04:10 +07:00
Sophearak Tha
857e4ef874 update now-cli readme (#3202) 2019-10-25 14:02:41 +02:00
Max
aa03f69bdc Add apiUrl debug log to now-client (#3201) 2019-10-25 13:38:58 +02:00
Sophearak Tha
8fc77139e0 Publish
- now@16.1.4-canary.23
2019-10-25 18:04:04 +07:00
Luc
87802cb002 Improve styling of 1.0 message (#3200) 2019-10-25 12:38:02 +02:00
Sophearak Tha
dcd57e148f Publish
- @now/build-utils@1.0.0-canary.14
 - now@16.1.4-canary.22
 - now-client@5.1.1-canary.11
2019-10-25 11:32:43 +07:00
Sophearak Tha
18fa193a17 [now-client] [now-cli] Update version of deployment API (#3188)
* Update version of deployment API

* Add `alias-assigned` event and handling

* Replace v9 api with v10

* Don't return on immediate ready

* Handle alias-assigned for v1 deployments

* Improve event ordering

* Detect upload deployment readiness by `alias-assigned`

* rebuild

* Fix upload readiness event type

* Check for aliases before running status checks

* Improve event flow and wait for `ready` in v1 deployments

* Remove console.log
2019-10-25 11:28:36 +07:00
Nathan Rajlich
34dce350be [now-cli] Loosen "engines" requirement to Node >= 8 (#3195)
Now that `now-client` does not use `fetch-h2`, any version of Node 8 or newer should work with `now-cli`.

Related to #2711.
2019-10-25 00:38:21 +00:00
Nathan Rajlich
c7caa7b905 [now-build-utils] Add more complete PackageJson typings (#3198)
* [now-build-utils] Add more complete `PackageJson` typings

Planning on using this in `now-cli`.

* Make "name" optional

* Use a namespace
2019-10-24 16:02:16 -07:00
Steven
7b52b26ff0 Publish
- now@16.1.4-canary.21
 - now-client@5.1.1-canary.10
 - @now/next@1.0.0-canary.13
 - @now/python@0.2.17-canary.7
 - @now/routing-utils@1.2.3-canary.3
2019-10-24 16:09:06 -04:00
Steven
3c958a4429 [now-routing-utils] Change cleanUrls to redirect only (#3196)
When `cleanUrls` is true, the redirects will be applied to the routes however there are no longer any rewrites. Instead (through a different PR to fmeta-util) we will rename the file output to remove the `.html` extension.
2019-10-24 20:07:16 +00:00
Steven
d8660b1db3 Fix circle ci xcode version (#3197) 2019-10-24 15:34:38 -04:00
Nathan Rajlich
0996640798 [now-cli] Gracefully exit when the cwd does not exist (#3194)
There have been Sentry errors where `process.cwd()` fails and throws an error. This patch handles that scenario gracefully by printing a more clear error message to the user and avoids sending a report to Sentry.

Fixes #3193.
2019-10-24 13:15:27 +00:00
Nathan Rajlich
079f4ba6bf [now-cli] Don't throw if builder child process can not be killed (#3192)
The Sentry error reports that the process has already been killed, so no need to throw in this case.

Fixes #3191.
2019-10-23 21:23:03 +00:00
Luc
bdf78efbfb [now-cli] Disable --prod and --target for Now 1.0 deployments (#3189)
This PR disables Now 1.0 production deployments with the following error message:

> Option --prod is not supported for Now 1.0 deployments. To manually alias a deployment, use `now alias` instead.

It looks like this:
<img width="835" alt="Capture d’écran 2019-10-23 à 19 42 09" src="https://user-images.githubusercontent.com/6616955/67419574-3e125380-f5cd-11e9-81ff-63bde292539b.png">

Also disables `--target` for Now 1.0 deployments.
2019-10-23 20:26:09 +00:00
Joe Haddad
07ba180ede [now-next] Cache All Next.js Directories (#3190)
Next.js now uses the `css` and `media` folders for its build-in CSS support. These files should be cached forever.
2019-10-23 17:34:13 +00:00
Max
94a2b017d4 [now-client] Add apiUrl support (#3168)
This PR adds support for `apiUrl` option to `now-client` as well as a test for it
2019-10-23 09:32:47 +00:00
Yevhen Amelin
37c06c89d0 [now-python] Set PIP_USER environment variable (#3111)
Disables `--user` parameter of the `pip` utility, which is forcibly set under the hood on Debian systems and causes an error in the `pipInstall` function:

`distutils.errors.DistutilsOptionError: can't combine user with prefix, exec_prefix/home, or install_(plat)base`

Fixes #3089
2019-10-22 22:26:54 +00:00
Max
42ffc2dafb [now-client] Yield an error event if path provided is not absolute (#3173)
This adds an `error` event when provided path to `now-client` isn't absolute
2019-10-22 20:19:11 +00:00
Steven
cf65fabe27 [now-client] Change npm publish to use files key (#3181)
- remove `.npmignore`
- use [`files`](https://docs.npmjs.com/files/package.json#files) key in `package.json`
- update metadata in `package.json`
- fix test harness to generate a token for each test deployment

This PR will prevent publishing [tests](https://cdn.jsdelivr.net/npm/now-client@5.2.0/tests/) to npm and any other unused files.
2019-10-22 15:30:57 +00:00
Steven
dc9b9c240a [now-cli] Add support for new keys in now dev (#3174)
This PR is a followup to #3138 so that `now dev` will validate and transform the following `now.json` config keys to routes:

- cleanUrls
- rewrites
- redirects
- headers
- trailingSlash

[PRODUCT-341] #close

[PRODUCT-341]: https://zeit.atlassian.net/browse/PRODUCT-341
2019-10-21 21:55:28 +00:00
Steven
84d8f73a2d Publish
- @now/build-utils@1.0.0-canary.13
2019-10-21 13:35:03 -04:00
Steven
b24cfb605b [now-build-utils] Add contentType prop to File (#3178)
This PR adds a `contentType` to the File interface.

This is necessary for [PRODUCT-341] to work properly with `cleanUrls` which will strip the file name but we will still need specify `contentType: 'text/html'`.

This is required for @dav-is `api-builds` PR 633 to work properly.

[PRODUCT-341]: https://zeit.atlassian.net/browse/PRODUCT-341
2019-10-21 17:27:46 +00:00
Steven
57f66fe62f Publish
- now@16.1.4-canary.20
 - @now/node@1.0.0-canary.9
2019-10-21 11:57:06 -04:00
Steven
d2cd4a3c5a [now-node] Fix helpers when POST json has empty body (#3177)
The `@now/node` helpers json parsing is too strict and doesn't match the behavior of Express when an incoming request has `{ method: 'POST', Content-Type: 'application/json', body: '' }`.

Instead of returning 400, this PR will continue with `body = {}` to match Express.

Fixes https://spectrum.chat/zeit/now/klarna-with-zeit-now~60852003-4db6-4ec4-a611-83b2349ece08
2019-10-21 15:45:38 +00:00
Nathan Rajlich
110e1445fa [now-cli] Temporarily force TooTallNate/signal-exit#update/sighub-to-sigint-on-windows for "signal-exit" module (#3171)
Fixes https://github.com/zeit/now/issues/2564.

Upstream fix PR: https://github.com/tapjs/signal-exit/pull/55.
2019-10-18 15:53:00 +00:00
Nathan Rajlich
f9355cbc77 [now-cli] Remove serveProjectAsStatic() (#3170)
Because we treat purely static projects as v2 projects with a `@now/static` builder specified, this is now dead code.
2019-10-18 13:25:01 +00:00
Steven
0a9bbd902f Publish
- @now/build-utils@1.0.0-canary.12
2019-10-17 14:41:00 -04:00
Steven
c8c960f785 [now-build-utils] Fix static file serving from now dev (#3167)
This PR does a few things

- Separate tests into `integration.test.js` and `unit.test.js`
- Use one static build instead of many to avoid `should NOT have more than 128 items` error
- Add a new unit test so we don't regress

Fixes #3159
2019-10-17 18:38:25 +00:00
Steven
fb0f4beca6 Publish
- now@16.1.4-canary.19
 - @now/next@1.0.0-canary.12
 - @now/routing-utils@1.2.3-canary.2
2019-10-17 08:32:17 -04:00
Nathan Rajlich
21398d7214 [now-cli] Spawn builder child processes with stdio: 'inherit' in now dev (#3113)
Inherit the `now dev` process stdio streams in builder child processes, so that ANSI color codes may be used when stdout is a TTY.

**Examples:**

_Next.js_

<img width="523" alt="Screen Shot 2019-10-01 at 3 42 33 PM" src="https://user-images.githubusercontent.com/71256/66006087-4fe75780-e462-11e9-927f-1e81466c4108.png">

_Gatsby_ (depends on #3112)

<img width="507" alt="Screen Shot 2019-10-01 at 3 40 02 PM" src="https://user-images.githubusercontent.com/71256/66006094-5d9cdd00-e462-11e9-81da-e60bd9516778.png">

Fixes #3135.
2019-10-17 00:18:14 +00:00
Steven
a5a56836a8 [now-routing-utils] Add superstatic transformations (#3138)
This implements a new function `getTransformedRoutes()` which transforms some [superstatic](https://github.com/firebase/superstatic#configuration) configuration keys to Now routes so we can eventually use this new keys in `now.json`.

In particular, the following new keys are transformed to `routes`.

- cleanUrls
- rewrites
- redirects
- headers
- trailingSlash

[PRODUCT-341] #close

[PRODUCT-341]: https://zeit.atlassian.net/browse/PRODUCT-341
2019-10-16 22:34:52 +00:00
Steven
e7fd1e3c3f [tests] Fix tests so they wait for v1 deployments (#3162)
Our tests periodically fail because we're creating too many v1 deployments on a free plan and it times out.

This limits the number of deployments by running v1 exclusively under Node 12.

It also adds a missing waitForDeployment().
2019-10-16 21:47:31 +00:00
Joe Haddad
64b24bde56 [now-next] Update console.time labels for clarity (#3157) 2019-10-15 12:39:40 +00:00
Allen Hai
613958d6a5 Publish
- now@16.1.4-canary.18
 - @now/python@0.2.17-canary.6
 - @now/static-build@0.9.9-canary.14
2019-10-14 19:08:32 -05:00
Allen Hai
21e4ebc7a9 [now-static-build] Add stencil to list of optimized static frameworks (#3158)
This PR enables a zero-config deployment experience for `stencil` projects. The default build command outputs to a `www` directory.
2019-10-14 23:49:59 +00:00
JJ Kasper
16ef6229e4 Add test for now dev and Next.js src dir (#3149)
Follow up on #3140 we needed to publish the change before we could test it in `now dev`
2019-10-10 20:36:24 +00:00
Jacob Mischka
9ecb011732 [now-python] Fix UnicodeDecodeError for binary response from handler (#3148)
Fixes #3147
2019-10-10 16:51:37 +00:00
JJ Kasper
de48e28fa1 Publish
- @now/build-utils@1.0.0-canary.11
 - now@16.1.4-canary.17
 - @now/next@1.0.0-canary.11
2019-10-10 10:43:51 -05:00
JJ Kasper
3aecb0905a [now-next] Add support for src dir in now-dev (#3140)
Fixes: #3133
Fixes: https://github.com/zeit/next.js/issues/9007
2019-10-10 14:40:38 +00:00
Joe Haddad
c1b2da1d57 [now-build-utils] Allow a null fallback in Prerender (#3144)
This allows a `null` `fallback` to be provided to a `Prerender`. The use case is a lazily prerendered route (often meaning dynamically rendered).
2019-10-10 05:07:26 +00:00
Nathan Rajlich
c4bee64abd [now-cli] Set the builder's debug env var based on DevServer.debug (#3139)
This makes it so that a programatically created `DevServer` instance that has `debug` mode enabled also gets set on the builder child processes as expected, rather than only when invoked via CLI.

For example, the `dev-server.unit.js` tests can set `debug: true` and with this change the builder child processes will also have debug logs enabled. See [here](https://git.io/JeW0O).
2019-10-09 01:19:10 +00:00
Nathan Rajlich
dbf9c5c46b Publish
- @now/build-utils@1.0.0-canary.10
 - @now/next@1.0.0-canary.10
 - @now/static-build@0.9.9-canary.13
2019-10-08 16:04:24 -07:00
Nathan Rajlich
630ec06d48 [now-static-build] Exit dev server child processes upon SIGINT/SIGTERM (#3136)
Explicitly send the SIGINT / SIGTERM signal to `now dev` server child processes, so that they are not left running when running the now-dev unit tests.

Related to #3113 which has hanging unit tests that never "complete".
2019-10-08 22:57:29 +00:00
Nathan Rajlich
d38e464bfe [now-next] Exit dev server child processes upon SIGINT/SIGTERM (#3137)
Explicitly send the SIGINT / SIGTERM signal to `now dev` server child processes, so that they are not left running when running the now-dev unit tests.

Related to #3113 which has hanging unit tests that never "complete".
2019-10-08 22:25:47 +00:00
Steven
49bd2439a7 [now-build-utils] Fix typo occurres => occurs (#3132)
Fixes #2931
2019-10-07 20:44:44 +00:00
Sophearak Tha
e97314a3c7 Publish
- now@16.1.4-canary.16
2019-10-04 20:25:15 +07:00
Sophearak Tha
acb46cef14 [now-cli] Update readme (#3128) 2019-10-04 20:23:45 +07:00
Sophearak Tha
06b9ff233e Publish
- now-client@5.1.1-canary.9
2019-10-04 19:59:22 +07:00
Andy
a189e72fbe Revert "[now-cli] Remove dev: "now dev" script detection logic in now dev (#3088)" (#3127)
This reverts commit 85170d7231.
2019-10-04 14:54:27 +02:00
Sophearak Tha
75e6b15199 [now-client] Fix ENOENT regression in now-client (#3125)
This PR should fixes `ENOENT` related errors

```
> Error! ENOENT: no such file or directory, stat '.../node_modules/.bin/...'
```

Related: https://github.com/zeit/now/issues/3104
2019-10-04 10:25:26 +00:00
Nathan Rajlich
85170d7231 [now-cli] Remove dev: "now dev" script detection logic in now dev (#3088)
As of https://github.com/zeit/now-builders/pull/679, this logic is unnecessary because the `@now/static-build` builder will never end up executing the `dev` script when there is a `now.json` file present (and thus, no builds present, aka zero config mode).

Also, statically detecting the `now dev` command from the script command is brittle, as the command could execute a separate shell script that ends up executing `now dev` (and this detection logic would be a false negative).
2019-10-03 21:29:54 +00:00
Steven
eeffb55021 Publish
- now@16.1.4-canary.15
 - now-client@5.1.1-canary.8
2019-10-03 13:44:39 -04:00
Steven
1fe8317f1e [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 17:41:37 +00:00
Sophearak Tha
6398ef194c [now-client] [now-cli] Handle notice type (#3122)
This PR handle `notice` type from API respond.
2019-10-03 14:58:00 +00:00
Steven
e4d691eda1 Publish
- now@16.1.4-canary.14
 - now-client@5.1.1-canary.7
 - @now/next@1.0.0-canary.9
 - @now/python@0.2.17-canary.5
 - @now/static-build@0.9.9-canary.12
2019-10-03 08:24:31 -04:00
Steven
d9bb6c8a54 [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 02:17:11 +00:00
Chris
c3020e3071 [now-python] Encode body as utf-8 before making a request (#3093)
Fixes #3091
2019-10-02 18:04:16 -04:00
Sophearak Tha
e3f31e3e52 [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-02 10:35:01 +00:00
Nathan Rajlich
8c40e1be0f [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-02 09:42:01 +00:00
Steven
b278c7148b Publish
- now@16.1.4-canary.13
 - now-client@5.1.1-canary.6
 - @now/next@1.0.0-canary.8
2019-10-01 21:36:12 -04:00
Steven
2791338e04 [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-02 01:28:58 +00:00
Steven
0af87dc2be 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-01 20:54:04 +00:00
Luis Alvarez D
32134dd1f5 [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-01 18:57:41 +00:00
Mark
bdd4441d5c [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-01 16:44:02 +00:00
Ana Trajkovska
99bc1ae7b6 [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-09-30 23:16:40 +00:00
Leo Lamprecht
32aa94bf3d Publish
- now@16.1.4-canary.12
2019-09-30 11:13:18 +02:00
Andy
3201227430 [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-28 15:01:22 +00:00
Max Rovensky
09a421c23b Publish
- @now/build-utils@1.0.0-canary.9
 - now@16.1.4-canary.11
 - now-client@5.1.1-canary.5
 - @now/next@1.0.0-canary.7
 - @now/static-build@0.9.9-canary.11
2019-09-28 03:21:27 +08:00
Max
73ce59b492 [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-27 19:19:35 +00:00
Steven
7911d5857d [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-27 14:16:48 +00:00
Max
05a44a0c70 [now-cli] Add not_domain_owner error handling (#3045)
This adds missing error handler for `not_domain_owner` error (fixes #3042)
2019-09-27 12:40:51 +00:00
Nathan Rajlich
efd8863f8b [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-27 14:02:35 +08:00
Sophearak Tha
6699028342 [now-next] Invoke build script (#3073)
This PR make `@now/next` invoke `build` script if user defined.

PRODUCT-106 #close
2019-09-27 02:09:45 +00:00
Leo Lamprecht
aa9a234a0b [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-26 20:48:04 +00:00
Leo Lamprecht
91f836818c [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-26 18:20:56 +00:00
Max
5e37bdc54c [now-cli] Add logging to execa calls in tests (#3077)
This implements #3075 for all integration tests
2019-09-26 15:13:03 +00:00
Max
31c48df795 [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-26 14:17:54 +00:00
Max
0383b9112b [now-cli] Move hexo test to testFixtureStdio (#3076)
This should fix the `now dev` tests that periodically hang
2019-09-26 05:33:43 +00:00
Max
f4fe8be4df [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-25 14:38:50 +00:00
Sophearak Tha
cc643e373d [now-cli] Render prompt when deploying home directory (#3057)
Fixes #3069 

PRODUCT-160 #close
2019-09-25 05:34:27 +00:00
Steven
651429cb52 [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-24 22:36:06 +00:00
Steven
a374a5ce96 Use PR description for merge commit body (#3071) 2019-09-24 17:53:14 -04:00
Steven
bbb4501e9b Publish
- now@16.1.4-canary.10
 - @now/node@1.0.0-canary.8
 - @now/static-build@0.9.9-canary.10
2019-09-24 17:15:45 -04:00
Steven
947f9093be [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-24 21:14:00 +00:00
Steven
42f16b6d1e [tests] Add benchmark script to randomly generate projects files (#3062) 2019-09-24 19:31:44 +00:00
Steven
9ae747a612 [now-node] Fix sharp test using lock file (#3064) 2019-09-24 01:47:46 +00:00
Steven
9217c5e436 [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-23 15:15:28 +00:00
Sophearak Tha
d23e7b1054 [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-23 14:03:30 +00:00
Sophearak Tha
d23eab61cf [now-cli] Fix 02-angular-node test fail (#3058)
* Improve `02-angular-node` test

* Add `yarn.lock` with pin version
2019-09-23 20:30:27 +07:00
Steven
e631de4cfe Publish
- @now/build-utils@1.0.0-canary.8
 - now@16.1.4-canary.9
 - @now/python@0.2.17-canary.4
 - @now/static-build@0.9.9-canary.9
2019-09-21 17:20:40 -04:00
Steven
0814bef36f [now-python] Fix headers with multiple values (#3053)
* [now-python] Add format_headers()

* Add tests

* Fix filenames

* Fix test probes
2019-09-20 21:55:39 +00:00
Andy
ddce65416c [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-20 13:26:55 +00:00
Sophearak Tha
9e66f9bb08 [now-build-utils] Remove NOW_BUILDER_ANNOTATE (#3027) 2019-09-20 09:07:36 +00:00
Steven
961fbfde55 Publish
- @now/build-utils@1.0.0-canary.7
 - @now/go@0.5.11-canary.5
 - @now/next@1.0.0-canary.6
 - @now/node@1.0.0-canary.7
 - @now/python@0.2.17-canary.3
 - @now/static-build@0.9.9-canary.8
2019-09-19 15:58:01 -04:00
Steven
22550c0c48 [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-19 19:44:38 +00:00
Sophearak Tha
4336f48d22 [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-19 17:21:43 +00:00
Steven
cf299562e3 Publish
- @now/static-build@0.9.9-canary.7
2019-09-19 11:17:33 -04:00
Steven
2cd5b35603 [now-static-build] Add prepareCache() function (#3047) 2019-09-19 15:11:37 +00:00
Steven
81abab81f7 [now-static-build] Add support for hugo extended (#3043) 2019-09-19 13:27:01 +00:00
Max Rovensky
ad0d7bd560 Publish
- @now/build-utils@1.0.0-canary.6
 - now@16.1.4-canary.8
 - @now/node@1.0.0-canary.6
 - @now/static-build@0.9.9-canary.6
2019-09-19 19:36:51 +08:00
Nathan Rajlich
140d10e87b [now-cli] Update @zeit/fun to v0.10.2 (#3038)
Fixes #2901.
2019-09-18 10:11:12 +00:00
Nathan Rajlich
6bebc49607 [now-cli] Render logs containing "warning" as yellow (#3035) 2019-09-17 23:14:34 +00:00
Steven
a3aa855290 [docs] Update CI badge to use master (#3037) 2019-09-17 22:33:02 +00:00
Steven
a853cb84cd [tests] Add env var FORCE_BUILD_IN_REGION (#3036)
* [tests] Add env var FORCE_BUILD_IN_REGION

* Add missing config
2019-09-17 21:56:46 +00:00
Steven
a07082ca5c [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 19:14:39 +00:00
Leo Lamprecht
81c27771bc [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:28:04 +00:00
Andy
502f78e835 [now-cli] Fix fetch body and add secrets tests (#3030) 2019-09-17 13:21:18 +00:00
Nathan Rajlich
d8935cf121 [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 06:58:43 +00:00
Nathan Rajlich
e3c61ac5b7 [now-cli] Render "stderr" as red in now logs (#3026) 2019-09-17 06:24:34 +00:00
Andy
e8e95b8df6 [now-cli] Adjust the printed version for the deployment (#3014) 2019-09-14 01:15:39 +00:00
Steven
f55be4f2de [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-13 23:35:40 +00:00
Steven
ef081cc4b8 [now-node] Change trace errors to warnings (#3016) 2019-09-13 22:40:26 +00:00
Max Rovensky
8e2444d3cd Publish
- now@16.1.4-canary.7
 - now-client@5.1.1-canary.4
2019-09-14 02:32:19 +08:00
Max
f26407e282 [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-13 18:23:34 +00:00
Andy
cded895bf7 [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-13 01:59:49 +00:00
Steven
1211ee4fb3 Publish
- now@16.1.4-canary.6
 - @now/go@0.5.11-canary.4
 - @now/node@1.0.0-canary.5
 - @now/ruby@0.1.5-canary.3
2019-09-12 19:29:03 -04:00
Steven
a42bdefe1a [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-12 23:26:11 +00:00
Steven
f025f1007b Add release notes script to PUBLISHING.md (#3006) 2019-09-12 18:26:21 +00:00
Andy
1c11a57371 [now-cli] Default to empty string for undefined env var when checking (#3008) 2019-09-12 17:48:32 +00:00
Andy
db2d033484 [now-cli] Add changelog link to update message (#3001) 2019-09-12 16:15:33 +00:00
Max
40537c9eba [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-12 14:40:46 +00:00
Steven
5cabcb7a27 [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-12 13:34:54 +00:00
Nathan Rajlich
1be9ac59f0 [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-12 00:12:29 +00:00
Andy
64356baed3 [now-cli] Add tests for alias rules (#3003) 2019-09-11 23:32:20 +00:00
Sophearak Tha
c944706a0f [now-go] Improve test for custom build flags (#2999) 2019-09-11 16:23:54 +00:00
Nathan Rajlich
f15deaa51e [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-11 15:47:56 +00:00
Steven
61c3b94460 Publish
- @now/build-utils@1.0.0-canary.5
 - @now/static-build@0.9.9-canary.5
2019-09-11 10:32:20 -04:00
Steven
89b018240f [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-11 10:28:24 -04:00
Steven
0124dc9969 Publish
- now@16.1.4-canary.5
 - @now/next@1.0.0-canary.5
 - @now/node@1.0.0-canary.4
2019-09-10 12:38:03 -04:00
Steven
936441c8a6 [now-node][now-next] Bump node-file-trace to 0.3.1 (#2990) 2019-09-10 16:12:24 +00:00
Sophearak Tha
c80570e096 [now-cli] Hide Init Duration and XRAY TraceId by default (#2984) 2019-09-10 14:22:08 +00:00
Steven
11c67e1c4c Publish
- @now/build-utils@1.0.0-canary.4
2019-09-10 09:18:57 -04:00
Steven
0d1c0e0f30 [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-09 21:51:46 +00:00
Steven
f50572813e Publish
- gatsby-plugin-now@1.2.1-canary.1
 - @now/build-utils@1.0.0-canary.3
 - now@16.1.4-canary.4
 - now-client@5.1.1-canary.3
 - @now/next@1.0.0-canary.4
 - @now/node@1.0.0-canary.3
 - @now/static-build@0.9.9-canary.4
2019-09-09 13:09:08 -04:00
Steven
648b3a4ae2 [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-09 16:39:40 +00:00
Joe Haddad
efe114fa86 [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-09 14:42:26 +00:00
Max
4445d5e00f [now-client] Fix windows paths handling (#2974)
* Fix windows paths handling in now-client

* Tweak windows paths  handling
2019-09-09 04:05:20 +00:00
Clément ALLAIN
96b3c1ee7f [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-06 23:38:49 +00:00
Andy
e1a770ac29 [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-06 22:02:33 +00:00
Andy
db1a2e6482 [now-cli] Display warning when changing the secret name (#2975) 2019-09-06 10:45:57 +00:00
Nathan Rajlich
ecdde7c367 [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-06 00:35:49 +00:00
Nathan Rajlich
4d31291ea4 [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-05 15:19:17 -07:00
Andy
b70bd670fd [now-build-utils] Throw error on Next.js pages/api and api/ (#2964) 2019-09-05 19:21:59 +00:00
Sophearak Tha
d3017649e0 [now-cli] Add platform in Sentry report (#2960) 2019-09-05 18:57:15 +00:00
Luc
e970584219 [gatsby-plugin-now] Add keywords in package.json (#2965)
* add keywords to gatsby-plugin-now

* add #readme in homepage url
2019-09-05 18:04:32 +00:00
Sophearak Tha
c6205293a9 [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-05 13:19:32 +00:00
Max Rovensky
cb47f7bf4b Publish
- now@16.1.4-canary.3
 - @now/python@0.2.17-canary.2
2019-09-05 17:07:52 +08:00
Nathan Rajlich
a60f107e4b [now-cli] Fix now alias with no arguments (#2959)
Fixes #2941.
2019-09-05 00:09:17 +00:00
Nathan Rajlich
6e43c322cc [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-04 20:33:19 +00:00
Steven
4b6387bdb5 [now-python] Use system python with now dev (#2956) 2019-09-04 18:35:33 +00:00
Steven
bcd770c0fe [now-cli] Bump @zeit/fun to 0.10.0 (#2955) 2019-09-04 18:10:33 +00:00
Max Rovensky
bb8c7e9f3b Publish
- now@16.1.4-canary.2
 - now-client@5.1.1-canary.2
2019-09-05 01:03:31 +08:00
Max
6e469272c3 [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-04 16:50:09 +00:00
Andy Bitz
5e7fa71148 Publish
- now@16.1.4-canary.1
 - @now/next@1.0.0-canary.3
2019-09-04 12:44:04 +02:00
Andy Bitz
12d9a4b4f4 [now-cli] Bump version 2019-09-04 12:42:50 +02:00
Andy
084ab2d0e5 [now-cli] Handle now certs ls for users and teams without certificates (#2945)
* [now-cli] Fix `now certs ls` when the user or team has no certs

* Add tests and move to typescript

* Move index and add to typescript

* Fix reduce function in ls

* Added linebreak

* Update packages/now-cli/src/commands/certs/add.ts

Co-Authored-By: Naoyuki Kanezawa <naoyuki.kanezawa@gmail.com>
2019-09-04 10:35:20 +00:00
Naoyuki Kanezawa
4ca0d936aa Add the nameservers verification check and improve messages (#2943)
* add the nameservers verification check and improve messages

* fix verify message condition
2019-09-04 15:44:49 +09:00
Nathan Rajlich
5d5a55e7e1 [now-cli] Remove now update from --help output (#2951)
Fixes #2940.
2019-09-04 03:30:25 +00:00
Nathan Rajlich
e033e5e0ca [now-cli] Remove scale subcommand from --help (#2923)
Fixes #1998.
2019-09-03 20:08:41 +00:00
Andy
7cb36ef1dd Fix the prettier config (#2946)
* Fix the prettier config

* Try prettier
2019-09-03 18:56:34 +00:00
Andy
9aafb168e9 [now-cli] Install dependencies before running now dev in tests (#2948)
* [now-cli] Install dependencies before running `now dev` in tests

* Check install exit code

* Add more logging

* Add more logging

* More logging

* Include yarn.lock file

* Add lock files to dev fixtures

* Ignore test

* Ignore another test

* Ignore another test

* Whitespace

* Install deps for unit tests

* Whitespace
2019-09-03 18:05:21 +00:00
Andy Bitz
fbb5caf955 Publish
- gatsby-plugin-now@1.2.1-canary.0
 - @now/build-utils@1.0.0-canary.2
 - now@16.1.3-canary.6
 - @now/go@0.5.11-canary.3
 - @now/next@1.0.0-canary.2
 - @now/node@1.0.0-canary.2
 - @now/static-build@0.9.9-canary.3
2019-09-03 09:57:03 +02:00
Andy
3f93da550b [now-build-utils][now-next][now-node] Bump version (#2942)
* [now-build-utils][now-next][now-node] Bump version

* Bump build-utils in @now-cli
2019-09-03 07:53:19 +00:00
Luc
a194e0cc6e [gatsby-plugin-now] Add new package w/support for Gatsby Redirects (#2897)
* add gatsby-plugin-now

* add test

* adjust with lerna

* fix test

* add tests to circleci

* add support for defaultRoutes functions

* add defaultRoutes to gatsby

* fix types

* add test case for gatsby redirects

* remove gatsby, react, react-dom from monorepo deps

* chmod +x build.sh

* add missing build script in fixtures

* do stuff during tests to avoid persistence issue

* move tests setup to build step

* copy gatsby plugin files in test case

* bring back ncc step

* prettier gatsby-plugin-now

* add missing semicolons

* remove eslint, prettier from plugin

* persist build step copied file

* fetch without following redirects

* add files in package.json

* remove force

* fix tests probes

* fetch location is not raw location

* fix test

* add readme

* fix type error

* adjust tests

* add support for `force`

* add tests for `force`

* adjust tests again

* gatsby-plugin-now@1.1.0

* `"` -> `'`

* tweak redirect names in test by precaution

* change file name and delete when consumed

* format files

* gatsby-plugin-now@1.2.0

* Apply suggestions from code review

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

* tests -> test

* add --verbose

* adjust circleci to persist fixtures

* trigger tests

* add repository and homepage in package.json

* glob files after `defaultRoute` invocation
2019-09-03 04:19:32 +00:00
Andy
b007200bcf [now-node][now-next] Skip installing user dependencies for now dev (#2926)
* Skip installing user dependencies for `now dev`

* Update type

* Install dependecies for test

* Add comma-dangle for @now/next

* Revert "Add comma-dangle for @now/next"

This reverts commit 720d5630f309ec44eb65e280af29db5b14bd50eb.

* Add trailing commas

* Reset typescript update

* Add trailing commas

* Bump @zeit/node-file-trace

* Readd trailing comma

* Bump @zeit/node-file-trace in @now/node
2019-09-02 19:35:48 +00:00
Sophearak Tha
05d88da887 [now-cli] Remove annotate from now logs (#2937)
* Remove annotate from `now logs`

* Filter out runtime logs to be consistency with dashboard logs

* Add integration test
2019-09-02 16:32:45 +00:00
Sophearak Tha
0a429bb2f3 [now-go] Add GO_BUILD_FLAGS support for go build custom flags (#2916)
* Add `GO_BUILD_FLAGS` support for go build custom flags

* Using string-argv package

* Simplify condition
2019-09-02 15:45:13 +00:00
Joe Haddad
468ccb7598 [now-next] Create default file with correct target (#2924) 2019-09-02 13:28:36 +00:00
Andy Bitz
e47f6f55dc Publish
- now@16.1.3-canary.5
2019-09-02 15:11:14 +02:00
Nathan Rajlich
17deed91b2 [now-cli] Update pcre-to-regexp to v1.0.0 (#2932)
No improvements, per say, but the module has been converted to
TypeScript so it supplies its own type definitions now, and we
can delete our hand-crafted typings from this repo.
2019-09-02 12:47:25 +00:00
Nathan Rajlich
cc0bd9f0a2 [now-cli] Use xdg-app-paths for now dev cache dir (#2921)
* [now-cli] Use `xdg-app-paths` for `now dev` cache dir

For consistency, because #2877 uses this module.
No need for multiple modules that do the same thing.

* Update `@zeit/fun` to v0.9.3
2019-09-01 14:25:26 +00:00
Steven
f6f99fef25 Publish
- now@16.1.3-canary.4
 - @now/go@0.5.11-canary.2
 - @now/node@0.12.8-canary.3
2019-08-30 14:36:09 -04:00
Nathan Rajlich
eb252edc7f [now-cli] Output --json to stdout for now alias ls <url> (#2922)
Fixes #1337.
2019-08-30 18:29:28 +00:00
Sophearak Tha
f3213dbcdc [now-node] Cleanup debug output (#2927)
* Ensure traced files have annotate present in all lines

* Remove traced file logs

* Remove `compiling es module file` log

* Cleanup debug output
2019-08-30 16:42:23 +00:00
Sophearak Tha
c637da7710 [now-go] Fix analyze.go fails to parse source file with comments (#2911)
* Fix `analyze.go` fails to parse source file with comments

* Add more tests

* Improve fallback

* Improve check for http.HandlerFunc signature`
2019-08-30 15:32:51 +00:00
531 changed files with 146457 additions and 5084 deletions

View File

@@ -1,6 +1,5 @@
version: 2 version: 2
jobs: jobs:
install: install:
docker: docker:
- image: circleci/node:10 - image: circleci/node:10
@@ -11,9 +10,9 @@ jobs:
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v1-dependencies-{{ checksum "yarn.lock" }} - v1-dependencies-{{ checksum "yarn.lock" }}
# fallback to using the latest cache if no exact match is found # fallback to using the latest cache if no exact match is found
- v1-dependencies- - v1-dependencies-
- run: - run:
name: Updating apt packages name: Updating apt packages
command: sudo apt-get update command: sudo apt-get update
@@ -26,6 +25,7 @@ jobs:
- save_cache: - save_cache:
paths: paths:
- node_modules - node_modules
- packages/gatsby-plugin-now/node_modules
- packages/now-build-utils/node_modules - packages/now-build-utils/node_modules
- packages/now-cgi/node_modules - packages/now-cgi/node_modules
- packages/now-cli/node_modules - packages/now-cli/node_modules
@@ -43,6 +43,7 @@ jobs:
root: . root: .
paths: paths:
- node_modules - node_modules
- packages/gatsby-plugin-now/node_modules
- packages/now-build-utils/node_modules - packages/now-build-utils/node_modules
- packages/now-cgi/node_modules - packages/now-cgi/node_modules
- packages/now-cli/node_modules - packages/now-cli/node_modules
@@ -67,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
@@ -75,6 +79,7 @@ jobs:
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: paths:
- packages/gatsby-plugin-now/test/fixtures
- packages/now-build-utils/dist - packages/now-build-utils/dist
- packages/now-cgi/dist - packages/now-cgi/dist
- packages/now-cli/dist - packages/now-cli/dist
@@ -91,6 +96,7 @@ jobs:
- packages/now-routing-utils/dist - packages/now-routing-utils/dist
- packages/now-ruby/dist - packages/now-ruby/dist
- packages/now-static-build/dist - packages/now-static-build/dist
- packages/now-static-build/test/fixtures/10a-gatsby-redirects/plugins
test-lint: test-lint:
docker: docker:
@@ -107,36 +113,14 @@ 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.0.1'
working_directory: ~/repo working_directory: ~/repo
steps: steps:
- checkout - checkout
- attach_workspace: - attach_workspace:
at: . at: .
- run:
name: Update Node.js
command: curl -sfLS install-node.now.sh/8.11 | sh -s -- --yes
- run: - run:
name: Output version name: Output version
command: node --version command: node --version
@@ -221,15 +205,12 @@ jobs:
test-integration-macos-now-dev-node-8: test-integration-macos-now-dev-node-8:
macos: macos:
xcode: '9.2.0' xcode: '9.0.1'
working_directory: ~/repo working_directory: ~/repo
steps: steps:
- checkout - checkout
- attach_workspace: - attach_workspace:
at: . at: .
- run:
name: Update Node.js
command: curl -sfLS install-node.now.sh/8.11 | sh -s -- --yes
- run: - run:
name: Output version name: Output version
command: node --version command: node --version
@@ -345,6 +326,24 @@ jobs:
name: Running Integration Tests Once name: Running Integration Tests Once
command: yarn test-integration-once --clean false command: yarn test-integration-once --clean false
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: Output version
command: node --version
- run:
name: Running Unit Tests
command: yarn test-unit --clean false
coverage: coverage:
docker: docker:
- image: circleci/node:10 - image: circleci/node:10
@@ -384,36 +383,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:
@@ -434,12 +403,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
@@ -515,12 +478,14 @@ workflows:
- test-integration-once: - test-integration-once:
requires: requires:
- build - build
- test-unit:
requires:
- build
filters: filters:
tags: tags:
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
@@ -534,23 +499,8 @@ workflows:
- test-integration-linux-now-dev-node-10 - test-integration-linux-now-dev-node-10
- test-integration-linux-now-dev-node-12 - test-integration-linux-now-dev-node-12
- test-integration-once - test-integration-once
- test-unit
- test-lint - test-lint
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

@@ -1,6 +1,9 @@
node_modules node_modules
dist dist
# gatsby-plugin-now
packages/gatsby-plugin-now/test/fixtures
# now-cli # now-cli
packages/now-cli/@types packages/now-cli/@types
packages/now-cli/download packages/now-cli/download

View File

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

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ packages/now-cli/.builders
packages/now-cli/assets packages/now-cli/assets
packages/now-cli/src/util/dev/templates/*.ts packages/now-cli/src/util/dev/templates/*.ts
packages/now-cli/test/**/yarn.lock packages/now-cli/test/**/yarn.lock
!packages/now-cli/test/dev/**/yarn.lock
packages/now-cli/test/**/node_modules packages/now-cli/test/**/node_modules
packages/now-cli/test/dev/fixtures/08-hugo/hugo packages/now-cli/test/dev/fixtures/08-hugo/hugo
packages/now-cli/test/dev/fixtures/**/dist packages/now-cli/test/dev/fixtures/**/dist

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,3 +1,4 @@
{ {
"singleQuote": true "singleQuote": true,
} "trailingComma": "es5"
}

View File

@@ -1,18 +1,18 @@
# Builders Developer Reference # Runtime Developer Reference
The following page is a reference for how to create a Builder using the available Builder's API. The following page is a reference for how to create a Runtime using the available Runtime API.
A Builder is an npm module that exposes a `build` function and optionally an `analyze` function and `prepareCache` function. A Runtime is an npm module that exposes a `build` function and optionally an `analyze` function and `prepareCache` function.
Official Builders are published to [npmjs.com](https://npmjs.com) as a package and referenced in the `use` property of the `now.json` configuration file. Official Runtimes are published to [npmjs.com](https://npmjs.com) as a package and referenced in the `use` property of the `now.json` configuration file.
However, the `use` property will work with any [npm install argument](https://docs.npmjs.com/cli/install) such as a git repo url which is useful for testing your Builder. However, the `use` property will work with any [npm install argument](https://docs.npmjs.com/cli/install) such as a git repo url which is useful for testing your Runtime.
See the [Builders Documentation](https://zeit.co/docs/v2/advanced/builders) to view example usage. See the [Runtimes Documentation](https://zeit.co/docs/v2/advanced/runtimes) to view example usage.
## Builder Exports ## Runtime Exports
### `version` ### `version`
A **required** exported constant that decides which version of the Builder API to use. A **required** exported constant that decides which version of the Runtime API to use.
The latest and suggested version is `2`. The latest and suggested version is `2`.
@@ -109,7 +109,7 @@ export prepareCache(options: PrepareCacheOptions) {
### `shouldServe` ### `shouldServe`
An **optional** exported function that is only used by `now dev` in [Now CLI](https:///download) and indicates whether a [Builder](https://zeit.co/docs/v2/advanced/builders) wants to be responsible for building a certain request path. An **optional** exported function that is only used by `now dev` in [Now CLI](https:///download) and indicates whether a [Runtime](https://zeit.co/docs/v2/advanced/runtimes) wants to be responsible for building a certain request path.
```js ```js
shouldServe({ shouldServe({
@@ -133,7 +133,7 @@ export shouldServe(options: ShouldServeOptions) {
If this method is not defined, Now CLI will default to [this function](https://github.com/zeit/now/blob/52994bfe26c5f4f179bdb49783ee57ce19334631/packages/now-build-utils/src/should-serve.ts). If this method is not defined, Now CLI will default to [this function](https://github.com/zeit/now/blob/52994bfe26c5f4f179bdb49783ee57ce19334631/packages/now-build-utils/src/should-serve.ts).
### Builder Options ### Runtime Options
The exported functions [`analyze`](#analyze), [`build`](#build), and [`prepareCache`](#preparecache) receive one argument with the following properties. The exported functions [`analyze`](#analyze), [`build`](#build), and [`prepareCache`](#preparecache) receive one argument with the following properties.
@@ -145,78 +145,15 @@ The exported functions [`analyze`](#analyze), [`build`](#build), and [`prepareCa
- `cachePath`: A writable temporary directory where you can build a cache for the next run. This is only passed to `prepareCache`. - `cachePath`: A writable temporary directory where you can build a cache for the next run. This is only passed to `prepareCache`.
- `config`: An arbitrary object passed from by the user in the [Build definition](#defining-the-build-step) in `now.json`. - `config`: An arbitrary object passed from by the user in the [Build definition](#defining-the-build-step) in `now.json`.
## Example: html-minifier ## Examples
Let's walk through what it takes to create a simple builder that takes in a HTML source file and yields a minified HTML static file as its build output. Check out our [Node.js Runtime](https://github.com/zeit/now/tree/canary/packages/now-node), [Go Runtime](https://github.com/zeit/now/tree/canary/packages/now-go), [Python Runtime](https://github.com/zeit/now/tree/canary/packages/now-python) or [Ruby Runtime](https://github.com/zeit/now/tree/canary/packages/now-ruby) for examples of how to build one.
While this is a very simple builder, the approach demonstrated here can be used to return anything: one or more static files and/or one or more lambdas.
## Setting up the module
### Defining the analyze step
The `analyze` hook is optional. Its goal is to give the developer a tool to avoid wasting time _re-computing a build_ that has already occurred.
The return value of `analyze` is a _fingerprint_: a simple string that uniquely identifies the build process.
If `analyze` is not specified, its behavior is to use as the fingerprint the combined checksums of **all the files in the same directory level as the entrypoint**. This is a default that errs on making sure that we re-execute builds when files _other than the entrypoint_ (like dependencies, manifest files, etc) have changed.
For our `html-minify` example, we know that HTML files don't have dependencies. Therefore, our analyze step can just return the `digest` of the entrypoint.
Our `index.js` file looks as follows:
```js
exports.analyze = function({ files, entrypoint }) {
return files[entrypoint].digest
}
```
This means that we will only re-minify and re-create the build output _only if the file contents (and therefore its digest) change._
### Defining the build step
Your module will need some utilities to manipulate the data structures we pass you, create new ones and alter the filesystem.
To that end, we expose our API as part of a `@now/build-utils` package. This package is always loaded on your behalf, so make sure it's only included as `peerDependencies` in your `package.json`.
Builders can include dependencies of their liking:
```js
const htmlMinifier = require('html-minifier')
exports.version = 2
exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest
exports.build = async ({ files, entrypoint, config }) => {
const stream = files[entrypoint].toStream()
const options = Object.assign({}, config || {})
const { data } = await FileBlob.fromStream({ stream })
const content = data.toString()
const minified = htmlMinifier(content, options)
const result = new FileBlob({ data: minified })
return {
output: {
[entrypoint]: result
},
watch: [],
routes: {}
}
}
```
### Defining a `prepareCache` step
If our builder had performed work that could be re-used in the next build invocation, we could define a `prepareCache` step.
In this case, there are not intermediate artifacts that we can cache, and our `analyze` step already takes care of caching the full output based on the fingerprint of the input.
## Technical Details ## Technical Details
### Execution Context ### Execution Context
A [Serverless Function](https://zeit.co/docs/v2/advanced/concepts/lambdas) is created where the builder logic is executed. The lambda is run using the Node.js 8 runtime. A brand new sandbox is created for each deployment, for security reasons. The sandbox is cleaned up between executions to ensure no lingering temporary files are shared from build to build. A [Serverless Function](https://zeit.co/docs/v2/advanced/concepts/lambdas) is created where the Runtime logic is executed. The lambda is run using the Node.js 8 runtime. A brand new sandbox is created for each deployment, for security reasons. The sandbox is cleaned up between executions to ensure no lingering temporary files are shared from build to build.
All the APIs you export ([`analyze`](#analyze), [`build`](#build) and [`prepareCache`](#preparecache)) are not guaranteed to be run in the same process, but the filesystem we expose (e.g.: `workPath` and the results of calling [`getWriteableDirectory`](#getWriteableDirectory) ) is retained. All the APIs you export ([`analyze`](#analyze), [`build`](#build) and [`prepareCache`](#preparecache)) are not guaranteed to be run in the same process, but the filesystem we expose (e.g.: `workPath` and the results of calling [`getWriteableDirectory`](#getWriteableDirectory) ) is retained.
@@ -228,15 +165,15 @@ When a new build is created, we pre-populate the `workPath` supplied to `analyze
The `analyze` step can modify that directory, and it will not be re-created when it's supplied to `build` and `prepareCache`. The `analyze` step can modify that directory, and it will not be re-created when it's supplied to `build` and `prepareCache`.
To learn how the cache key is computed and invalidated, refer to the [overview](https://zeit.co/docs/v2/advanced/builders#technical-details). To learn how the cache key is computed and invalidated, refer to the [overview](https://zeit.co/docs/v2/advanced/runtimes#technical-details).
### Accessing Environment and Secrets ### Accessing Environment and Secrets
The env and secrets specified by the user as `build.env` are passed to the builder process. This means you can access user env via `process.env` in Node.js. The env and secrets specified by the user as `build.env` are passed to the Runtime process. This means you can access user env via `process.env` in Node.js.
### Utilities as peerDependencies ### Utilities as peerDependencies
When you publish your builder to npm, make sure to not specify `@now/build-utils` (as seen below in the API definitions) as a dependency, but rather as part of `peerDependencies`. When you publish your Runtime to npm, make sure to not specify `@now/build-utils` (as seen below in the API definitions) as a dependency, but rather as part of `peerDependencies`.
## Types ## Types
@@ -358,7 +295,7 @@ This is an abstract enumeration type that is implemented by one of the following
## JavaScript API ## JavaScript API
The following is exposed by `@now/build-utils` to simplify the process of writing Builders, manipulating the file system, using the above types, etc. The following is exposed by `@now/build-utils` to simplify the process of writing Runtimes, manipulating the file system, using the above types, etc.
### `createLambda` ### `createLambda`

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,9 +1,9 @@
![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).
## Usage ## Usage

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

@@ -2,7 +2,7 @@
#### Why This Error Occurred #### Why This Error Occurred
The domain you supplied cannot be verified using either the intended set of nameservers of the given verification TXT record. The domain you supplied cannot be verified using either the intended set of nameservers or the given verification TXT record.
#### Possible Ways to Fix It #### Possible Ways to Fix It

View File

@@ -19,6 +19,8 @@ the provided `$PORT` that the builder expects the server to bind to.
For example, if you are using Gatsby, your `now-dev` script must use the `-p` For example, if you are using Gatsby, your `now-dev` script must use the `-p`
(port) option to bind to the `$PORT` specified from the builder: (port) option to bind to the `$PORT` specified from the builder:
> *In Windows environments, reference the `PORT` environment variable with `%PORT%`*
``` ```
{ {
... ...

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",
@@ -56,5 +58,8 @@
"hooks": { "hooks": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
} }
},
"resolutions": {
"signal-exit": "TooTallNate/signal-exit#update/sighub-to-sigint-on-windows"
} }
} }

View File

@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
# build fixtures for tests
yarn --cwd test/fixtures install
yarn --cwd test/fixtures run build

View File

@@ -0,0 +1,51 @@
const path = require('path');
const writeFile = require('util').promisify(require('fs').writeFile);
const REDIRECT_FILE_NAME = '__now_routes_g4t5bY.json';
exports.onPostBuild = async ({ store }) => {
const { redirects, program } = store.getState();
const routes = [{ handle: 'filesystem' }];
for (const redirect of redirects) {
const route = {
src: redirect.fromPath,
status: redirect.statusCode || (redirect.isPermanent ? 301 : 302),
headers: { Location: redirect.toPath },
};
if (redirect.force) {
routes.unshift(route);
} else {
routes.push(route);
}
}
// we implement gatsby's recommendations
// https://www.gatsbyjs.org/docs/caching/
const finalRoutes = [
{
src: '^/static/(.*)$',
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{
src: '^/.*\\.(js|css)$',
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{
src: '^/(sw\\.js|app-data\\.json|.*\\.html|page-data/.*)$',
headers: { 'cache-control': 'public,max-age=0,must-revalidate' },
continue: true,
},
...routes,
{ src: '.*', status: 404, dest: '/404.html' },
];
await writeFile(
path.join(program.directory, 'public', REDIRECT_FILE_NAME),
JSON.stringify(finalRoutes)
);
};

View File

@@ -0,0 +1 @@
// noop

View File

@@ -0,0 +1,36 @@
{
"name": "gatsby-plugin-now",
"version": "1.2.1-canary.4",
"main": "index.js",
"license": "MIT",
"homepage": "https://zeit.co/guides/deploying-gatsby-with-now",
"repository": {
"type": "git",
"url": "https://github.com/zeit/now.git",
"directory": "packages/gatsby-plugin-now"
},
"keywords": [
"gatsby",
"gatsby-plugin"
],
"scripts": {
"build": "./build.sh",
"test-integration-once": "jest --verbose"
},
"files": [
"index.js",
"gatsby-node.js"
],
"peerDependencies": {
"gatsby": ">=2.0.0"
},
"devDependencies": {
"jest": "24.9.0"
},
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
"<rootDir>/test/fixtures/"
]
}
}

View File

@@ -0,0 +1,7 @@
# gatsby-plugin-now
⚠️ The use of this plugin is deprecated. ZEIT Now supports Gatsby Redirects out-of-the-box and does not require the use a plugin.
---
This plugin generates [Now Routes](https://zeit.co/docs/v2/advanced/routes) for [redirects](https://www.gatsbyjs.org/docs/actions/#createRedirect) you configured for to your Gatsby project.

View File

@@ -0,0 +1,112 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test generated now routes 1`] = `
Array [
Object {
"continue": true,
"headers": Object {
"cache-control": "public,max-age=31536000,immutable",
},
"src": "^/static/(.*)$",
},
Object {
"continue": true,
"headers": Object {
"cache-control": "public,max-age=31536000,immutable",
},
"src": "^/.*\\\\.(js|css)$",
},
Object {
"continue": true,
"headers": Object {
"cache-control": "public,max-age=0,must-revalidate",
},
"src": "^/(sw\\\\.js|app-data\\\\.json|.*\\\\.html|page-data/.*)$",
},
Object {
"headers": Object {
"Location": "/",
},
"src": "/my-special-redirect",
"status": 302,
},
Object {
"handle": "filesystem",
},
Object {
"headers": Object {
"Location": "/page-2",
},
"src": "/page2",
"status": 301,
},
Object {
"headers": Object {
"Location": "/page-2/",
},
"src": "/page2/",
"status": 301,
},
Object {
"headers": Object {
"Location": "/",
},
"src": "/orange",
"status": 302,
},
Object {
"headers": Object {
"Location": "/",
},
"src": "/grape",
"status": 302,
},
Object {
"headers": Object {
"Location": "/page-2/",
},
"src": "/blue",
"status": 302,
},
Object {
"headers": Object {
"Location": "/page-2/",
},
"src": "/randirect",
"status": 302,
},
Object {
"headers": Object {
"Location": "/",
},
"src": "/juice",
"status": 302,
},
Object {
"headers": Object {
"Location": "/",
},
"src": "/soda",
"status": 302,
},
Object {
"headers": Object {
"Location": "/page-2/",
},
"src": "/donut",
"status": 302,
},
Object {
"headers": Object {
"Location": "/page-2/",
},
"src": "/randorect",
"status": 302,
},
Object {
"dest": "/404.html",
"src": ".*",
"status": 404,
},
]
`;

View File

@@ -0,0 +1,4 @@
public
node_modules
.cache
yarn.lock

View File

@@ -0,0 +1,3 @@
module.exports = {
plugins: [{ resolve: require.resolve('../../') }]
};

View File

@@ -0,0 +1,105 @@
'use strict';
// Implement the Gatsby API “createPages”. This is called once the
// data layer is bootstrapped to let plugins create pages from data.
exports.createPages = ({ actions }) => {
// need createRedirect action in actions collection
// to make the redirection magic happen.
// https://www.gatsbyjs.org/docs/bound-action-creators/
const { createRedirect } = actions;
// Lets set up some string consts to use thoroughout the following.
// MDN > JavaScript reference > Statements and declarations
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
// Maybe we usually redirect to page 2, with trailing slash.
const page2Path = `/page-2/`;
// But sometimes to homepage.
const homePath = `/`;
// One-off redirect, note trailing slash missing on fromPath and
// toPath here.
createRedirect({
fromPath: `/page2`,
isPermanent: true,
redirectInBrowser: true,
toPath: `/page-2`
});
// Another one-off redirect, note trailing slash on toPath here.
// This time we want trailing slash on toPath so we use
// page2Path. Need to handle trailing-slashed and non-trailing-
// slashed fromPaths separately, Gatsby serves page components
// at either version by default, but we need to explicitly redirect
// both versions independently, more on page components:
// Docs > Building with Components
// https://www.gatsbyjs.org/docs/building-with-components/
// and handling trailing slashes:
// Docs > Creating and modifying pages > Removing trailing slashes
// https://www.gatsbyjs.org/docs/creating-and-modifying-pages/#removing-trailing-slashes
createRedirect({
fromPath: `/page2/`,
isPermanent: true,
redirectInBrowser: true,
toPath: page2Path
});
// One approach to handle several redirects at once is to create an
// array of from/to path pairs.
let redirectBatch1 = [
{ f: `/orange`, t: `/` },
// We could use homePath and page2Path directly here.
{ f: `/grape`, t: homePath },
{ f: `/blue`, t: page2Path },
// or leave to empty and swap for page2Path later on.
{ f: `/randirect`, t: `` }
];
// Then we can loop through the array of object literals to create
// each redirect. A for loop would do the trick
for (var { f: f, t: t } of redirectBatch1) {
// Here we swap any empty toPath values for trusty page 2 via
// page2Path.
if (t === ``) {
t = page2Path;
}
createRedirect({
fromPath: f,
redirectInBrowser: true,
toPath: t
});
// Uncomment next line to see loop in action during build
// console.log('\nRedirecting:\n' + f + '\nTo:\n' + t + '\n');
// or check .cache/redirects.json post-compile.
}
// A more modern approach might use forEach rather than for...of
// Compare
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Loops_and_iteration#for...of_statement
// and
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
let redirectBatch2 = [
{ f: `/juice`, t: `/` },
{ f: `/soda`, t: `/` },
{ f: `/donut`, t: page2Path },
{ f: `/randorect`, t: `` }
];
redirectBatch2.forEach(({ f, t }) => {
if (t === ``) {
t = page2Path;
}
createRedirect({
fromPath: f,
redirectInBrowser: true,
toPath: t
});
// Uncomment next line to see forEach in action during build
// console.log('\nRedirecting:\n' + f + '\nTo:\n' + t + '\n');
});
createRedirect({
fromPath: '/my-special-redirect',
toPath: homePath,
force: true
});
};

View File

@@ -0,0 +1,11 @@
{
"name": "fixtures",
"dependencies": {
"gatsby": "2.14.0",
"react": "16.9.0",
"react-dom": "16.9.0"
},
"scripts": {
"build": "gatsby build"
}
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
const IndexPage = () => (
<div>
<h1>Hi people</h1>
</div>
);
export default IndexPage;

View File

@@ -0,0 +1,5 @@
test('test generated now routes', async () => {
const nowRoutes = require('./fixtures/public/__now_routes_g4t5bY.json');
expect(nowRoutes).toMatchSnapshot();
});

View File

@@ -1,10 +1,10 @@
{ {
"name": "@now/build-utils", "name": "@now/build-utils",
"version": "0.9.14-canary.2", "version": "1.0.0-canary.20",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",
"homepage": "https://zeit.co/docs/v2/deployments/builders/developer-guide", "homepage": "https://github.com/zeit/now/blob/canary/DEVELOPING_A_RUNTIME.md",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/zeit/now.git", "url": "https://github.com/zeit/now.git",
@@ -12,7 +12,8 @@
}, },
"scripts": { "scripts": {
"build": "./build.sh", "build": "./build.sh",
"test-integration-once": "jest --env node --verbose --runInBand", "test-unit": "jest --env node --verbose --runInBand test/unit.test.js",
"test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
"prepublishOnly": "./build.sh" "prepublishOnly": "./build.sh"
}, },
"devDependencies": { "devDependencies": {

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

@@ -1,5 +1,6 @@
import { PackageJson, Builder, Config } from './types';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
interface ErrorResponse { interface ErrorResponse {
code: string; code: string;
@@ -8,6 +9,7 @@ interface ErrorResponse {
interface Options { interface Options {
tag?: 'canary' | 'latest' | string; tag?: 'canary' | 'latest' | string;
functions?: BuilderFunctions;
} }
const src = 'package.json'; const src = 'package.json';
@@ -17,25 +19,31 @@ 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/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({ tag }: Options = {}): Map<string, Builder> {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return new Map<string, Builder>([ return new Map<string, Builder>([
['next', { src, use: '@now/next', config }] ['next', { src, use: `@now/next${withTag}`, config }],
]); ]);
} }
// Must be a function to ensure that the returned // Must be a function to ensure that the returned
// object won't be a reference // object won't be a reference
function getApiBuilders(): Builder[] { function getApiBuilders({ tag }: Pick<Options, 'tag'> = {}): Builder[] {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return [ return [
{ src: 'api/**/*.js', use: '@now/node', config }, { src: 'api/**/*.js', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.ts', use: '@now/node', config }, { src: 'api/**/*.ts', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.go', use: '@now/go', config }, { src: 'api/**/*.go', use: `@now/go${withTag}`, config },
{ src: 'api/**/*.py', use: '@now/python', config }, { src: 'api/**/*.py', use: `@now/python${withTag}`, config },
{ src: 'api/**/*.rb', use: '@now/ruby', config } { src: 'api/**/*.rb', use: `@now/ruby${withTag}`, config },
]; ];
} }
@@ -48,12 +56,61 @@ function hasBuildScript(pkg: PackageJson | undefined) {
return Boolean(scripts && scripts['build']); return Boolean(scripts && scripts['build']);
} }
async function detectBuilder(pkg: PackageJson): Promise<Builder> { function getApiFunctionBuilder(
for (const [dependency, builder] of getBuilders()) { file: string,
prevBuilder: Builder | undefined,
{ functions = {} }: Pick<Options, 'functions'>
) {
const key = Object.keys(functions).find(
k => file === k || minimatch(file, k)
);
const fn = key ? functions[key] : undefined;
if (!fn || (!fn.runtime && !prevBuilder)) {
return prevBuilder;
}
const src = (prevBuilder && prevBuilder.src) || file;
const use = fn.runtime || (prevBuilder && prevBuilder.use);
const config: Config = { zeroConfig: true };
if (key) {
Object.assign(config, {
functions: {
[key]: fn,
},
});
}
return use ? { use, src, config } : prevBuilder;
}
async function detectFrontBuilder(
pkg: PackageJson,
builders: Builder[],
options: Options
): Promise<Builder> {
for (const [dependency, builder] of getBuilders(options)) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies); const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
// Return the builder when a dependency matches // Return the builder when a dependency matches
if (deps[dependency]) { if (deps[dependency]) {
if (options.functions) {
Object.entries(options.functions).forEach(([key, func]) => {
// When the builder is not used yet we'll use it for the frontend
if (
builders.every(
b => !(b.config && b.config.functions && b.config.functions[key])
)
) {
if (!builder.config) builder.config = {};
if (!builder.config.functions) builder.config.functions = {};
builder.config.functions[key] = { ...func };
}
});
}
return builder; return builder;
} }
} }
@@ -63,26 +120,40 @@ async function detectBuilder(pkg: PackageJson): Promise<Builder> {
} }
// Files that match a specific pattern will get ignored // Files that match a specific pattern will get ignored
export function ignoreApiFilter(file: string) { export function getIgnoreApiFilter(optionsOrBuilders: Options | Builder[]) {
if (file.includes('/.')) { const possiblePatterns: string[] = getApiBuilders().map(b => b.src);
return false;
if (Array.isArray(optionsOrBuilders)) {
optionsOrBuilders.forEach(({ src }) => possiblePatterns.push(src));
} else if (optionsOrBuilders.functions) {
Object.keys(optionsOrBuilders.functions).forEach(p =>
possiblePatterns.push(p)
);
} }
if (file.includes('/_')) { return (file: string) => {
return false; if (!file.startsWith('api/')) {
} return false;
}
if (file.endsWith('.d.ts')) { if (file.includes('/.')) {
return false; return false;
} }
// If the file does not match any builder we also if (file.includes('/_')) {
// don't want to create a route e.g. `package.json` return false;
if (getApiBuilders().every(({ src }) => !minimatch(file, src))) { }
return false;
}
return true; if (file.endsWith('.d.ts')) {
return false;
}
if (possiblePatterns.every(p => !(file === p || minimatch(file, p)))) {
return false;
}
return true;
};
} }
// We need to sort the file paths by alphabet to make // We need to sort the file paths by alphabet to make
@@ -91,20 +162,144 @@ export function sortFiles(fileA: string, fileB: string) {
return fileA.localeCompare(fileB); return fileA.localeCompare(fileB);
} }
async function detectApiBuilders(files: string[]): Promise<Builder[]> { async function detectApiBuilders(
files: string[],
options: Options
): Promise<Builder[]> {
const builds = files const builds = files
.sort(sortFiles) .sort(sortFiles)
.filter(ignoreApiFilter) .filter(getIgnoreApiFilter(options))
.map(file => { .map(file => {
const result = getApiBuilders().find(({ src }): boolean => const apiBuilders = getApiBuilders(options);
minimatch(file, src) const apiBuilder = apiBuilders.find(b => minimatch(file, b.src));
); const fnBuilder = getApiFunctionBuilder(file, apiBuilder, options);
return fnBuilder ? { ...fnBuilder, src: file } : null;
return result ? { ...result, src: file } : null;
}); });
const finishedBuilds = builds.filter(Boolean); return builds.filter(Boolean) 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(b => b.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
const hasApiBuilders = builders.some(b => b.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 e.g. Next.js receives a `functions` property it has to make sure,
// that it can handle those files, otherwise there are unused functions.
async function checkUnusedFunctionsOnFrontendBuilder(
files: string[],
builder: Builder
): Promise<ErrorResponse | null> {
const { config: { functions = undefined } = {} } = builder;
if (!functions) return null;
if (builder.use.startsWith('@now/next')) {
const matchingFiles = files.filter(file =>
Object.keys(functions).some(key => file === key || minimatch(file, key))
);
for (const matchedFile of matchingFiles) {
if (
!matchedFile.startsWith('src/pages/') &&
!matchedFile.startsWith('pages/')
) {
return {
code: 'unused_function',
message: `The function for ${matchedFile} can't be handled by any builder`,
};
}
}
}
return null;
}
function validateFunctions(files: string[], { functions = {} }: Options) {
for (const [path, func] of Object.entries(functions)) {
if (path.length > 256) {
return {
code: 'invalid_function_glob',
message: 'Function globs must be less than 256 characters long.',
};
}
if (!func || typeof func !== 'object') {
return {
code: 'invalid_function',
message: 'Function must be an object.',
};
}
if (Object.keys(func).length === 0) {
return {
code: 'invalid_function',
message: 'Function must contain at least one property.',
};
}
if (
func.maxDuration !== undefined &&
(func.maxDuration < 1 ||
func.maxDuration > 900 ||
!Number.isInteger(func.maxDuration))
) {
return {
code: 'invalid_function_duration',
message: 'Functions must have a duration between 1 and 900.',
};
}
if (
func.memory !== undefined &&
(func.memory < 128 || func.memory > 3008 || func.memory % 64 !== 0)
) {
return {
code: 'invalid_function_memory',
message:
'Functions must have a memory value between 128 and 3008 in steps of 64.',
};
}
if (files.some(f => f === path || minimatch(f, path)) === false) {
return {
code: 'invalid_function_source',
message: `No source file matched the function for ${path}.`,
};
}
if (func.runtime !== undefined) {
const tag = `${func.runtime}`.split('@').pop();
if (!tag || !validSemver(tag)) {
return {
code: 'invalid_function_runtime',
message:
'Function runtimes must have a valid version, for example `@now/node@1.0.0`.',
};
}
}
}
return null;
} }
// When zero config is used we can call this function // When zero config is used we can call this function
@@ -112,24 +307,56 @@ async function detectApiBuilders(files: string[]): Promise<Builder[]> {
export async function detectBuilders( export async function detectBuilders(
files: string[], files: string[],
pkg?: PackageJson | undefined | null, pkg?: PackageJson | undefined | null,
options?: Options options: Options = {}
): 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[] = [];
const functionError = validateFunctions(files, options);
if (functionError) {
return {
builders: null,
errors: [functionError],
warnings,
};
}
// 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); const builders = await detectApiBuilders(files, options);
if (pkg && hasBuildScript(pkg)) { if (pkg && hasBuildScript(pkg)) {
builders.push(await detectBuilder(pkg)); const frontendBuilder = await detectFrontBuilder(pkg, builders, options);
builders.push(frontendBuilder);
const conflictError = await checkConflictingFiles(files, builders);
if (conflictError) {
warnings.push(conflictError);
}
const unusedFunctionError = await checkUnusedFunctionsOnFrontendBuilder(
files,
frontendBuilder
);
if (unusedFunctionError) {
return {
builders: null,
errors: [unusedFunctionError],
warnings,
};
}
} 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,45 +365,25 @@ 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 (
// We can't use pattern matching, since `!(api)` and `!(api)/**/*` builders.length > 0 &&
// won't give the correct results files.some(f => !f.startsWith('api/') && f !== 'package.json')
builders.push( ) {
...files // Everything besides the api directory
.filter(name => !name.startsWith('api/')) // and package.json can be served as static files
.filter(name => !(name === 'package.json')) builders.push({
.map(name => ({ use: '@now/static',
use: '@now/static', src: '!{api/**,package.json}',
src: name, config,
config
}))
);
}
}
// Change the tag for the builders
if (builders && builders.length) {
const tag = options && options.tag;
if (tag) {
builders = builders.map((originBuilder: Builder) => {
// Copy builder to make sure it is not a reference
const builder = { ...originBuilder };
// @now/static has no canary builder
if (builder.use !== '@now/static') {
builder.use = `${builder.use}@${tag}`;
}
return builder;
}); });
} }
} }
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,6 +1,6 @@
import { Route, Builder } from './types';
import { parse as parsePath } from 'path'; import { parse as parsePath } from 'path';
import { ignoreApiFilter, sortFiles } from './detect-builders'; import { Route, Builder } from './types';
import { getIgnoreApiFilter, sortFiles } from './detect-builders';
function escapeName(name: string) { function escapeName(name: string) {
const special = '[]^$.|?*+()'.split(''); const special = '[]^$.|?*+()'.split('');
@@ -60,9 +60,9 @@ function createRouteFromPath(filePath: string): Route {
const prefix = isIndex ? '\\/' : ''; const prefix = isIndex ? '\\/' : '';
const names = [ const names = [
prefix, isIndex ? prefix : `${fileName}\\/`,
prefix + escapeName(fileName), prefix + escapeName(fileName),
prefix + escapeName(fileName) + escapeName(ext) prefix + escapeName(fileName) + escapeName(ext),
].filter(Boolean); ].filter(Boolean);
// Either filename with extension, filename without extension // Either filename with extension, filename without extension
@@ -118,7 +118,7 @@ function partiallyMatches(pathA: string, pathB: string): boolean {
return false; return false;
} }
// Counts how often a path occurres when all placeholders // Counts how often a path occurs when all placeholders
// got resolved, so we can check if they have conflicts // got resolved, so we can check if they have conflicts
function pathOccurrences(filePath: string, files: string[]): string[] { function pathOccurrences(filePath: string, files: string[]): string[] {
const getAbsolutePath = (unresolvedPath: string): string => { const getAbsolutePath = (unresolvedPath: string): string => {
@@ -197,7 +197,10 @@ interface RoutesResult {
error: { [key: string]: string } | null; error: { [key: string]: string } | null;
} }
async function detectApiRoutes(files: string[]): Promise<RoutesResult> { async function detectApiRoutes(
files: string[],
builders: Builder[]
): Promise<RoutesResult> {
if (!files || files.length === 0) { if (!files || files.length === 0) {
return { defaultRoutes: null, error: null }; return { defaultRoutes: null, error: null };
} }
@@ -205,7 +208,7 @@ async function detectApiRoutes(files: string[]): Promise<RoutesResult> {
// The deepest routes need to be // The deepest routes need to be
// the first ones to get handled // the first ones to get handled
const sortedFiles = files const sortedFiles = files
.filter(ignoreApiFilter) .filter(getIgnoreApiFilter(builders))
.sort(sortFiles) .sort(sortFiles)
.sort(sortFilesBySegmentCount); .sort(sortFilesBySegmentCount);
@@ -226,10 +229,10 @@ async function detectApiRoutes(files: string[]): Promise<RoutesResult> {
error: { error: {
code: 'conflicting_path_segment', code: 'conflicting_path_segment',
message: message:
`The segment "${conflictingSegment}" occurres more than ` + `The segment "${conflictingSegment}" occurs more than ` +
`one time in your path "${file}". Please make sure that ` + `one time in your path "${file}". Please make sure that ` +
`every segment in a path is unique` `every segment in a path is unique`,
} },
}; };
} }
@@ -249,8 +252,8 @@ async function detectApiRoutes(files: string[]): Promise<RoutesResult> {
message: message:
`Two or more files have conflicting paths or names. ` + `Two or more files have conflicting paths or names. ` +
`Please make sure path segments and filenames, without their extension, are unique. ` + `Please make sure path segments and filenames, without their extension, are unique. ` +
`The path "${file}" has conflicts with ${messagePaths}` `The path "${file}" has conflicts with ${messagePaths}`,
} },
}; };
} }
@@ -261,7 +264,7 @@ async function detectApiRoutes(files: string[]): Promise<RoutesResult> {
if (defaultRoutes.length) { if (defaultRoutes.length) {
defaultRoutes.push({ defaultRoutes.push({
status: 404, status: 404,
src: '/api(\\/.*)?$' src: '/api(\\/.*)?$',
}); });
} }
@@ -282,12 +285,12 @@ export async function detectRoutes(
files: string[], files: string[],
builders: Builder[] builders: Builder[]
): Promise<RoutesResult> { ): Promise<RoutesResult> {
const routesResult = await detectApiRoutes(files); const routesResult = await detectApiRoutes(files, builders);
if (routesResult.defaultRoutes && hasPublicBuilder(builders)) { if (routesResult.defaultRoutes && hasPublicBuilder(builders)) {
routesResult.defaultRoutes.push({ routesResult.defaultRoutes.push({
src: '/(.*)', src: '/(.*)',
dest: '/public/$1' dest: '/public/$1',
}); });
} }

View File

@@ -4,11 +4,13 @@ import { File } from './types';
interface FileBlobOptions { interface FileBlobOptions {
mode?: number; mode?: number;
contentType?: string;
data: string | Buffer; data: string | Buffer;
} }
interface FromStreamOptions { interface FromStreamOptions {
mode?: number; mode?: number;
contentType?: string;
stream: NodeJS.ReadableStream; stream: NodeJS.ReadableStream;
} }
@@ -16,16 +18,22 @@ export default class FileBlob implements File {
public type: 'FileBlob'; public type: 'FileBlob';
public mode: number; public mode: number;
public data: string | Buffer; public data: string | Buffer;
public contentType: string | undefined;
constructor({ mode = 0o100644, data }: FileBlobOptions) { constructor({ mode = 0o100644, contentType, data }: FileBlobOptions) {
assert(typeof mode === 'number'); assert(typeof mode === 'number');
assert(typeof data === 'string' || Buffer.isBuffer(data)); assert(typeof data === 'string' || Buffer.isBuffer(data));
this.type = 'FileBlob'; this.type = 'FileBlob';
this.mode = mode; this.mode = mode;
this.contentType = contentType;
this.data = data; this.data = data;
} }
static async fromStream({ mode = 0o100644, stream }: FromStreamOptions) { static async fromStream({
mode = 0o100644,
contentType,
stream,
}: FromStreamOptions) {
assert(typeof mode === 'number'); assert(typeof mode === 'number');
assert(typeof stream.pipe === 'function'); // is-stream assert(typeof stream.pipe === 'function'); // is-stream
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
@@ -37,7 +45,7 @@ export default class FileBlob implements File {
}); });
const data = Buffer.concat(chunks); const data = Buffer.concat(chunks);
return new FileBlob({ mode, data }); return new FileBlob({ mode, contentType, data });
} }
toStream(): NodeJS.ReadableStream { toStream(): NodeJS.ReadableStream {

View File

@@ -9,11 +9,13 @@ const semaToPreventEMFILE = new Sema(20);
interface FileFsRefOptions { interface FileFsRefOptions {
mode?: number; mode?: number;
contentType?: string;
fsPath: string; fsPath: string;
} }
interface FromStreamOptions { interface FromStreamOptions {
mode: number; mode: number;
contentType?: string;
stream: NodeJS.ReadableStream; stream: NodeJS.ReadableStream;
fsPath: string; fsPath: string;
} }
@@ -22,17 +24,20 @@ class FileFsRef implements File {
public type: 'FileFsRef'; public type: 'FileFsRef';
public mode: number; public mode: number;
public fsPath: string; public fsPath: string;
public contentType: string | undefined;
constructor({ mode = 0o100644, fsPath }: FileFsRefOptions) { constructor({ mode = 0o100644, contentType, fsPath }: FileFsRefOptions) {
assert(typeof mode === 'number'); assert(typeof mode === 'number');
assert(typeof fsPath === 'string'); assert(typeof fsPath === 'string');
this.type = 'FileFsRef'; this.type = 'FileFsRef';
this.mode = mode; this.mode = mode;
this.contentType = contentType;
this.fsPath = fsPath; this.fsPath = fsPath;
} }
static async fromFsPath({ static async fromFsPath({
mode, mode,
contentType,
fsPath, fsPath,
}: FileFsRefOptions): Promise<FileFsRef> { }: FileFsRefOptions): Promise<FileFsRef> {
let m = mode; let m = mode;
@@ -40,11 +45,12 @@ class FileFsRef implements File {
const stat = await fs.lstat(fsPath); const stat = await fs.lstat(fsPath);
m = stat.mode; m = stat.mode;
} }
return new FileFsRef({ mode: m, fsPath }); return new FileFsRef({ mode: m, contentType, fsPath });
} }
static async fromStream({ static async fromStream({
mode = 0o100644, mode = 0o100644,
contentType,
stream, stream,
fsPath, fsPath,
}: FromStreamOptions): Promise<FileFsRef> { }: FromStreamOptions): Promise<FileFsRef> {
@@ -63,7 +69,7 @@ class FileFsRef implements File {
dest.on('error', reject); dest.on('error', reject);
}); });
return new FileFsRef({ mode, fsPath }); return new FileFsRef({ mode, contentType, fsPath });
} }
async toStreamAsync(): Promise<NodeJS.ReadableStream> { async toStreamAsync(): Promise<NodeJS.ReadableStream> {

View File

@@ -8,6 +8,7 @@ import { File } from './types';
interface FileRefOptions { interface FileRefOptions {
mode?: number; mode?: number;
digest: string; digest: string;
contentType?: string;
mutable?: boolean; mutable?: boolean;
} }
@@ -26,14 +27,21 @@ export default class FileRef implements File {
public type: 'FileRef'; public type: 'FileRef';
public mode: number; public mode: number;
public digest: string; public digest: string;
public contentType: string | undefined;
private mutable: boolean; private mutable: boolean;
constructor({ mode = 0o100644, digest, mutable = false }: FileRefOptions) { constructor({
mode = 0o100644,
digest,
contentType,
mutable = false,
}: FileRefOptions) {
assert(typeof mode === 'number'); assert(typeof mode === 'number');
assert(typeof digest === 'string'); assert(typeof digest === 'string');
this.type = 'FileRef'; this.type = 'FileRef';
this.mode = mode; this.mode = mode;
this.digest = digest; this.digest = digest;
this.contentType = contentType;
this.mutable = mutable; this.mutable = mutable;
} }

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

@@ -1,21 +1,22 @@
import assert from 'assert'; import assert from 'assert';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
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) {
@@ -54,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;
} }
@@ -128,38 +132,100 @@ async function scanParentDirs(destPath: string, readPackageJson = false) {
export async function runNpmInstall( export async function runNpmInstall(
destPath: string, destPath: string,
args: string[] = [], args: string[] = [],
spawnOpts?: SpawnOptions spawnOpts?: SpawnOptions,
meta?: Meta
) { ) {
if (meta && meta.isDev) {
debug('Skipping dependency installation because dev mode is enabled');
return;
}
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(
@@ -174,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

@@ -1,15 +1,19 @@
import FileBlob from './file-blob'; 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, getLambdaOptionsFromFunction } 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,
@@ -42,6 +50,7 @@ export {
detectBuilders, detectBuilders,
detectRoutes, detectRoutes,
debug, debug,
getLambdaOptionsFromFunction,
}; };
export * from './types'; export * from './types';

View File

@@ -1,8 +1,9 @@
import assert from 'assert'; import assert from 'assert';
import Sema from 'async-sema'; import Sema from 'async-sema';
import { ZipFile } from 'yazl'; import { ZipFile } from 'yazl';
import minimatch from 'minimatch';
import { readlink } from 'fs-extra'; import { readlink } from 'fs-extra';
import { Files } from './types'; import { Files, Config } from './types';
import FileFsRef from './file-fs-ref'; import FileFsRef from './file-fs-ref';
import { isSymbolicLink } from './fs/download'; import { isSymbolicLink } from './fs/download';
import streamToBuffer from './fs/stream-to-buffer'; import streamToBuffer from './fs/stream-to-buffer';
@@ -15,6 +16,8 @@ interface LambdaOptions {
zipBuffer: Buffer; zipBuffer: Buffer;
handler: string; handler: string;
runtime: string; runtime: string;
memory?: number;
maxDuration?: number;
environment: Environment; environment: Environment;
} }
@@ -22,21 +25,39 @@ interface CreateLambdaOptions {
files: Files; files: Files;
handler: string; handler: string;
runtime: string; runtime: string;
memory?: number;
maxDuration?: number;
environment?: Environment; environment?: Environment;
} }
interface GetLambdaOptionsFromFunctionOptions {
sourceFile: string;
config?: Config;
}
export class Lambda { export class Lambda {
public type: 'Lambda'; public type: 'Lambda';
public zipBuffer: Buffer; public zipBuffer: Buffer;
public handler: string; public handler: string;
public runtime: string; public runtime: string;
public memory?: number;
public maxDuration?: number;
public environment: Environment; public environment: Environment;
constructor({ zipBuffer, handler, runtime, environment }: LambdaOptions) { constructor({
zipBuffer,
handler,
runtime,
maxDuration,
memory,
environment,
}: LambdaOptions) {
this.type = 'Lambda'; this.type = 'Lambda';
this.zipBuffer = zipBuffer; this.zipBuffer = zipBuffer;
this.handler = handler; this.handler = handler;
this.runtime = runtime; this.runtime = runtime;
this.memory = memory;
this.maxDuration = maxDuration;
this.environment = environment; this.environment = environment;
} }
} }
@@ -48,6 +69,8 @@ export async function createLambda({
files, files,
handler, handler,
runtime, runtime,
memory,
maxDuration,
environment = {}, environment = {},
}: CreateLambdaOptions): Promise<Lambda> { }: CreateLambdaOptions): Promise<Lambda> {
assert(typeof files === 'object', '"files" must be an object'); assert(typeof files === 'object', '"files" must be an object');
@@ -55,6 +78,14 @@ export async function createLambda({
assert(typeof runtime === 'string', '"runtime" is not a string'); assert(typeof runtime === 'string', '"runtime" is not a string');
assert(typeof environment === 'object', '"environment" is not an object'); assert(typeof environment === 'object', '"environment" is not an object');
if (memory !== undefined) {
assert(typeof memory === 'number', '"memory" is not a number');
}
if (maxDuration !== undefined) {
assert(typeof maxDuration === 'number', '"maxDuration" is not a number');
}
await sema.acquire(); await sema.acquire();
try { try {
@@ -63,6 +94,8 @@ export async function createLambda({
zipBuffer, zipBuffer,
handler, handler,
runtime, runtime,
memory,
maxDuration,
environment, environment,
}); });
} finally { } finally {
@@ -105,3 +138,23 @@ export async function createZip(files: Files): Promise<Buffer> {
return zipBuffer; return zipBuffer;
} }
export async function getLambdaOptionsFromFunction({
sourceFile,
config,
}: GetLambdaOptionsFromFunctionOptions): Promise<
Pick<LambdaOptions, 'memory' | 'maxDuration'>
> {
if (config && config.functions) {
for (const [pattern, fn] of Object.entries(config.functions)) {
if (sourceFile === pattern || minimatch(sourceFile, pattern)) {
return {
memory: fn.memory,
maxDuration: fn.maxDuration,
};
}
}
}
return {};
}

View File

@@ -0,0 +1,42 @@
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 | null;
group?: number;
}
export class Prerender {
public type: 'Prerender';
public expiration: number;
public lambda: Lambda;
public fallback: FileBlob | FileFsRef | FileRef | null;
public group?: number;
constructor({ expiration, lambda, fallback, group }: PrerenderOptions) {
this.type = 'Prerender';
this.expiration = expiration;
this.lambda = lambda;
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;
if (typeof fallback === 'undefined') {
throw new Error(
'The `fallback` argument for `Prerender` needs to be a `FileBlob`, `FileFsRef`, `FileRef`, or null.'
);
}
this.fallback = fallback;
}
}

View File

@@ -1,9 +1,14 @@
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;
contentType?: string;
toStream: () => NodeJS.ReadableStream; toStream: () => NodeJS.ReadableStream;
/** /**
* The absolute path to the file in the filesystem * The absolute path to the file in the filesystem
@@ -34,6 +39,7 @@ export interface Config {
| boolean | boolean
| number | number
| { [key: string]: string } | { [key: string]: string }
| BuilderFunctions
| undefined; | undefined;
maxLambdaSize?: string; maxLambdaSize?: string;
includeFiles?: string | string[]; includeFiles?: string | string[];
@@ -44,6 +50,7 @@ export interface Config {
debug?: boolean; debug?: boolean;
zeroConfig?: boolean; zeroConfig?: boolean;
import?: { [key: string]: string }; import?: { [key: string]: string };
functions?: BuilderFunctions;
} }
export interface Meta { export interface Meta {
@@ -52,6 +59,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 {
@@ -184,23 +193,110 @@ export interface ShouldServeOptions {
config: Config; config: Config;
} }
/**
* Credit to Iain Reid, MIT license.
* Source: https://gist.github.com/iainreid820/5c1cc527fe6b5b7dba41fec7fe54bf6e
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace PackageJson {
/**
* An author or contributor
*/
export interface Author {
name: string;
email?: string;
homepage?: string;
}
/**
* A map of exposed bin commands
*/
export interface BinMap {
[commandName: string]: string;
}
/**
* A bugs link
*/
export interface Bugs {
email: string;
url: string;
}
export interface Config {
name?: string;
config?: unknown;
}
/**
* A map of dependencies
*/
export interface DependencyMap {
[dependencyName: string]: string;
}
/**
* CommonJS package structure
*/
export interface Directories {
lib?: string;
bin?: string;
man?: string;
doc?: string;
example?: string;
}
export interface Engines {
node?: string;
npm?: string;
}
export interface PublishConfig {
registry?: string;
}
/**
* A project repository
*/
export interface Repository {
type: string;
url: string;
}
export interface ScriptsMap {
[scriptName: string]: string;
}
}
export interface PackageJson { export interface PackageJson {
name: string; readonly name?: string;
version: string; readonly version?: string;
engines?: { readonly description?: string;
[key: string]: string; readonly keywords?: string[];
node: string; readonly homepage?: string;
npm: string; readonly bugs?: string | PackageJson.Bugs;
}; readonly license?: string;
scripts?: { readonly author?: string | PackageJson.Author;
[key: string]: string; readonly contributors?: string[] | PackageJson.Author[];
}; readonly files?: string[];
dependencies?: { readonly main?: string;
[key: string]: string; readonly bin?: string | PackageJson.BinMap;
}; readonly man?: string | string[];
devDependencies?: { readonly directories?: PackageJson.Directories;
[key: string]: string; readonly repository?: string | PackageJson.Repository;
}; readonly scripts?: PackageJson.ScriptsMap;
readonly config?: PackageJson.Config;
readonly dependencies?: PackageJson.DependencyMap;
readonly devDependencies?: PackageJson.DependencyMap;
readonly peerDependencies?: PackageJson.DependencyMap;
readonly optionalDependencies?: PackageJson.DependencyMap;
readonly bundledDependencies?: string[];
readonly engines?: PackageJson.Engines;
readonly os?: string[];
readonly cpu?: string[];
readonly preferGlobal?: boolean;
readonly private?: boolean;
readonly publishConfig?: PackageJson.PublishConfig;
} }
export interface NodeVersion { export interface NodeVersion {
@@ -214,3 +310,11 @@ export interface Builder {
src: string; src: string;
config?: Config; config?: Config;
} }
export interface BuilderFunctions {
[key: string]: {
memory?: number;
maxDuration?: number;
runtime?: string;
};
}

View File

@@ -0,0 +1,209 @@
const path = require('path');
const fs = require('fs-extra');
const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
const { glob, detectBuilders, detectRoutes } = require('../');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
console.log('buildUtilsUrl', buildUtilsUrl);
});
const fixturesPath = path.resolve(__dirname, 'fixtures');
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (fixture.includes('zero-config')) {
// Those have separate tests
continue; // eslint-disable-line no-continue
}
// eslint-disable-next-line no-loop-func
it(`should build ${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath, fixture)
)
).resolves.toBeDefined();
});
}
// few foreign tests
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
const fixturesPath2 = path.resolve(
__dirname,
`../../${builder}/test/fixtures`
);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath2)) {
// don't run all foreign fixtures, just some
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
// eslint-disable-next-line no-loop-func
it(`should build ${builder}/${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath2, fixture)
)
).resolves.toBeDefined();
});
}
}
}
it('Test `detectBuilders` and `detectRoutes`', async () => {
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/my-endpoint',
mustContain: 'my-endpoint',
status: 200,
},
{
path: '/api/other-endpoint',
mustContain: 'other-endpoint',
status: 200,
},
{
path: '/api/team/zeit',
mustContain: 'team/zeit',
status: 200,
},
{
path: '/api/user/myself',
mustContain: 'user/myself',
status: 200,
},
{
path: '/api/not-okay/',
status: 404,
},
{
path: '/api',
status: 404,
},
{
path: '/api/',
status: 404,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/not-okay',
status: 404,
},
{
path: '/api',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index.js',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/date.js',
mustContain: 'hello from api/date.js',
status: 200,
},
{
// Someone might expect this to be `date.js`,
// but I doubt that there is any case were both
// `date/index.js` and `date.js` exists,
// so it is not special cased
path: '/api/date',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});

View File

@@ -6,25 +6,8 @@ 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 {
packAndDeploy,
testDeployment
} = require('../../../test/lib/deployment/test-deployment');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
console.log('buildUtilsUrl', buildUtilsUrl);
});
// unit tests
it('should re-create symlinks properly', async () => { it('should re-create symlinks properly', async () => {
const files = await glob('**', path.join(__dirname, 'symlinks')); const files = await glob('**', path.join(__dirname, 'symlinks'));
@@ -38,7 +21,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 +43,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 +103,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');
@@ -142,160 +125,98 @@ it('should support require by path for legacy builders', () => {
expect(Lambda2).toBe(index.Lambda); expect(Lambda2).toBe(index.Lambda);
}); });
// own fixtures describe('Test `detectBuilders`', () => {
it('package.json + no build', async () => {
const fixturesPath = path.resolve(__dirname, 'fixtures');
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (fixture.includes('zero-config')) {
// Those have separate tests
continue; // eslint-disable-line no-continue
}
// eslint-disable-next-line no-loop-func
it(`should build ${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath, fixture)
)
).resolves.toBeDefined();
});
}
// few foreign tests
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
const fixturesPath2 = path.resolve(
__dirname,
`../../${builder}/test/fixtures`
);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath2)) {
// don't run all foreign fixtures, just some
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
// eslint-disable-next-line no-loop-func
it(`should build ${builder}/${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath2, fixture)
)
).resolves.toBeDefined();
});
}
}
}
it('Test `detectBuilders`', async () => {
{
// package.json + no build
const pkg = { dependencies: { next: '9.0.0' } }; const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'pages/index.js', 'public/index.html']; const files = ['package.json', 'pages/index.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files, pkg); const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null); expect(builders).toBe(null);
expect(errors.length).toBe(1); expect(errors.length).toBe(1);
} });
{ it('package.json + no build + next', async () => {
// 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);
expect(builders[0].use).toBe('@now/next'); expect(builders[0].use).toBe('@now/next');
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('package.json + no build + next', async () => {
// 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);
expect(builders[0].use).toBe('@now/next'); expect(builders[0].use).toBe('@now/next');
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('package.json + no build', async () => {
// package.json + no build
const pkg = {}; const pkg = {};
const files = ['package.json']; const files = ['package.json'];
const { builders, errors } = await detectBuilders(files, pkg); const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null); expect(builders).toBe(null);
expect(errors.length).toBe(1); expect(errors.length).toBe(1);
} });
{ it('static file', async () => {
// static file
const files = ['index.html']; const files = ['index.html'];
const { builders, errors } = await detectBuilders(files); const { builders, errors } = await detectBuilders(files);
expect(builders).toBe(null); expect(builders).toBe(null);
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('no package.json + public', async () => {
// no package.json + public
const files = ['api/users.js', 'public/index.html']; const files = ['api/users.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files); const { builders, errors } = await detectBuilders(files);
expect(builders[1].use).toBe('@now/static'); expect(builders[1].use).toBe('@now/static');
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('no package.json + no build + raw static + api', async () => {
// no package.json + no build + raw static + api
const files = ['api/users.js', 'index.html']; const files = ['api/users.js', 'index.html'];
const { builders, errors } = await detectBuilders(files); const { builders, errors } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node'); expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/users.js'); expect(builders[0].src).toBe('api/users.js');
expect(builders[1].use).toBe('@now/static'); expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('index.html'); expect(builders[1].src).toBe('!{api/**,package.json}');
expect(builders.length).toBe(2); expect(builders.length).toBe(2);
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('package.json + no build + root + api', async () => {
// package.json + no build + root + api
const files = ['index.html', 'api/[endpoint].js', 'static/image.png']; const files = ['index.html', 'api/[endpoint].js', 'static/image.png'];
const { builders, errors } = await detectBuilders(files); const { builders, errors } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node'); expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js'); expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders[1].use).toBe('@now/static'); expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('index.html'); expect(builders[1].src).toBe('!{api/**,package.json}');
expect(builders[2].use).toBe('@now/static'); expect(builders.length).toBe(2);
expect(builders[2].src).toBe('static/image.png');
expect(builders.length).toBe(3);
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('api + ignore files', async () => {
// api + ignore files
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);
expect(builders[0].use).toBe('@now/node'); expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint]/[id].js'); expect(builders[0].src).toBe('api/[endpoint]/[id].js');
expect(builders.length).toBe(1); expect(builders.length).toBe(1);
} });
{ it('api + next + public', async () => {
// 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'];
@@ -305,13 +226,12 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/next'); expect(builders[1].use).toBe('@now/next');
expect(builders[1].src).toBe('package.json'); expect(builders[1].src).toBe('package.json');
expect(builders.length).toBe(2); expect(builders.length).toBe(2);
} });
{ it('api + next + raw static', async () => {
// 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'];
@@ -321,29 +241,25 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/next'); expect(builders[1].use).toBe('@now/next');
expect(builders[1].src).toBe('package.json'); expect(builders[1].src).toBe('package.json');
expect(builders.length).toBe(2); expect(builders.length).toBe(2);
} });
{ it('api + raw static', async () => {
// api + raw static
const files = ['api/endpoint.js', 'index.html', 'favicon.ico']; const files = ['api/endpoint.js', 'index.html', 'favicon.ico'];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node'); expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js'); expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/static'); expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('favicon.ico'); expect(builders[1].src).toBe('!{api/**,package.json}');
expect(builders[2].use).toBe('@now/static'); expect(builders.length).toBe(2);
expect(builders[2].src).toBe('index.html'); });
expect(builders.length).toBe(3);
}
{ it('api + public', async () => {
// api + public
const files = [ const files = [
'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);
@@ -352,22 +268,20 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/static'); expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('public/**/*'); expect(builders[1].src).toBe('public/**/*');
expect(builders.length).toBe(2); expect(builders.length).toBe(2);
} });
{ it('just public', async () => {
// just public
const files = ['public/index.html', 'public/favicon.ico', 'README.md']; const files = ['public/index.html', 'public/favicon.ico', 'README.md'];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
expect(builders[0].src).toBe('public/**/*'); expect(builders[0].src).toBe('public/**/*');
expect(builders.length).toBe(1); expect(builders.length).toBe(1);
} });
{ it('next + public', async () => {
// 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'];
@@ -375,13 +289,12 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/next'); expect(builders[0].use).toBe('@now/next');
expect(builders[0].src).toBe('package.json'); expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1); expect(builders.length).toBe(1);
} });
{ it('nuxt', async () => {
// 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'];
@@ -389,10 +302,9 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/static-build'); expect(builders[0].use).toBe('@now/static-build');
expect(builders[0].src).toBe('package.json'); expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1); expect(builders.length).toBe(1);
} });
{ it('package.json with no build + api', async () => {
// package.json with no build + api
const pkg = { dependencies: { next: '9.0.0' } }; const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'api/[endpoint].js']; const files = ['package.json', 'api/[endpoint].js'];
@@ -400,45 +312,41 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/node'); expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js'); expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders.length).toBe(1); expect(builders.length).toBe(1);
} });
{ it('package.json with no build + public directory', async () => {
// package.json with no build + public directory
const pkg = { dependencies: { next: '9.0.0' } }; const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'public/index.html']; const files = ['package.json', 'public/index.html'];
const { builders, errors } = await detectBuilders(files, pkg); const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null); expect(builders).toBe(null);
expect(errors.length).toBe(1); expect(errors.length).toBe(1);
} });
{ it('no package.json + api', async () => {
// no package.json + api
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js']; const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files); const { builders } = await detectBuilders(files);
expect(builders.length).toBe(2); expect(builders.length).toBe(2);
} });
{ it('no package.json + no api', async () => {
// no package.json + no api
const files = ['index.html']; const files = ['index.html'];
const { builders, errors } = await detectBuilders(files); const { builders, errors } = await detectBuilders(files);
expect(builders).toBe(null); expect(builders).toBe(null);
expect(errors).toBe(null); expect(errors).toBe(null);
} });
{ it('package.json + api + canary', async () => {
// 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' });
@@ -446,18 +354,17 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/node@canary'); expect(builders[1].use).toBe('@now/node@canary');
expect(builders[2].use).toBe('@now/next@canary'); expect(builders[2].use).toBe('@now/next@canary');
expect(builders.length).toBe(3); expect(builders.length).toBe(3);
} });
{ it('package.json + api + latest', async () => {
// 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' });
@@ -465,18 +372,17 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/node@latest'); expect(builders[1].use).toBe('@now/node@latest');
expect(builders[2].use).toBe('@now/next@latest'); expect(builders[2].use).toBe('@now/next@latest');
expect(builders.length).toBe(3); expect(builders.length).toBe(3);
} });
{ it('package.json + api + random tag', async () => {
// 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' });
@@ -484,7 +390,238 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/node@haha'); expect(builders[1].use).toBe('@now/node@haha');
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);
} });
it('next.js pages/api + api', async () => {
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]).toBeDefined();
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('many static files + one api file', async () => {
const files = Array.from({ length: 5000 }).map((_, i) => `file${i}.html`);
files.push('api/index.ts');
const { builders } = await detectBuilders(files);
expect(builders.length).toBe(2);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/index.ts');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('!{api/**,package.json}');
});
it('functions with nextjs', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const functions = {
'pages/api/teams/**': {
memory: 128,
maxDuration: 10,
},
};
const files = [
'package.json',
'pages/index.js',
'pages/api/teams/members.ts',
];
const { builders, errors } = await detectBuilders(files, pkg, {
functions,
});
expect(errors).toBe(null);
expect(builders.length).toBe(1);
expect(builders[0]).toEqual({
src: 'package.json',
use: '@now/next',
config: {
zeroConfig: true,
functions: {
'pages/api/teams/**': {
memory: 128,
maxDuration: 10,
},
},
},
});
});
it('extend with functions', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const functions = {
'api/users/*.ts': {
runtime: 'my-custom-runtime-package@1.0.0',
},
'api/teams/members.ts': {
memory: 128,
maxDuration: 10,
},
};
const files = [
'package.json',
'pages/index.js',
'api/users/[id].ts',
'api/teams/members.ts',
];
const { builders } = await detectBuilders(files, pkg, { functions });
expect(builders.length).toBe(3);
expect(builders[0]).toEqual({
src: 'api/teams/members.ts',
use: '@now/node',
config: {
zeroConfig: true,
functions: {
'api/teams/members.ts': {
memory: 128,
maxDuration: 10,
},
},
},
});
expect(builders[1]).toEqual({
src: 'api/users/[id].ts',
use: 'my-custom-runtime-package@1.0.0',
config: {
zeroConfig: true,
functions: {
'api/users/*.ts': {
runtime: 'my-custom-runtime-package@1.0.0',
},
},
},
});
expect(builders[2]).toEqual({
src: 'package.json',
use: '@now/next',
config: {
zeroConfig: true,
},
});
});
it('invalid function key', async () => {
const functions = { ['a'.repeat(1000)]: { memory: 128 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(builders).toBe(null);
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function_glob');
});
it('invalid function maxDuration', async () => {
const functions = { 'pages/index.ts': { maxDuration: -1 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(builders).toBe(null);
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function_duration');
});
it('invalid function memory', async () => {
const functions = { 'pages/index.ts': { memory: 200 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(builders).toBe(null);
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function_memory');
});
it('missing runtime version', async () => {
const functions = { 'pages/index.ts': { runtime: 'haha' } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(builders).toBe(null);
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function_runtime');
});
it('use a custom runtime', async () => {
const functions = { 'api/user.php': { runtime: 'now-php@0.0.5' } };
const files = ['api/user.php'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(errors).toBe(null);
expect(builders.length).toBe(1);
expect(builders[0].use).toBe('now-php@0.0.5');
});
it('use a custom runtime but without a source', async () => {
const functions = { 'api/user.php': { runtime: 'now-php@0.0.5' } };
const files = ['api/team.js'];
const { errors } = await detectBuilders(files, null, {
functions,
});
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function_source');
});
it('do not allow empty functions', async () => {
const functions = { 'api/user.php': {} };
const files = ['api/user.php'];
const { errors } = await detectBuilders(files, null, {
functions,
});
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function');
});
it('do not allow null functions', async () => {
const functions = { 'api/user.php': null };
const files = ['api/user.php'];
const { errors } = await detectBuilders(files, null, {
functions,
});
expect(errors.length).toBe(1);
expect(errors[0].code).toBe('invalid_function');
});
it('Do not allow functions that are not used by @now/next', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const functions = { 'test.js': { memory: 1024 } };
const files = ['pages/index.js', 'test.js'];
const { errors } = await detectBuilders(files, pkg, { functions });
expect(errors).toBeDefined();
expect(errors[0].code).toBe('unused_function');
});
}); });
it('Test `detectRoutes`', async () => { it('Test `detectRoutes`', async () => {
@@ -545,7 +682,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 +697,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'];
@@ -591,7 +728,7 @@ it('Test `detectRoutes`', async () => {
'^/api/date(\\/|\\/index|\\/index\\.js)?$' '^/api/date(\\/|\\/index|\\/index\\.js)?$'
); );
expect(defaultRoutes[0].dest).toBe('/api/date/index.js'); expect(defaultRoutes[0].dest).toBe('/api/date/index.js');
expect(defaultRoutes[1].src).toBe('^/api/(date|date\\.js)$'); expect(defaultRoutes[1].src).toBe('^/api/(date\\/|date|date\\.js)$');
expect(defaultRoutes[1].dest).toBe('/api/date.js'); expect(defaultRoutes[1].dest).toBe('/api/date.js');
} }
@@ -606,7 +743,7 @@ it('Test `detectRoutes`', async () => {
'^/api/([^\\/]+)(\\/|\\/index|\\/index\\.js)?$' '^/api/([^\\/]+)(\\/|\\/index|\\/index\\.js)?$'
); );
expect(defaultRoutes[0].dest).toBe('/api/[date]/index.js?date=$1'); expect(defaultRoutes[0].dest).toBe('/api/[date]/index.js?date=$1');
expect(defaultRoutes[1].src).toBe('^/api/(date|date\\.js)$'); expect(defaultRoutes[1].src).toBe('^/api/(date\\/|date|date\\.js)$');
expect(defaultRoutes[1].dest).toBe('/api/date.js'); expect(defaultRoutes[1].dest).toBe('/api/date.js');
} }
@@ -617,7 +754,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);
@@ -629,147 +766,16 @@ it('Test `detectRoutes`', async () => {
expect(builders[3].use).toBe('@now/node'); expect(builders[3].use).toBe('@now/node');
expect(defaultRoutes.length).toBe(5); expect(defaultRoutes.length).toBe(5);
} }
});
{
it('Test `detectBuilders` and `detectRoutes`', async () => { // use a custom runtime
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api'); const functions = { 'api/user.php': { runtime: 'now-php@0.0.5' } };
const pkg = await fs.readJSON(path.join(fixture, 'package.json')); const files = ['api/user.php'];
const fileList = await glob('**', fixture);
const files = Object.keys(fileList); const { builders } = await detectBuilders(files, null, { functions });
const { defaultRoutes } = await detectRoutes(files, builders);
const probes = [
{ expect(defaultRoutes.length).toBe(2);
path: '/api/my-endpoint', expect(defaultRoutes[0].dest).toBe('/api/user.php');
mustContain: 'my-endpoint', }
status: 200
},
{
path: '/api/other-endpoint',
mustContain: 'other-endpoint',
status: 200
},
{
path: '/api/team/zeit',
mustContain: 'team/zeit',
status: 200
},
{
path: '/api/user/myself',
mustContain: 'user/myself',
status: 200
},
{
path: '/api/not-okay/',
status: 404
},
{
path: '/api',
status: 404
},
{
path: '/api/',
status: 404
},
{
path: '/',
mustContain: 'hello from index.txt'
}
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/not-okay',
status: 404
},
{
path: '/api',
mustContain: 'hello from api/index.js',
status: 200
},
{
path: '/api/',
mustContain: 'hello from api/index.js',
status: 200
},
{
path: '/api/index',
mustContain: 'hello from api/index.js',
status: 200
},
{
path: '/api/index.js',
mustContain: 'hello from api/index.js',
status: 200
},
{
path: '/api/date.js',
mustContain: 'hello from api/date.js',
status: 200
},
{
// Someone might expect this to be `date.js`,
// but I doubt that there is any case were both
// `date/index.js` and `date.js` exists,
// so it is not special cased
path: '/api/date',
mustContain: 'hello from api/date/index.js',
status: 200
},
{
path: '/api/date/',
mustContain: 'hello from api/date/index.js',
status: 200
},
{
path: '/api/date/index',
mustContain: 'hello from api/date/index.js',
status: 200
},
{
path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js',
status: 200
},
{
path: '/',
mustContain: 'hello from index.txt'
}
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
}); });

View File

@@ -1,15 +1,18 @@
const path = require('path'); const path = require('path');
const { mkdirp, copyFile } = require('fs-extra'); const { mkdirp, copyFile } = require('fs-extra');
const glob = require('@now/build-utils/fs/glob'); const {
const download = require('@now/build-utils/fs/download'); glob,
const { createLambda } = require('@now/build-utils/lambda'); download,
const getWritableDirectory = require('@now/build-utils/fs/get-writable-directory'); shouldServe,
const { shouldServe } = require('@now/build-utils'); createLambda,
getWritableDirectory,
getLambdaOptionsFromFunction,
} = require('@now/build-utils');
exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest; exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest;
exports.build = async ({ workPath, files, entrypoint, meta }) => { exports.build = async ({ workPath, files, entrypoint, meta, config }) => {
console.log('downloading files...'); console.log('downloading files...');
const outDir = await getWritableDirectory(); const outDir = await getWritableDirectory();
@@ -27,17 +30,23 @@ exports.build = async ({ workPath, files, entrypoint, meta }) => {
path.join(outDir, entrypoint) path.join(outDir, entrypoint)
); );
const lambdaOptions = await getLambdaOptionsFromFunction({
sourceFile: entrypoint,
config,
});
const lambda = await createLambda({ const lambda = await createLambda({
files: await glob('**', outDir), files: await glob('**', outDir),
handler: 'handler', handler: 'handler',
runtime: 'go1.x', runtime: 'go1.x',
environment: { environment: {
SCRIPT_FILENAME: entrypoint SCRIPT_FILENAME: entrypoint,
} },
...lambdaOptions,
}); });
return { return {
[entrypoint]: lambda [entrypoint]: lambda,
}; };
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "@now/cgi", "name": "@now/cgi",
"version": "0.1.5-canary.1", "version": "0.1.5-canary.2",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,3 +0,0 @@
declare module 'cache-or-tmp-directory' {
export default function (appName: string) : string | null
}

View File

@@ -1,3 +0,0 @@
declare module 'pcre-to-regexp' {
export default function (pattern: string, keys?: string[]): RegExp
}

View File

@@ -2,11 +2,11 @@
[![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)
## Usage ## Usages
To install the latest version of Now CLI, visit [zeit.co/download](https://zeit.co/download) or run this command: To install the latest version of Now CLI, visit [zeit.co/download](https://zeit.co/download) or run this command:
``` ```bash
npm i -g now npm i -g now
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "now", "name": "now",
"version": "16.1.3-canary.3", "version": "16.1.4-canary.35",
"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",
@@ -14,6 +14,7 @@
"preinstall": "node ./scripts/preinstall.js", "preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/*unit.js --serial --fail-fast --verbose", "test-unit": "nyc ava test/*unit.js --serial --fail-fast --verbose",
"test-integration": "ava test/integration.js --serial --fail-fast", "test-integration": "ava test/integration.js --serial --fail-fast",
"test-integration-v1": "ava test/integration-v1.js --serial --fail-fast",
"test-integration-now-dev": "ava test/dev/integration.js --serial --fail-fast --verbose", "test-integration-now-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"prepublishOnly": "yarn build", "prepublishOnly": "yarn build",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
@@ -58,15 +59,9 @@
] ]
}, },
"engines": { "engines": {
"node": ">= 8.11" "node": ">= 8"
}, },
"devDependencies": { "devDependencies": {
"@now/build-utils": "0.9.14-canary.2",
"@now/go": "latest",
"@now/next": "latest",
"@now/node": "latest",
"@now/routing-utils": "1.2.3-canary.1",
"@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 +93,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.1", "@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",
@@ -111,7 +106,6 @@
"async-sema": "2.1.4", "async-sema": "2.1.4",
"ava": "2.2.0", "ava": "2.2.0",
"bytes": "3.0.0", "bytes": "3.0.0",
"cache-or-tmp-directory": "1.0.0",
"chalk": "2.4.2", "chalk": "2.4.2",
"chokidar": "2.1.6", "chokidar": "2.1.6",
"clipboardy": "2.1.0", "clipboardy": "2.1.0",
@@ -132,10 +126,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",
@@ -146,12 +140,13 @@
"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",
"pcre-to-regexp": "0.0.5", "pcre-to-regexp": "1.0.0",
"pluralize": "7.0.0", "pluralize": "7.0.0",
"pre-commit": "1.2.2", "pre-commit": "1.2.2",
"printf": "0.2.5", "printf": "0.2.5",
@@ -174,6 +169,7 @@
"through2": "2.0.3", "through2": "2.0.3",
"title": "3.4.1", "title": "3.4.1",
"tmp-promise": "1.0.3", "tmp-promise": "1.0.3",
"tree-kill": "1.2.1",
"ts-node": "8.3.0", "ts-node": "8.3.0",
"typescript": "3.2.4", "typescript": "3.2.4",
"universal-analytics": "0.4.20", "universal-analytics": "0.4.20",

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

@@ -11,7 +11,10 @@ import strlen from '../../util/strlen.ts';
import wait from '../../util/output/wait'; import wait from '../../util/output/wait';
export default async function ls(ctx, opts, args, output) { export default async function ls(ctx, opts, args, output) {
const { authConfig: { token }, config } = ctx; const {
authConfig: { token },
config
} = ctx;
const { currentTeam } = config; const { currentTeam } = config;
const { apiUrl } = ctx; const { apiUrl } = ctx;
const { '--debug': debugEnabled } = opts; const { '--debug': debugEnabled } = opts;
@@ -48,15 +51,13 @@ export default async function ls(ctx, opts, args, output) {
return 1; return 1;
} }
if (!opts['--json']) { cancelWait = wait(
cancelWait = wait( args[0]
args[0] ? `Fetching alias details for "${args[0]}" under ${chalk.bold(
? `Fetching alias details for "${args[0]}" under ${chalk.bold( contextName
contextName )}`
)}` : `Fetching aliases under ${chalk.bold(contextName)}`
: `Fetching aliases under ${chalk.bold(contextName)}` );
);
}
const aliases = await getAliases(now); const aliases = await getAliases(now);
if (cancelWait) cancelWait(); if (cancelWait) cancelWait();
@@ -72,7 +73,7 @@ export default async function ls(ctx, opts, args, output) {
} }
if (opts['--json']) { if (opts['--json']) {
output.print(JSON.stringify({ rules: alias.rules }, null, 2)); console.log(JSON.stringify({ rules: alias.rules }, null, 2));
} else { } else {
const rules = alias.rules || []; const rules = alias.rules || [];
output.log( output.log(
@@ -105,11 +106,11 @@ function printAliasTable(aliases) {
a.rules && a.rules.length a.rules && a.rules.length
? chalk.cyan(`[${plural('rule', a.rules.length, true)}]`) ? chalk.cyan(`[${plural('rule', a.rules.length, true)}]`)
: // for legacy reasons, we might have situations : // for legacy reasons, we might have situations
// where the deployment was deleted and the alias // where the deployment was deleted and the alias
// not collected appropriately, and we need to handle it // not collected appropriately, and we need to handle it
a.deployment && a.deployment.url a.deployment && a.deployment.url
? a.deployment.url ? a.deployment.url
: chalk.gray(''), : chalk.gray(''),
a.alias, a.alias,
ms(Date.now() - new Date(a.created)) ms(Date.now() - new Date(a.created))
]) ])

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,12 +79,14 @@ export default async function set(
return 1; return 1;
} }
if (!isValidName(args[0])) { if (args.length >= 1 && !isValidName(args[0])) {
output.error(`The provided argument "${args[0]}" is not a valid deployment`); output.error(
`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;
} }
@@ -212,6 +214,7 @@ export default async function set(
for (const target of targets) { for (const target of targets) {
output.log(`Assigning alias ${target} to deployment ${deployment.url}`); output.log(`Assigning alias ${target} to deployment ${deployment.url}`);
const isWildcard = isWildcardAlias(target);
const record = await assignAlias( const record = await assignAlias(
output, output,
client, client,
@@ -222,13 +225,14 @@ export default async function set(
); );
const handleResult = handleSetupDomainError( const handleResult = handleSetupDomainError(
output, output,
handleCreateAliasError(output, record) handleCreateAliasError(output, record),
isWildcard
); );
if (handleResult === 1) { if (handleResult === 1) {
return 1; return 1;
} }
const prefix = isWildcardAlias(handleResult.alias) ? '' : 'https://'; const prefix = isWildcard ? '' : 'https://';
console.log( console.log(
`${chalk.cyan('> Success!')} ${chalk.bold( `${chalk.cyan('> Success!')} ${chalk.bold(
@@ -246,10 +250,15 @@ type SetupDomainError = Exclude<SetupDomainResolve, Domain>;
function handleSetupDomainError<T>( function handleSetupDomainError<T>(
output: Output, output: Output,
error: SetupDomainError | T error: SetupDomainError | T,
isWildcard: boolean = false
): T | 1 { ): T | 1 {
if (error instanceof ERRORS.DomainVerificationFailed) { if (
const { nsVerification, txtVerification, domain } = error.meta; error instanceof ERRORS.DomainVerificationFailed ||
error instanceof ERRORS.DomainNsNotVerifiedForWildcard
) {
const { nsVerification, domain } = error.meta;
output.error( output.error(
`We could not alias since the domain ${domain} could not be verified due to the following reasons:\n` `We could not alias since the domain ${domain} could not be verified due to the following reasons:\n`
); );
@@ -265,25 +274,34 @@ function handleSetupDomainError<T>(
{ extraSpace: ' ' } { extraSpace: ' ' }
)}\n\n` )}\n\n`
); );
output.print( if (error instanceof ERRORS.DomainVerificationFailed && !isWildcard) {
` ${chalk.gray( const { txtVerification } = error.meta;
'b)' output.print(
)} DNS TXT verification failed since found no matching records.` ` ${chalk.gray(
); 'b)'
output.print( )} DNS TXT verification failed since found no matching records.`
`\n${formatDnsTable( );
[['_now', 'TXT', txtVerification.verificationRecord]], output.print(
{ extraSpace: ' ' } `\n${formatDnsTable(
)}\n\n` [['_now', 'TXT', txtVerification.verificationRecord]],
); { extraSpace: ' ' }
output.print( )}\n\n`
` Once your domain uses either the nameservers or the TXT DNS record from above, run again ${cmd( );
'now domains verify <domain>' output.print(
)}.\n` ` Once your domain uses either the nameservers or the TXT DNS record from above, run again ${cmd(
); 'now domains verify <domain>'
output.print( )}.\n`
` We will also periodically run a verification check for you and you will receive an email once your domain is verified.\n` );
); output.print(
` We will also periodically run a verification check for you and you will receive an email once your domain is verified.\n`
);
} else {
output.print(
` Once your domain uses the nameservers from above, run again ${cmd(
'now domains verify <domain>'
)}.\n`
);
}
output.print(' Read more: https://err.sh/now/domain-verification\n'); output.print(' Read more: https://err.sh/now/domain-verification\n');
return 1; return 1;
} }
@@ -349,9 +367,7 @@ function handleSetupDomainError<T>(
if (error instanceof ERRORS.DomainPurchasePending) { if (error instanceof ERRORS.DomainPurchasePending) {
output.error( output.error(
`The domain ${ `The domain ${error.meta.domain} is processing and will be available once the order is completed.`
error.meta.domain
} is processing and will be available once the order is completed.`
); );
output.print( output.print(
` An email will be sent upon completion so you can alias to your new domain.\n` ` An email will be sent upon completion so you can alias to your new domain.\n`
@@ -467,9 +483,7 @@ function handleCreateAliasError<T>(
} }
if (error instanceof ERRORS.ForbiddenScaleMinInstances) { if (error instanceof ERRORS.ForbiddenScaleMinInstances) {
output.error( output.error(
`You can't scale to more than ${ `You can't scale to more than ${error.meta.max} min instances with your current plan.`
error.meta.max
} min instances with your current plan.`
); );
return 1; return 1;
} }
@@ -490,9 +504,7 @@ function handleCreateAliasError<T>(
if (error instanceof ERRORS.CertMissing) { if (error instanceof ERRORS.CertMissing) {
output.error( output.error(
`There is no certificate for the domain ${ `There is no certificate for the domain ${error.meta.domain} and it could not be created.`
error.meta.domain
} and it could not be created.`
); );
output.log( output.log(
`Please generate a new certificate manually with ${cmd( `Please generate a new certificate manually with ${cmd(

View File

@@ -1,12 +1,15 @@
import chalk from 'chalk'; import chalk from 'chalk';
// @ts-ignore
import Now from '../../util'; import Now from '../../util';
import Client from '../../util/client.ts'; import Client from '../../util/client';
import getScope from '../../util/get-scope.ts'; import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp.ts'; import stamp from '../../util/output/stamp';
import wait from '../../util/output/wait'; import wait from '../../util/output/wait';
import createCertFromFile from '../../util/certs/create-cert-from-file'; import createCertFromFile from '../../util/certs/create-cert-from-file';
import createCertForCns from '../../util/certs/create-cert-for-cns'; import createCertForCns from '../../util/certs/create-cert-for-cns';
import { NowContext } from '../../types';
import { Output } from '../../util/output';
import { import {
DomainPermissionDenied, DomainPermissionDenied,
@@ -14,7 +17,20 @@ import {
} from '../../util/errors-ts'; } from '../../util/errors-ts';
import handleCertError from '../../util/certs/handle-cert-error'; import handleCertError from '../../util/certs/handle-cert-error';
async function add(ctx, opts, args, output) { interface Options {
'--overwrite'?: boolean;
'--debug'?: boolean;
'--crt'?: string;
'--key'?: string;
'--ca'?: string;
}
async function add(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
): Promise<number> {
const { const {
authConfig: { token }, authConfig: { token },
config config
@@ -77,10 +93,12 @@ async function add(ctx, opts, args, output) {
// Create a custom certificate from the given file paths // Create a custom certificate from the given file paths
cert = await createCertFromFile(now, keyPath, crtPath, caPath, contextName); cert = await createCertFromFile(now, keyPath, crtPath, caPath, contextName);
if (cert instanceof InvalidCert) { if (cert instanceof InvalidCert) {
output.error(`The provided certificate is not valid and can't be added.`); output.error(`The provided certificate is not valid and can't be added.`);
return 1; return 1;
} }
if (cert instanceof DomainPermissionDenied) { if (cert instanceof DomainPermissionDenied) {
output.error( output.error(
`You don't have permissions over domain ${chalk.underline( `You don't have permissions over domain ${chalk.underline(
@@ -97,6 +115,7 @@ async function add(ctx, opts, args, output) {
'now certs issue <cn> <cns>' 'now certs issue <cn> <cns>'
)} instead` )} instead`
); );
if (args.length < 1) { if (args.length < 1) {
output.error( output.error(
`Invalid number of arguments to create a custom certificate entry. Usage:` `Invalid number of arguments to create a custom certificate entry. Usage:`
@@ -107,34 +126,43 @@ async function add(ctx, opts, args, output) {
} }
// Create the certificate from the given array of CNs // Create the certificate from the given array of CNs
const cns = args.reduce((res, item) => [...res, ...item.split(',')], []); const cns = args.reduce<string[]>((res, item) => res.concat(item.split(',')), []);
const cancelWait = wait( const cancelWait = wait(
`Generating a certificate for ${chalk.bold(cns.join(', '))}` `Generating a certificate for ${chalk.bold(cns.join(', '))}`
); );
cert = await createCertForCns(now, cns, contextName); cert = await createCertForCns(now, cns, contextName);
cancelWait(); cancelWait();
const result = handleCertError(output, cert);
if (result === 1) {
return result
}
if (cert instanceof DomainPermissionDenied) {
output.error(
`You don't have permissions over domain ${chalk.underline(
cert.meta.domain
)} under ${chalk.bold(cert.meta.context)}.`
);
return 1;
}
} }
// Print success message const result = handleCertError(output, cert);
output.success(
`Certificate entry for ${chalk.bold( if (result === 1) {
cert.cns.join(', ') return result;
)} created ${addStamp()}` }
);
if (cert instanceof DomainPermissionDenied) {
output.error(
`You don't have permissions over domain ${chalk.underline(
cert.meta.domain
)} under ${chalk.bold(cert.meta.context)}.`
);
return 1;
}
if (cert instanceof Error) {
// All cert errors are handled above,
// so this is only for typescript
throw cert;
} else {
// Print success message
output.success(
`Certificate entry for ${chalk.bold(
cert.cns.join(', ')
)} created ${addStamp()}`
);
}
return 0; return 0;
} }

View File

@@ -1,6 +1,6 @@
//
import chalk from 'chalk'; import chalk from 'chalk';
// @ts-ignore
import { handleError } from '../../util/error'; import { handleError } from '../../util/error';
import createOutput from '../../util/output'; import createOutput from '../../util/output';
@@ -12,6 +12,7 @@ import add from './add';
import issue from './issue'; import issue from './issue';
import ls from './ls'; import ls from './ls';
import rm from './rm'; import rm from './rm';
import { NowContext } from '../../types';
const help = () => { const help = () => {
console.log(` console.log(`
@@ -71,7 +72,7 @@ const COMMAND_CONFIG = {
rm: ['rm', 'remove'] rm: ['rm', 'remove']
}; };
export default async function main(ctx) { export default async function main(ctx: NowContext) {
let argv; let argv;
try { try {

View File

@@ -3,16 +3,29 @@ import ms from 'ms';
import plural from 'pluralize'; import plural from 'pluralize';
import psl from 'psl'; import psl from 'psl';
import table from 'text-table'; import table from 'text-table';
// @ts-ignore
import Now from '../../util'; import Now from '../../util';
import cmd from '../../util/output/cmd'; import cmd from '../../util/output/cmd';
import Client from '../../util/client.ts'; import Client from '../../util/client';
import getScope from '../../util/get-scope.ts'; import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp.ts'; import stamp from '../../util/output/stamp';
import getCerts from '../../util/certs/get-certs'; import getCerts from '../../util/certs/get-certs';
import { CertNotFound } from '../../util/errors-ts'; import { CertNotFound } from '../../util/errors-ts';
import strlen from '../../util/strlen.ts'; import strlen from '../../util/strlen';
import { Output } from '../../util/output';
import { NowContext, Cert } from '../../types';
async function ls(ctx, opts, args, output) { interface Options {
'--debug'?: boolean;
'--after'?: string;
}
async function ls(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
): Promise<number> {
const { authConfig: { token }, config } = ctx; const { authConfig: { token }, config } = ctx;
const { currentTeam } = config; const { currentTeam } = config;
const { apiUrl } = ctx; const { apiUrl } = ctx;
@@ -32,7 +45,6 @@ async function ls(ctx, opts, args, output) {
throw err; throw err;
} }
// $FlowFixMe
const now = new Now({ apiUrl, token, debug, currentTeam }); const now = new Now({ apiUrl, token, debug, currentTeam });
const lsStamp = stamp(); const lsStamp = stamp();
@@ -55,7 +67,6 @@ async function ls(ctx, opts, args, output) {
throw certificates; throw certificates;
} }
const { uid: lastCert } = certificates[certificates.length - 1];
const certs = sortByCn(certificates); const certs = sortByCn(certificates);
output.log( output.log(
@@ -65,7 +76,8 @@ async function ls(ctx, opts, args, output) {
); );
if (certs.length >= 100) { if (certs.length >= 100) {
output.note(`There may be more certificates that can be retrieved with ${cmd(`now ${process.argv.slice(2).join(' ')} --after=${lastCert}`)}.`); const { uid: lastCert } = certificates[certificates.length - 1];
output.note(`There may be more certificates that can be retrieved with ${cmd(`now ${process.argv.slice(2).join(' ')} --after=${lastCert}`)}.\n`);
} }
if (certs.length > 0) { if (certs.length > 0) {
@@ -75,7 +87,7 @@ async function ls(ctx, opts, args, output) {
return 0; return 0;
} }
function formatCertsTable(certsList) { function formatCertsTable(certsList: Cert[]) {
return `${table( return `${table(
[formatCertsTableHead(), ...formatCertsTableBody(certsList)], [formatCertsTableHead(), ...formatCertsTableBody(certsList)],
{ {
@@ -86,7 +98,7 @@ function formatCertsTable(certsList) {
).replace(/^(.*)/gm, ' $1')}\n`; ).replace(/^(.*)/gm, ' $1')}\n`;
} }
function formatCertsTableHead() { function formatCertsTableHead(): string[] {
return [ return [
chalk.dim('id'), chalk.dim('id'),
chalk.dim('cns'), chalk.dim('cns'),
@@ -96,15 +108,12 @@ function formatCertsTableHead() {
]; ];
} }
function formatCertsTableBody(certsList) { function formatCertsTableBody(certsList: Cert[]) {
const now = new Date(); const now = new Date();
return certsList.reduce( return certsList.reduce<string[][]>((result, cert) => result.concat(formatCert(now, cert)), []);
(result, cert) => [...result, ...formatCert(now, cert)],
[]
);
} }
function formatCert(time, cert) { function formatCert(time: Date, cert: Cert) {
return cert.cns.map( return cert.cns.map(
(cn, idx) => (cn, idx) =>
idx === 0 idx === 0
@@ -113,26 +122,26 @@ function formatCert(time, cert) {
); );
} }
function formatCertNonFirstCn(cn, multiple) { function formatCertNonFirstCn(cn: string, multiple: boolean): string[] {
return ['', formatCertCn(cn, multiple), '', '', '']; return ['', formatCertCn(cn, multiple), '', '', ''];
} }
function formatCertCn(cn, multiple) { function formatCertCn(cn: string, multiple: boolean) {
return multiple ? `${chalk.gray('-')} ${chalk.bold(cn)}` : chalk.bold(cn); return multiple ? `${chalk.gray('-')} ${chalk.bold(cn)}` : chalk.bold(cn);
} }
function formatCertFirstCn(time, cert, cn, multiple) { function formatCertFirstCn(time: Date, cert: Cert, cn: string, multiple: boolean): string[] {
return [ return [
cert.uid, cert.uid,
formatCertCn(cn, multiple), formatCertCn(cn, multiple),
formatExpirationDate(new Date(cert.expiration)), formatExpirationDate(new Date(cert.expiration)),
cert.autoRenew ? 'yes' : 'no', cert.autoRenew ? 'yes' : 'no',
chalk.gray(ms(time - new Date(cert.created))) chalk.gray(ms(time.getTime() - new Date(cert.created).getTime()))
]; ];
} }
function formatExpirationDate(date) { function formatExpirationDate(date: Date) {
const diff = date - Date.now(); const diff = date.getTime() - Date.now();
return diff < 0 return diff < 0
? chalk.gray(`${ms(-diff)} ago`) ? chalk.gray(`${ms(-diff)} ago`)
: chalk.gray(`in ${ms(diff)}`); : chalk.gray(`in ${ms(diff)}`);
@@ -143,8 +152,8 @@ function formatExpirationDate(date) {
* to 'wildcard' since that will allow psl get the root domain * to 'wildcard' since that will allow psl get the root domain
* properly to make the comparison. * properly to make the comparison.
*/ */
function sortByCn(certsList) { function sortByCn(certsList: Cert[]) {
return certsList.concat().sort((a, b) => { return certsList.concat().sort((a: Cert, b: Cert) => {
const domainA = psl.get(a.cns[0].replace('*', 'wildcard')); const domainA = psl.get(a.cns[0].replace('*', 'wildcard'));
const domainB = psl.get(b.cns[0].replace('*', 'wildcard')); const domainB = psl.get(b.cns[0].replace('*', 'wildcard'));
if (!domainA || !domainB) return 0; if (!domainA || !domainB) return 0;

View File

@@ -10,7 +10,9 @@ export const latestHelp = () => `
${chalk.dim('Basic')} ${chalk.dim('Basic')}
deploy [path] Performs a deployment ${chalk.bold('(default)')} deploy [path] Performs a deployment ${chalk.bold(
'(default)'
)}
dev Start a local development server dev Start a local development server
init [example] Initialize an example project init [example] Initialize an example project
ls | list [app] Lists deployments ls | list [app] Lists deployments
@@ -18,7 +20,6 @@ export const latestHelp = () => `
login [email] Logs into your account or creates a new one login [email] Logs into your account or creates a new one
logout Logs out of your account logout Logs out of your account
switch [scope] Switches between teams and your personal account switch [scope] Switches between teams and your personal account
update Updates Now CLI to the latest version
help [cmd] Displays complete help for [cmd] help [cmd] Displays complete help for [cmd]
${chalk.dim('Advanced')} ${chalk.dim('Advanced')}
@@ -29,7 +30,6 @@ export const latestHelp = () => `
certs [cmd] Manages your SSL certificates certs [cmd] Manages your SSL certificates
secrets [name] Manages your secret environment variables secrets [name] Manages your secret environment variables
logs [url] Displays the logs for a deployment logs [url] Displays the logs for a deployment
scale [args] Scales the instance count of a deployment
teams Manages your teams teams Manages your teams
whoami Shows the username of the currently logged in user whoami Shows the username of the currently logged in user
@@ -115,7 +115,7 @@ export const latestArgs = {
'-e': '--env', '-e': '--env',
'-b': '--build-env', '-b': '--build-env',
'-C': '--no-clipboard', '-C': '--no-clipboard',
'-m': '--meta' '-m': '--meta',
}; };
export const legacyArgsMri = { export const legacyArgsMri = {
@@ -126,7 +126,8 @@ export const legacyArgsMri = {
'meta', 'meta',
'session-affinity', 'session-affinity',
'regions', 'regions',
'dotenv' 'dotenv',
'target',
], ],
boolean: [ boolean: [
'help', 'help',
@@ -143,11 +144,12 @@ export const legacyArgsMri = {
'public', 'public',
'no-scale', 'no-scale',
'no-verify', 'no-verify',
'dotenv' 'dotenv',
'prod',
], ],
default: { default: {
C: false, C: false,
clipboard: true clipboard: true,
}, },
alias: { alias: {
env: 'e', env: 'e',
@@ -164,8 +166,8 @@ export const legacyArgsMri = {
'session-affinity': 'S', 'session-affinity': 'S',
name: 'n', name: 'n',
project: 'P', project: 'P',
alias: 'a' alias: 'a',
} },
}; };
// The following arg parsing is simply to make it compatible // The following arg parsing is simply to make it compatible

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, 'v10')
);
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,27 @@ 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 = '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'
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)}`
);
if (argv.prod || argv.target) {
error(
`The option ${cmd(
argv.prod ? '--prod' : '--target'
)} is not supported for Now 1.0 deployments. To manually alias a deployment, use ${cmd(
'now alias'
)} instead.`
);
await exit(1);
return 1;
}
const { const {
authConfig: { token }, authConfig: { token },
config config,
} = ctx; } = ctx;
try { try {
@@ -314,7 +326,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 +339,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 +488,7 @@ async function sync({
// XXX: legacy // XXX: legacy
deploymentType, deploymentType,
sessionAffinity sessionAffinity,
}; };
} }
@@ -486,7 +498,7 @@ async function sync({
meta, meta,
deploymentName, deploymentName,
deploymentType, deploymentType,
sessionAffinity sessionAffinity,
} = await readMeta( } = await readMeta(
paths[0], paths[0],
deploymentName, deploymentName,
@@ -499,7 +511,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 +549,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 +560,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 +575,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 +671,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 +765,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 +785,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 +801,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 +831,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 +871,10 @@ async function sync({
output, output,
token, token,
config: { config: {
currentTeam currentTeam,
}, },
firstRun: false, firstRun: false,
deploymentType deploymentType,
}); });
} }
@@ -1002,8 +919,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 +937,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 +1004,7 @@ async function readMeta(
deploymentType, deploymentType,
deploymentName: _deploymentName, deploymentName: _deploymentName,
quiet: true, quiet: true,
sessionAffinity: _sessionAffinity sessionAffinity: _sessionAffinity,
}); });
if (!deploymentType) { if (!deploymentType) {
@@ -1150,7 +1021,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 +1035,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 +1061,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 +1077,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 +1144,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 +1156,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,18 +54,12 @@ 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;
output = createOutput({ debug }); output = createOutput({ debug });
// Builders won't show debug logs by default
// the `NOW_BUILDER_DEBUG` env variable will enable them
if (debug) {
process.env.NOW_BUILDER_DEBUG = '1';
}
if ('--port' in argv) { if ('--port' in argv) {
output.warn('`--port` is deprecated, please use `--listen` instead'); output.warn('`--port` is deprecated, please use `--listen` instead');
argv['--listen'] = String(argv['--port']); argv['--listen'] = String(argv['--port']);
@@ -90,7 +84,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 +92,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

@@ -23,7 +23,7 @@ export default async function verify(
) { ) {
const { const {
authConfig: { token }, authConfig: { token },
config config,
} = ctx; } = ctx;
const { currentTeam } = config; const { currentTeam } = config;
const { apiUrl } = ctx; const { apiUrl } = ctx;
@@ -122,16 +122,11 @@ export default async function verify(
return 1; return 1;
} }
if (result.txtVerifiedAt) { if (result.nsVerifiedAt) {
console.log( console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold( `${chalk.cyan('> Success!')} Domain ${chalk.bold(
domain.name domain.name
)} was verified using DNS TXT record. ${verifyStamp()}` )} was verified using nameservers. ${verifyStamp()}`
);
output.print(
` You can verify with nameservers too. Run ${cmd(
`now domains inspect ${domain.name}`
)} to find out the intended set.\n`
); );
return 0; return 0;
} }
@@ -139,7 +134,12 @@ export default async function verify(
console.log( console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold( `${chalk.cyan('> Success!')} Domain ${chalk.bold(
domain.name domain.name
)} was verified using nameservers. ${verifyStamp()}` )} was verified using DNS TXT record. ${verifyStamp()}`
);
output.print(
` You can verify with nameservers too. Run ${cmd(
`now domains inspect ${domain.name}`
)} to find out the intended set.\n`
); );
return 0; return 0;
} }

View File

@@ -1,39 +1,38 @@
export default new Map([ export default new Map([
[ 'alias', 'alias'], ['alias', 'alias'],
[ 'aliases', 'alias'], ['aliases', 'alias'],
[ 'billing', 'billing'], ['billing', 'billing'],
[ 'cc', 'billing'], ['cc', 'billing'],
[ 'cert', 'certs'], ['cert', 'certs'],
[ 'certs', 'certs'], ['certs', 'certs'],
[ 'deploy', 'deploy'], ['deploy', 'deploy'],
[ 'deploy-v1', 'deploy'], ['deploy-v1', 'deploy'],
[ 'deploy-v2', 'deploy'], ['deploy-v2', 'deploy'],
[ 'dev', 'dev'], ['dev', 'dev'],
[ 'dns', 'dns'], ['dns', 'dns'],
[ 'domain', 'domains'], ['domain', 'domains'],
[ 'domains', 'domains'], ['domains', 'domains'],
[ 'downgrade', 'upgrade'], ['downgrade', 'upgrade'],
[ 'help', 'help'], ['help', 'help'],
[ 'init', 'init'], ['init', 'init'],
[ 'inspect', 'inspect'], ['inspect', 'inspect'],
[ 'list', 'list'], ['list', 'list'],
[ 'ln', 'alias'], ['ln', 'alias'],
[ 'log', 'logs'], ['log', 'logs'],
[ 'login', 'login'], ['login', 'login'],
[ 'logout', 'logout'], ['logout', 'logout'],
[ 'logs', 'logs'], ['logs', 'logs'],
[ 'ls', 'list'], ['ls', 'list'],
[ 'project', 'projects'], ['project', 'projects'],
[ 'projects', 'projects'], ['projects', 'projects'],
[ 'remove', 'remove'], ['remove', 'remove'],
[ 'rm', 'remove'], ['rm', 'remove'],
[ 'scale', 'scale'], ['scale', 'scale'],
[ 'secret', 'secrets'], ['secret', 'secrets'],
[ 'secrets', 'secrets'], ['secrets', 'secrets'],
[ 'switch', 'teams'], ['switch', 'teams'],
[ 'team', 'teams'], ['team', 'teams'],
[ 'teams', 'teams'], ['teams', 'teams'],
[ 'update', 'update'], ['update', 'update'],
[ 'upgrade', 'upgrade'], ['whoami', 'whoami'],
[ 'whoami', 'whoami']
]); ]);

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;
@@ -165,7 +168,7 @@ export default async function main(ctx) {
try { try {
debug('Fetching deployments'); debug('Fetching deployments');
deployments = await now.list(app, { version: 4, meta }); deployments = await now.list(app, { version: 5, meta });
} catch (err) { } catch (err) {
stopSpinner(); stopSpinner();
throw err; throw err;
@@ -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,19 +274,18 @@ 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', 'state', 'age', 'username'].map(s =>
chalk.dim(s)
),
...deployments ...deployments
.sort(sortRecent()) .sort(sortRecent())
.map(dep => [ .map(dep => [
[ [
getProjectName(dep), getProjectName(dep),
chalk.bold((includeScheme ? 'https://' : '') + dep.url), chalk.bold((includeScheme ? 'https://' : '') + dep.url),
dep.instanceCount == null || dep.type === 'LAMBDAS'
? chalk.gray('-')
: dep.instanceCount,
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))),
dep.creator.username,
], ],
...(argv['--all'] ...(argv['--all']
? dep.instances.map(i => [ ? dep.instances.map(i => [
@@ -280,9 +293,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 +306,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 +323,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);
@@ -136,14 +136,17 @@ export default async function main(ctx) {
types = argv.all ? [] : ['command', 'stdout', 'stderr', 'exit']; types = argv.all ? [] : ['command', 'stdout', 'stderr', 'exit'];
outputMode = argv.output in logPrinters ? argv.output : 'short'; outputMode = argv.output in logPrinters ? argv.output : 'short';
const { authConfig: { token }, config } = ctx; const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config; const { currentTeam } = config;
const now = new Now({ apiUrl, token, debug, currentTeam }); const now = new Now({ apiUrl, token, debug, currentTeam });
const client = new Client({ const client = new Client({
apiUrl, apiUrl,
token, token,
currentTeam, currentTeam,
debug: debugEnabled debug: debugEnabled,
}); });
let contextName = null; let contextName = null;
@@ -206,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);
@@ -216,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();
@@ -238,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,
}); });
} }
@@ -280,25 +283,53 @@ 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:') ||
line.includes('XRAY TraceId:')
) {
return;
}
if (line.includes('REPORT RequestId:')) {
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) {
console.log(`${chalk.dim(date)} ${line}`); console.log(
`${chalk.dim(date)} ${line.replace('[now-builder-debug] ', '')}`
);
} else { } else {
console.log(`${' '.repeat(date.length)} ${line}`); console.log(
`${' '.repeat(date.length)} ${line.replace(
'[now-builder-debug] ',
''
)}`
);
} }
}); });
@@ -327,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

@@ -1,323 +0,0 @@
import chalk from 'chalk';
import createOutput from '../util/output';
import cmd from '../util/output/cmd.ts';
import logo from '../util/output/logo';
import { handleError } from '../util/error';
import Client from '../util/client.ts';
import getScope from '../util/get-scope.ts';
import getArgs from '../util/get-args';
import promptBool from '../util/prompt-bool';
import Now from '../util';
import wait from '../util/output/wait';
import plans from '../util/plans';
const help = type => {
console.log(`
${chalk.bold(`${logo} now ${type}`)} [options]
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-S, --scope Set a custom scope
-y, --yes Skip the confirmation prompt
${chalk.dim('Examples:')}
${chalk.gray('')} ${type === 'upgrade'
? 'Upgrade to the Unlimited plan'
: 'Downgrade to the Free plan'}
${chalk.cyan(`$ now ${type}`)}
${type === 'upgrade'
? `
${chalk.yellow('NOTE:')} ${chalk.gray(
'Make sure you have a payment method, or add one:'
)}
${chalk.cyan(`$ now billing add`)}
`
: ''}
${chalk.gray('')} ${type === 'upgrade'
? 'Upgrade to the Unlimited plan without confirming'
: 'Downgrade to the Free plan without confirming'}
${chalk.cyan(`$ now ${type} --yes`)}
`);
};
const upgradeToUnlimited = async ({ error }, now, reactivation = false) => {
const cancelWait = wait(reactivation ? 'Re-activating' : 'Upgrading');
try {
await now.fetch(`/plan`, {
method: 'PUT',
body: {
plan: 'unlimited',
reactivation
}
});
} catch (err) {
cancelWait();
if (err.code === 'no_team_owner') {
error(
`You are not an owner of this team. Please ask an owner to upgrade your membership.`
);
return 1;
}
if (err.code === 'customer_not_found') {
error(
`No payment method available. Please add one using ${cmd(
'now billing add'
)} before upgrading.`
);
return 1;
}
error(`Not able to upgrade. Please try again later.`);
return 1;
}
cancelWait();
};
const downgradeToFree = async ({ error }, now) => {
const cancelWait = wait('Downgrading');
try {
await now.fetch(`/plan`, {
method: 'PUT',
body: {
plan: 'free'
}
});
} catch (err) {
cancelWait();
if (err.code === 'no_team_owner') {
error(
`You are not an owner of this team. Please ask an owner to upgrade your membership.`
);
return 1;
}
error(`Not able to downgrade. Please try again later.`);
return 1;
}
cancelWait();
};
export default async function main(ctx) {
let argv;
try {
argv = getArgs(ctx.argv.slice(2), {
'--yes': Boolean,
'-y': '--yes'
});
} catch (err) {
handleError(err);
return 1;
}
const type = argv._[0];
const skipConfirmation = argv['--yes'];
if (argv['--help']) {
help(type);
return 2;
}
const apiUrl = ctx.apiUrl;
const debugEnabled = argv['--debug'];
const output = createOutput({ debug: debugEnabled });
const { log, success, warn } = output;
if (type === 'upgrade') {
log(`Are you trying to upgrade Now CLI? Run ${cmd('now update')}!`);
}
warn(`${cmd(`now ${type}`)} is deprecated and will soon be removed.`);
log(`Change your plan here: ${chalk.cyan('https://zeit.co/account/plan')}\n`);
const { authConfig: { token }, config } = ctx;
const { currentTeam } = config;
const client = new Client({
apiUrl,
token,
currentTeam,
debug: debugEnabled
});
let user = null;
let team = null;
try {
({ user, team } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const prefix = currentTeam
? `Your team ${chalk.bold(team.name)} is`
: 'You are';
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam });
const billing = currentTeam ? team.billing : user.billing;
const plan = (billing && billing.plan) || 'free';
if (billing && billing.cancelation) {
const date = new Date(billing.cancelation).toLocaleString();
log(
`Your subscription is set to ${chalk.bold('downgrade')} on ${chalk.bold(
date
)}.`
);
const confirmed =
skipConfirmation ||
(await promptBool(
output,
`Would you like to prevent this from happening?`
));
if (!confirmed) {
log(`No action taken`);
return 0;
}
await upgradeToUnlimited(output, now, true);
success(`${prefix} back on the ${chalk.bold(plans[plan])} plan. Enjoy!`);
return 0;
}
if (plan === 'unlimited') {
if (type === 'upgrade') {
log(
`${prefix} already on the ${chalk.bold(
'Unlimited'
)} plan. This is the highest plan.`
);
log(
`If you want to upgrade a different scope, switch to it by using ${cmd(
'now switch'
)} first.`
);
return 0;
}
if (type === 'downgrade') {
log(`${prefix} on the ${chalk.bold('Unlimited')} plan.`);
const confirmed =
skipConfirmation ||
(await promptBool(
output,
`Would you like to downgrade to the ${chalk.bold('Free')} plan?`
));
if (!confirmed) {
log(`Aborted`);
return 0;
}
await downgradeToFree(output, now);
success(
`${prefix} now on the ${chalk.bold(
'Free'
)} plan. We are sad to see you go!`
);
}
}
if (plan === 'free' || plan === 'oss') {
if (type === 'downgrade') {
log(
`${prefix} already on the ${chalk.bold(
'Free'
)} plan. This is the lowest plan.`
);
log(
`If you want to downgrade a different scope, switch to it by using ${cmd(
'now switch'
)} first.`
);
return 0;
}
if (type === 'upgrade') {
log(`${prefix} on the ${chalk.bold('Free')} plan.`);
const confirmed =
skipConfirmation ||
(await promptBool(
output,
`Would you like to upgrade to the ${chalk.bold(
'Unlimited'
)} plan (starting at ${chalk.bold('$0.99/month')})?`
));
if (!confirmed) {
log(`Aborted`);
return 0;
}
await upgradeToUnlimited(output, now);
success(`${prefix} now on the ${chalk.bold('Unlimited')} plan. Enjoy!`);
}
}
log(`${prefix} on the old ${chalk.bold(plans[plan])} plan (Now 1.0).`);
if (type === 'upgrade') {
const confirmed =
skipConfirmation ||
(await promptBool(
output,
`Would you like to upgrade to the new ${chalk.bold(
'Unlimited'
)} plan (starting at ${chalk.bold('$0.99/month')})?`
));
if (!confirmed) {
log(`Aborted`);
return 0;
}
await upgradeToUnlimited(output, now);
success(`${prefix} now on the new ${chalk.bold('Unlimited')} plan. Enjoy!`);
return 0;
}
const confirmed =
skipConfirmation ||
(await promptBool(
output,
`Would you like to downgrade to the new ${chalk.bold('Free')} plan?`
));
if (!confirmed) {
log(`Aborted`);
return 0;
}
await downgradeToFree(output, now);
success(
`${prefix} now on the new ${chalk.bold(
'Free'
)} plan. We are sad to see you go!`
);
}

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';
@@ -36,8 +36,8 @@ import getConfig from './util/get-config';
import * as ERRORS from './util/errors-ts'; import * as ERRORS from './util/errors-ts';
import { NowError } from './util/now-error'; import { NowError } from './util/now-error';
import { SENTRY_DSN } from './util/constants.ts'; import { SENTRY_DSN } from './util/constants.ts';
import { metrics, shouldCollectMetrics } from './util/metrics.ts';
import getUpdateCommand from './util/get-update-command'; import getUpdateCommand from './util/get-update-command';
import { metrics, shouldCollectMetrics } from './util/metrics.ts';
const NOW_DIR = getNowDir(); const NOW_DIR = getNowDir();
const NOW_CONFIG_PATH = configFiles.getConfigFilePath(); const NOW_CONFIG_PATH = configFiles.getConfigFilePath();
@@ -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

@@ -195,28 +195,31 @@ export type DNSRecord = {
}; };
type SRVRecordData = { type SRVRecordData = {
name: string, name: string;
type: 'SRV', type: 'SRV';
srv: { srv: {
port: number, port: number;
priority: number, priority: number;
target: string, target: string;
weight: number, weight: number;
} };
}
type MXRecordData = {
name: string,
type: 'MX',
value: string,
mxPriority: number,
}; };
export type DNSRecordData = { type MXRecordData = {
name: string, name: string;
type: string, type: 'MX';
value: string, value: string;
} | SRVRecordData | MXRecordData; mxPriority: number;
};
export type DNSRecordData =
| {
name: string;
type: string;
value: string;
}
| SRVRecordData
| MXRecordData;
export interface Project { export interface Project {
id: string; id: string;

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

@@ -4,11 +4,11 @@ import { Deployment } from '../../types';
import { import {
DeploymentNotFound, DeploymentNotFound,
DeploymentPermissionDenied, DeploymentPermissionDenied,
InvalidDeploymentId InvalidDeploymentId,
} from '../errors-ts'; } from '../errors-ts';
import mapCertError from '../certs/map-cert-error'; import mapCertError from '../certs/map-cert-error';
type APIVersion = 'v5' | 'v9'; type APIVersion = 'v5' | 'v10';
export default async function getDeploymentByIdOrHost( export default async function getDeploymentByIdOrHost(
client: Client, client: Client,

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,246 @@
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,
apiUrl: now._apiUrl,
};
if (!legacy) {
let queuedSpinner = null;
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 (queuedSpinner === null) {
queuedSpinner = wait('Queued...');
}
}
if (
event.type === 'build-state-changed' &&
event.payload.readyState === 'BUILDING'
) {
if (queuedSpinner) {
queuedSpinner();
}
if (buildSpinner === null) {
buildSpinner = wait('Building...');
}
}
if (event.type === 'all-builds-completed') {
if (queuedSpinner) {
queuedSpinner();
}
if (buildSpinner) {
buildSpinner();
}
deploySpinner = wait('Finalizing...');
}
// Handle error events
if (event.type === 'error') {
if (queuedSpinner) {
queuedSpinner();
}
if (buildSpinner) {
buildSpinner();
}
if (deploySpinner) {
deploySpinner();
}
throw await now.handleDeploymentError(event.payload, { hashes, env });
}
// Handle ready event
if (event.type === 'alias-assigned') {
if (queuedSpinner) {
queuedSpinner();
}
if (buildSpinner) {
buildSpinner();
}
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

@@ -2,31 +2,33 @@ import chalk from 'chalk';
import execa from 'execa'; import execa from 'execa';
import semver from 'semver'; import semver from 'semver';
import pipe from 'promisepipe'; import pipe from 'promisepipe';
import retry from 'async-retry';
import npa from 'npm-package-arg'; import npa from 'npm-package-arg';
import { extract } from 'tar-fs'; import { extract } from 'tar-fs';
import { createHash } from 'crypto'; 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 cacheDirectory from 'cache-or-tmp-directory'; import { PackageJson } from '@now/build-utils';
import XDGAppPaths from 'xdg-app-paths';
import { import {
createReadStream, createReadStream,
mkdirp, mkdirp,
readFile, readFile,
readJSON, readJSON,
writeFile, writeFile,
remove remove,
} from 'fs-extra'; } from 'fs-extra';
import pkg from '../../../package.json'; import pkg from '../../../package.json';
import { NoBuilderCacheError, BuilderCacheCleanError } from '../errors-ts'; import { NoBuilderCacheError } 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 +36,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();
@@ -80,7 +78,7 @@ export async function prepareCacheDir() {
const { NOW_BUILDER_CACHE_DIR } = process.env; const { NOW_BUILDER_CACHE_DIR } = process.env;
const designated = NOW_BUILDER_CACHE_DIR const designated = NOW_BUILDER_CACHE_DIR
? resolve(NOW_BUILDER_CACHE_DIR) ? resolve(NOW_BUILDER_CACHE_DIR)
: cacheDirectory('co.zeit.now'); : XDGAppPaths('co.zeit.now').cache();
if (!designated) { if (!designated) {
throw new NoBuilderCacheError(); throw new NoBuilderCacheError();
@@ -117,7 +115,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);
@@ -140,24 +138,6 @@ export async function prepareBuilderModulePath() {
return cachedBuilderPath; return cachedBuilderPath;
} }
// Is responsible for cleaning the cache
export async function cleanCacheDir(output: Output): Promise<void> {
const cacheDir = await cacheDirPromise;
try {
output.log(chalk`{magenta Deleting} ${cacheDir}`);
await remove(cacheDir);
} catch (err) {
throw new BuilderCacheCleanError(cacheDir, err.message);
}
try {
await remove(funCacheDir);
output.log(chalk`{magenta Deleting} ${funCacheDir}`);
} catch (err) {
throw new BuilderCacheCleanError(funCacheDir, err.message);
}
}
function getNpmVersion(use = ''): string { function getNpmVersion(use = ''): string {
const parsed = npa(use); const parsed = npa(use);
if (registryTypes.has(parsed.type)) { if (registryTypes.has(parsed.type)) {
@@ -179,7 +159,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 +167,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(
@@ -251,19 +231,23 @@ export async function installBuilders(
`Installing builders: ${packagesToInstall.sort().join(', ')}` `Installing builders: ${packagesToInstall.sort().join(', ')}`
); );
try { try {
await execa( await retry(
process.execPath, () =>
[ execa(
yarnPath, process.execPath,
'add', [
'--exact', yarnPath,
'--no-lockfile', 'add',
'--non-interactive', '--exact',
...packagesToInstall '--no-lockfile',
], '--non-interactive',
{ ...packagesToInstall,
cwd: builderDir ],
} {
cwd: builderDir,
}
),
{ retries: 2 }
); );
} finally { } finally {
stopSpinner(); stopSpinner();
@@ -286,19 +270,23 @@ export async function updateBuilders(
packages.push(getBuildUtils(packages)); packages.push(getBuildUtils(packages));
await execa( await retry(
process.execPath, () =>
[ execa(
yarnPath, process.execPath,
'add', [
'--exact', yarnPath,
'--no-lockfile', 'add',
'--non-interactive', '--exact',
...packages.filter(p => p !== '@now/static') '--no-lockfile',
], '--non-interactive',
{ ...packages.filter(p => p !== '@now/static'),
cwd: builderDir ],
} {
cwd: builderDir,
}
),
{ retries: 2 }
); );
const updatedPackages: string[] = []; const updatedPackages: string[] = [];
@@ -336,7 +324,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 +345,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 +366,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

@@ -2,16 +2,17 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import ms from 'ms'; import ms from 'ms';
import bytes from 'bytes'; import bytes from 'bytes';
import { promisify } from 'util';
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';
import plural from 'pluralize'; import plural from 'pluralize';
import ora, { Ora } from 'ora';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import _treeKill from 'tree-kill';
import { Output } from '../output'; import { Output } from '../output';
import highlight from '../output/highlight'; import highlight from '../output/highlight';
@@ -23,13 +24,13 @@ 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';
import { normalizeRoutes } from '@now/routing-utils';
interface BuildMessage { interface BuildMessage {
type: string; type: string;
@@ -41,7 +42,7 @@ interface BuildMessageResult extends BuildMessage {
error?: object; error?: object;
} }
const isLogging = new WeakSet<ChildProcess>(); const treeKill = promisify(_treeKill);
let nodeBinPromise: Promise<string>; let nodeBinPromise: Promise<string>;
@@ -49,43 +50,48 @@ async function getNodeBin(): Promise<string> {
return which.sync('node', { nothrow: true }) || process.execPath; return which.sync('node', { nothrow: true }) || process.execPath;
} }
function pipeChildLogging(child: ChildProcess): void {
if (!isLogging.has(child)) {
child.stdout!.pipe(process.stdout);
child.stderr!.pipe(process.stderr);
isLogging.add(child);
}
}
async function createBuildProcess( async function createBuildProcess(
match: BuildMatch, match: BuildMatch,
buildEnv: EnvConfig, buildEnv: EnvConfig,
workPath: string, workPath: string,
output: Output, output: Output,
yarnPath?: string yarnPath?: string,
debugEnabled: boolean = false
): Promise<ChildProcess> { ): Promise<ChildProcess> {
if (!nodeBinPromise) { if (!nodeBinPromise) {
nodeBinPromise = getNodeBin(); nodeBinPromise = getNodeBin();
} }
const [execPath, modulePath] = await Promise.all([ const [execPath, modulePath] = await Promise.all([
nodeBinPromise, nodeBinPromise,
builderModulePathPromise builderModulePathPromise,
]); ]);
// Ensure that `node` is in the builder's `PATH`
let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`; let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`;
// Ensure that `yarn` is in the builder's `PATH`
if (yarnPath) { if (yarnPath) {
PATH = `${yarnPath}${delimiter}${PATH}`; PATH = `${yarnPath}${delimiter}${PATH}`;
} }
const env: EnvConfig = {
...process.env,
PATH,
...buildEnv,
NOW_REGION: 'dev1',
};
// Builders won't show debug logs by default.
// The `NOW_BUILDER_DEBUG` env variable enables them.
if (debugEnabled) {
env.NOW_BUILDER_DEBUG = '1';
}
const buildProcess = fork(modulePath, [], { const buildProcess = fork(modulePath, [], {
cwd: workPath, cwd: workPath,
env: { env,
...process.env,
PATH,
...buildEnv,
NOW_REGION: 'dev1'
},
execPath, execPath,
execArgv: [], execArgv: [],
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
}); });
match.buildProcess = buildProcess; match.buildProcess = buildProcess;
@@ -96,9 +102,6 @@ async function createBuildProcess(
match.buildProcess = undefined; match.buildProcess = undefined;
}); });
buildProcess.stdout!.setEncoding('utf8');
buildProcess.stderr!.setEncoding('utf8');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// The first message that the builder process sends is the `ready` event // The first message that the builder process sends is the `ready` event
buildProcess.once('message', ({ type }) => { buildProcess.once('message', ({ type }) => {
@@ -122,7 +125,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;
@@ -150,7 +153,8 @@ export async function executeBuild(
buildEnv, buildEnv,
workPath, workPath,
devServer.output, devServer.output,
yarnPath yarnPath,
debug
); );
} }
@@ -165,91 +169,45 @@ export async function executeBuild(
filesChanged, filesChanged,
filesRemoved, filesRemoved,
env, env,
buildEnv buildEnv,
} },
}; };
let buildResultOrOutputs: BuilderOutputs | BuildResult; let buildResultOrOutputs: BuilderOutputs | BuildResult;
if (buildProcess) { if (buildProcess) {
let spinLogger; buildProcess.send({
let spinner: Ora | undefined; type: 'build',
const fullLogs: string[] = []; builderName: pkg.name,
buildParams,
});
if (isInitialBuild && !debug && process.stdout.isTTY) { buildResultOrOutputs = await new Promise((resolve, reject) => {
const logTitle = `${chalk.bold( function onMessage({ type, result, error }: BuildMessageResult) {
`Preparing ${chalk.underline(entrypoint)} for build` cleanup();
)}:`; if (type === 'buildResult') {
spinner = ora(logTitle).start(); if (result) {
resolve(result);
spinLogger = (data: Buffer) => { } else if (error) {
const rawLog = stripAnsi(data.toString()); reject(Object.assign(new Error(), error));
fullLogs.push(rawLog);
const lines = rawLog.replace(/\s+$/, '').split('\n');
const spinText = `${logTitle} ${lines[lines.length - 1]}`;
const maxCols = process.stdout.columns || 80;
const overflow = stripAnsi(spinText).length + 2 - maxCols;
spinner!.text =
overflow > 0 ? `${spinText.slice(0, -overflow - 3)}...` : spinText;
};
buildProcess!.stdout!.on('data', spinLogger);
buildProcess!.stderr!.on('data', spinLogger);
} else {
pipeChildLogging(buildProcess!);
}
try {
buildProcess.send({
type: 'build',
builderName: pkg.name,
buildParams
});
buildResultOrOutputs = await new Promise((resolve, reject) => {
function onMessage({ type, result, error }: BuildMessageResult) {
cleanup();
if (type === 'buildResult') {
if (result) {
resolve(result);
} else if (error) {
reject(Object.assign(new Error(), error));
}
} else {
reject(new Error(`Got unexpected message type: ${type}`));
} }
} else {
reject(new Error(`Got unexpected message type: ${type}`));
} }
function onExit(code: number | null, signal: string | null) {
cleanup();
const err = new Error(
`Builder exited with ${signal || code} before sending build result`
);
reject(err);
}
function cleanup() {
buildProcess!.removeListener('exit', onExit);
buildProcess!.removeListener('message', onMessage);
}
buildProcess!.on('exit', onExit);
buildProcess!.on('message', onMessage);
});
} catch (err) {
if (spinner) {
spinner.stop();
spinner = undefined;
console.log(fullLogs.join(''));
} }
throw err; function onExit(code: number | null, signal: string | null) {
} finally { cleanup();
if (spinLogger) { const err = new Error(
buildProcess.stdout!.removeListener('data', spinLogger); `Builder exited with ${signal || code} before sending build result`
buildProcess.stderr!.removeListener('data', spinLogger); );
reject(err);
} }
if (spinner) { function cleanup() {
spinner.stop(); buildProcess!.removeListener('exit', onExit);
buildProcess!.removeListener('message', onMessage);
} }
pipeChildLogging(buildProcess!); buildProcess!.on('exit', onExit);
} buildProcess!.on('message', onMessage);
});
} else { } else {
buildResultOrOutputs = await builder.build(buildParams); buildResultOrOutputs = await builder.build(buildParams);
} }
@@ -260,15 +218,42 @@ 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;
} }
// Normalize Builder Routes
const normalized = normalizeRoutes(result.routes);
if (normalized.error) {
throw new Error(normalized.error.message);
} else {
result.routes = normalized.routes || [];
}
const { output } = result;
// Mimic fmeta-util and convert cleanUrls
if (nowConfig.cleanUrls) {
Object.entries(output)
.filter(([name, value]) => name.endsWith('.html'))
.forEach(([name, value]) => {
const cleanName = name.slice(0, -5);
delete output[name];
output[cleanName] = value;
if (value.type === 'FileBlob' || value.type === 'FileFsRef') {
value.contentType = value.contentType || 'text/html; charset=utf-8';
}
});
}
// Convert the JSON-ified output map back into their corresponding `File` // Convert the JSON-ified output map back into their corresponding `File`
// subclass type instances. // subclass type instances.
const output = result.output as BuilderOutputs;
for (const name of Object.keys(output)) { for (const name of Object.keys(output)) {
const obj = output[name] as File; const obj = output[name] as File;
let lambda: Lambda; let lambda: Lambda;
@@ -346,9 +331,9 @@ export async function executeBuild(
...nowConfig.env, ...nowConfig.env,
...asset.environment, ...asset.environment,
...env, ...env,
NOW_REGION: 'dev1' NOW_REGION: 'dev1',
} },
} },
}); });
} }
@@ -382,7 +367,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 +405,7 @@ export async function getBuildMatches(
builderWithPkg, builderWithPkg,
buildOutput: {}, buildOutput: {},
buildResults: new Map(), buildResults: new Map(),
buildTimestamp: 0 buildTimestamp: 0,
}); });
} }
} }
@@ -442,3 +427,35 @@ export async function getBuildMatches(
return matches; return matches;
} }
export async function shutdownBuilder(
match: BuildMatch,
{ debug }: Output
): Promise<void> {
const ops: Promise<void>[] = [];
if (match.buildProcess) {
const { pid } = match.buildProcess;
debug(`Killing builder sub-process with PID ${pid}`);
const killPromise = treeKill(pid)
.then(() => {
debug(`Killed builder with PID ${pid}`);
})
.catch((err: Error) => {
debug(`Failed to kill builder with PID ${pid}: ${err}`);
});
ops.push(killPromise);
delete match.buildProcess;
}
if (match.buildOutput) {
for (const asset of Object.values(match.buildOutput)) {
if (asset.type === 'Lambda' && asset.fn) {
debug(`Shutting down Lambda function`);
ops.push(asset.fn.destroy());
}
}
}
await Promise.all(ops);
}

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

@@ -4,7 +4,13 @@ import PCRE from 'pcre-to-regexp';
import isURL from './is-url'; import isURL from './is-url';
import DevServer from './server'; import DevServer from './server';
import { HttpHeadersConfig, RouteConfig, RouteResult } from './types'; import {
HttpHeadersConfig,
RouteConfig,
RouteResult,
NowConfig,
} from './types';
import { isHandler } from '@now/routing-utils';
export function resolveRouteParameters( export function resolveRouteParameters(
str: string, str: string,
@@ -40,9 +46,8 @@ export default async function(
let idx = -1; let idx = -1;
for (const routeConfig of routes) { for (const routeConfig of routes) {
idx++; idx++;
let { src, headers, methods, handle } = routeConfig; if (isHandler(routeConfig)) {
if (handle) { if (routeConfig.handle === 'filesystem' && devServer) {
if (handle === 'filesystem' && devServer) {
if (await devServer.hasFilesystem(reqPathname)) { if (await devServer.hasFilesystem(reqPathname)) {
break; break;
} }
@@ -50,18 +55,12 @@ export default async function(
continue; continue;
} }
let { src, headers, methods } = routeConfig;
if (Array.isArray(methods) && reqMethod && !methods.includes(reqMethod)) { if (Array.isArray(methods) && reqMethod && !methods.includes(reqMethod)) {
continue; continue;
} }
if (!src.startsWith('^')) {
src = `^${src}`;
}
if (!src.endsWith('$')) {
src = `${src}$`;
}
const keys: string[] = []; const keys: string[] = [];
const matcher = PCRE(`%${src}%i`, keys); const matcher = PCRE(`%${src}%i`, keys);
const match = const match =
@@ -98,7 +97,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 +113,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 +126,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

@@ -13,13 +13,15 @@ import serveHandler from 'serve-handler';
import { watch, FSWatcher } from 'chokidar'; import { watch, FSWatcher } from 'chokidar';
import { parse as parseDotenv } from 'dotenv'; import { parse as parseDotenv } from 'dotenv';
import { basename, dirname, extname, join } from 'path'; import { basename, dirname, extname, join } from 'path';
import { getTransformedRoutes } from '@now/routing-utils';
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,20 +35,29 @@ 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,
validateNowConfigCleanUrls,
validateNowConfigHeaders,
validateNowConfigRedirects,
validateNowConfigRewrites,
validateNowConfigTrailingSlash,
validateNowConfigFunctions,
} from './validate';
import isURL from './is-url'; import isURL from './is-url';
import devRouter from './router'; import devRouter from './router';
import getMimeType from './mime-type'; import getMimeType from './mime-type';
import { getYarnPath } from './yarn-installer'; import { getYarnPath } from './yarn-installer';
import { executeBuild, getBuildMatches } from './builder'; import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
import { generateErrorMessage, generateHttpStatusDescription } from './errors'; 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 +71,6 @@ import {
EnvConfig, EnvConfig,
NowConfig, NowConfig,
DevServerOptions, DevServerOptions,
BuildConfig,
BuildMatch, BuildMatch,
BuildResult, BuildResult,
BuilderInputs, BuilderInputs,
@@ -70,7 +80,7 @@ import {
InvokeResult, InvokeResult,
ListenSpec, ListenSpec,
RouteConfig, RouteConfig,
RouteResult RouteResult,
} from './types'; } from './types';
interface FSEvent { interface FSEvent {
@@ -87,7 +97,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 +192,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 +279,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}"`
); );
} }
} }
@@ -335,13 +357,18 @@ export default class DevServer {
} }
// Delete build matches that no longer exists // Delete build matches that no longer exists
const ops: Promise<void>[] = [];
for (const src of this.buildMatches.keys()) { for (const src of this.buildMatches.keys()) {
if (!sources.includes(src)) { if (!sources.includes(src)) {
this.output.debug(`Removing build match for "${src}"`); this.output.debug(`Removing build match for "${src}"`);
// TODO: shutdown lambda functions const match = this.buildMatches.get(src);
if (match) {
ops.push(shutdownBuilder(match, this.output));
}
this.buildMatches.delete(src); this.buildMatches.delete(src);
} }
} }
await Promise.all(ops);
// Add the new matches to the `buildMatches` map // Add the new matches to the `buildMatches` map
const blockingBuilds: Promise<void>[] = []; const blockingBuilds: Promise<void>[] = [];
@@ -376,7 +403,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 +440,11 @@ 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)) {
shutdownBuilder(buildMatch, this.output);
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 +469,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 +478,7 @@ export default class DevServer {
throw err; throw err;
} }
} }
return { ...base, ...env }; return {};
} }
async getNowConfig( async getNowConfig(
@@ -473,7 +501,6 @@ export default class DevServer {
isInitialLoad: boolean = false isInitialLoad: boolean = false
): Promise<NowConfig> { ): Promise<NowConfig> {
if (canUseCache && this.cachedNowConfig) { if (canUseCache && this.cachedNowConfig) {
this.output.debug('Using cached `now.json` config');
return this.cachedNowConfig; return this.cachedNowConfig;
} }
@@ -506,18 +533,30 @@ export default class DevServer {
} }
} }
const allFiles = await getAllProjectFiles(this.cwd, this.output);
const files = allFiles.filter(this.filter);
this.output.debug(
`Found ${allFiles.length} and ` +
`filtered out ${allFiles.length - files.length} files`
);
await this.validateNowConfig(config);
const { error: routeError, routes: maybeRoutes } = getTransformedRoutes({
nowConfig: config,
filePaths: files,
});
if (routeError) {
this.output.error(routeError.message);
await this.exit();
}
config.routes = maybeRoutes || [];
// no builds -> zero config // no builds -> zero config
if (!config.builds || config.builds.length === 0) { if (!config.builds || config.builds.length === 0) {
const allFiles = await getAllProjectFiles(this.cwd, this.output); const { builders, warnings, errors } = await detectBuilders(files, pkg, {
const files = allFiles.filter(this.filter); tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
this.output.debug(
`Found ${allFiles.length} and ` +
`filtered out ${allFiles.length - files.length} files`
);
const { builders, errors } = await detectBuilders(files, pkg, {
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest'
}); });
if (errors) { if (errors) {
@@ -525,6 +564,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,
@@ -583,32 +626,41 @@ export default class DevServer {
return pkg; return pkg;
} }
async tryValidateOrExit(
config: NowConfig,
validate: (c: NowConfig) => string | null
): Promise<void> {
const message = validate(config);
if (message) {
this.output.error(message);
await this.exit(1);
}
}
async validateNowConfig(config: NowConfig): Promise<void> { async validateNowConfig(config: NowConfig): Promise<void> {
if (config.version === 1) { if (config.version === 1) {
this.output.error('Only `version: 2` is supported by `now dev`'); this.output.error('Only `version: 2` is supported by `now dev`');
await this.exit(1); await this.exit(1);
} }
const buildsError = validateNowConfigBuilds(config); await this.tryValidateOrExit(config, validateNowConfigBuilds);
await this.tryValidateOrExit(config, validateNowConfigRoutes);
if (buildsError) { await this.tryValidateOrExit(config, validateNowConfigCleanUrls);
this.output.error(buildsError); await this.tryValidateOrExit(config, validateNowConfigHeaders);
await this.exit(1); await this.tryValidateOrExit(config, validateNowConfigRedirects);
} await this.tryValidateOrExit(config, validateNowConfigRewrites);
await this.tryValidateOrExit(config, validateNowConfigTrailingSlash);
const routesError = validateNowConfigRoutes(config); await this.tryValidateOrExit(config, validateNowConfigFunctions);
if (routesError) {
this.output.error(routesError);
await this.exit(1);
}
} }
validateEnvConfig( validateEnvConfig(
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 +669,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 +731,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 +749,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);
@@ -684,10 +763,12 @@ export default class DevServer {
this.yarnPath, this.yarnPath,
this.output this.output
) )
.then(updatedBuilders => .then(updatedBuilders => {
this.invalidateBuildMatches(nowConfig, updatedBuilders) this.updateBuildersPromise = null;
) this.invalidateBuildMatches(nowConfig, updatedBuilders);
})
.catch(err => { .catch(err => {
this.updateBuildersPromise = null;
this.output.error(`Failed to update builders: ${err.message}`); this.output.error(`Failed to update builders: ${err.message}`);
this.output.debug(err.stack); this.output.debug(err.stack);
}); });
@@ -716,7 +797,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);
@@ -786,22 +867,18 @@ export default class DevServer {
const ops: Promise<void>[] = []; const ops: Promise<void>[] = [];
for (const match of this.buildMatches.values()) { for (const match of this.buildMatches.values()) {
if (!match.buildOutput) continue; ops.push(shutdownBuilder(match, this.output));
for (const asset of Object.values(match.buildOutput)) {
if (asset.type === 'Lambda' && asset.fn) {
ops.push(asset.fn.destroy());
}
}
} }
ops.push(close(this.server)); ops.push(close(this.server));
if (this.watcher) { if (this.watcher) {
this.output.debug(`Closing file watcher`);
this.watcher.close(); this.watcher.close();
} }
if (this.updateBuildersPromise) { if (this.updateBuildersPromise) {
this.output.debug(`Waiting for builders update to complete`);
ops.push(this.updateBuildersPromise); ops.push(this.updateBuildersPromise);
} }
@@ -856,8 +933,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 +947,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 +955,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 +994,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 +1026,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 +1053,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,
}; };
} }
@@ -1057,7 +1134,7 @@ export default class DevServer {
} }
const method = req.method || 'GET'; const method = req.method || 'GET';
this.output.log(`${chalk.bold(method)} ${req.url}`); this.output.debug(`${chalk.bold(method)} ${req.url}`);
try { try {
const nowConfig = await this.getNowConfig(); const nowConfig = await this.getNowConfig();
@@ -1139,7 +1216,7 @@ export default class DevServer {
if (status) { if (status) {
res.statusCode = status; res.statusCode = status;
if ([301, 302, 303].includes(status)) { if (300 <= status && status <= 399) {
await this.sendRedirect( await this.sendRedirect(
req, req,
res, res,
@@ -1183,9 +1260,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,
@@ -1210,12 +1285,12 @@ export default class DevServer {
} }
} }
let foundAsset = findAsset(match, requestPath); let foundAsset = findAsset(match, requestPath, nowConfig);
if ((!foundAsset || this.shouldRebuild(req)) && callLevel === 0) { if ((!foundAsset || this.shouldRebuild(req)) && callLevel === 0) {
await this.triggerBuild(match, buildRequestPath, req); await this.triggerBuild(match, buildRequestPath, req);
// Since the `asset` was re-built, resolve it again to get the new asset // Since the `asset` was re-built, resolve it again to get the new asset
foundAsset = findAsset(match, requestPath); foundAsset = findAsset(match, requestPath, nowConfig);
} }
if (!foundAsset) { if (!foundAsset) {
@@ -1224,7 +1299,10 @@ export default class DevServer {
} }
const { asset, assetKey } = foundAsset; const { asset, assetKey } = foundAsset;
this.output.debug(`Serving asset: [${asset.type}] ${assetKey}`); this.output.debug(
`Serving asset: [${asset.type}] ${assetKey} ${(asset as any)
.contentType || ''}`
);
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
switch (asset.type) { switch (asset.type) {
@@ -1238,17 +1316,17 @@ export default class DevServer {
headers: [ headers: [
{ {
key: 'Content-Type', key: 'Content-Type',
value: getMimeType(assetKey) value: asset.contentType || 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': asset.contentType || getMimeType(assetKey),
}; };
this.setResponseHeaders(res, nowRequestId, headers); this.setResponseHeaders(res, nowRequestId, headers);
res.end(asset.data); res.end(asset.data);
@@ -1273,7 +1351,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 +1361,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 +1370,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 +1457,7 @@ export default class DevServer {
relative: href, relative: href,
ext, ext,
title: href, title: href,
base base,
}; };
}); });
@@ -1391,13 +1469,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');
@@ -1409,25 +1487,6 @@ export default class DevServer {
return true; return true;
} }
/**
* Serve project directory as a static deployment.
*/
serveProjectAsStatic = async (
req: http.IncomingMessage,
res: http.ServerResponse,
nowRequestId: string
) => {
const filePath = req.url ? req.url.replace(/^\//, '') : '';
if (filePath && typeof this.files[filePath] === 'undefined') {
await this.send404(req, res, nowRequestId);
return;
}
this.setResponseHeaders(res, nowRequestId);
return serveStaticFile(req, res, this.cwd, { cleanUrls: true });
};
async hasFilesystem(dest: string): Promise<boolean> { async hasFilesystem(dest: string): Promise<boolean> {
const requestPath = dest.replace(/^\//, ''); const requestPath = dest.replace(/^\//, '');
if ( if (
@@ -1459,7 +1518,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 +1549,7 @@ function serveStaticFile(
public: cwd, public: cwd,
cleanUrls: false, cleanUrls: false,
etag: true, etag: true,
...opts ...opts,
}); });
} }
@@ -1544,22 +1603,49 @@ async function shouldServe(
isFilesystem?: boolean isFilesystem?: boolean
): Promise<boolean> { ): Promise<boolean> {
const { const {
src: entrypoint, src,
config, config,
builderWithPkg: { builder } builderWithPkg: { builder },
} = match; } = match;
if (typeof builder.shouldServe === 'function') { const nowConfig = await devServer.getNowConfig();
const cleanSrc = src.endsWith('.html') ? src.slice(0, -5) : src;
const trimmedPath = requestPath.endsWith('/')
? requestPath.slice(0, -1)
: requestPath;
if (
nowConfig.cleanUrls &&
nowConfig.trailingSlash &&
cleanSrc === trimmedPath
) {
// Mimic fmeta-util and convert cleanUrls and trailingSlash
return true;
} else if (
nowConfig.cleanUrls &&
!nowConfig.trailingSlash &&
cleanSrc === requestPath
) {
// Mimic fmeta-util and convert cleanUrls
return true;
} else if (
!nowConfig.cleanUrls &&
nowConfig.trailingSlash &&
src === trimmedPath
) {
// Mimic fmeta-util and convert trailingSlash
return true;
} else if (typeof builder.shouldServe === 'function') {
const shouldServe = await builder.shouldServe({ const shouldServe = await builder.shouldServe({
entrypoint, entrypoint: src,
files, files,
config, config,
requestPath, requestPath,
workPath: devServer.cwd workPath: devServer.cwd,
}); });
if (shouldServe) { if (shouldServe) {
return true; return true;
} }
} else if (findAsset(match, requestPath)) { } else if (findAsset(match, requestPath, nowConfig)) {
// If there's no `shouldServe()` function, then look up if there's // If there's no `shouldServe()` function, then look up if there's
// a matching build asset on the `match` that has already been built. // a matching build asset on the `match` that has already been built.
return true; return true;
@@ -1597,7 +1683,8 @@ async function findMatchingRoute(
function findAsset( function findAsset(
match: BuildMatch, match: BuildMatch,
requestPath: string requestPath: string,
nowConfig: NowConfig
): { asset: BuilderOutput; assetKey: string } | void { ): { asset: BuilderOutput; assetKey: string } | void {
if (!match.buildOutput) { if (!match.buildOutput) {
return; return;
@@ -1605,6 +1692,10 @@ function findAsset(
let assetKey: string = requestPath.replace(/\/$/, ''); let assetKey: string = requestPath.replace(/\/$/, '');
let asset = match.buildOutput[requestPath]; let asset = match.buildOutput[requestPath];
if (nowConfig.trailingSlash && requestPath.endsWith('/')) {
asset = match.buildOutput[requestPath.slice(0, -1)];
}
// In the case of an index path, fall back to iterating over the // In the case of an index path, fall back to iterating over the
// builder outputs and doing an "is index" check until a match is found. // builder outputs and doing an "is index" check until a match is found.
if (!asset) { if (!asset) {

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,15 @@
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,
BuilderFunctions,
} from '@now/build-utils';
import { NowRedirect, NowRewrite, NowHeader, Route } from '@now/routing-utils';
import { Output } from '../output'; import { Output } from '../output';
export interface DevServerOptions { export interface DevServerOptions {
@@ -13,12 +21,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;
@@ -27,15 +29,7 @@ export interface BuildMatch extends BuildConfig {
buildProcess?: ChildProcess; buildProcess?: ChildProcess;
} }
export interface RouteConfig { export type RouteConfig = Route;
src: string;
dest: string;
methods?: string[];
headers?: HttpHeadersConfig;
status?: number;
handle?: string;
continue?: boolean;
}
export interface NowConfig { export interface NowConfig {
name?: string; name?: string;
@@ -47,6 +41,12 @@ export interface NowConfig {
builds?: BuildConfig[]; builds?: BuildConfig[];
routes?: RouteConfig[]; routes?: RouteConfig[];
files?: string[]; files?: string[];
cleanUrls?: boolean;
rewrites?: NowRewrite[];
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
functions?: BuilderFunctions;
} }
export interface HttpHandler { export interface HttpHandler {
@@ -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

@@ -1,5 +1,12 @@
import Ajv from 'ajv'; import Ajv from 'ajv';
import { schema as routesSchema } from '@now/routing-utils'; import {
routesSchema,
cleanUrlsSchema,
headersSchema,
redirectsSchema,
rewritesSchema,
trailingSlashSchema,
} from '@now/routing-utils';
import { NowConfig } from './types'; import { NowConfig } from './types';
const ajv = new Ajv(); const ajv = new Ajv();
@@ -16,52 +23,106 @@ 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 functionsSchema = {
type: 'object',
minProperties: 1,
maxProperties: 50,
additionalProperties: false,
patternProperties: {
'^.{1,256}$': {
type: 'object',
additionalProperties: false,
properties: {
runtime: {
type: 'string',
maxLength: 256,
},
memory: {
enum: Object.keys(Array.from({ length: 50 }))
.slice(2, 48)
.map(x => Number(x) * 64),
},
maxDuration: {
type: 'number',
minimum: 1,
maximum: 900,
},
},
},
},
}; };
const validateBuilds = ajv.compile(buildsSchema); const validateBuilds = ajv.compile(buildsSchema);
const validateRoutes = ajv.compile(routesSchema); const validateRoutes = ajv.compile(routesSchema);
const validateCleanUrls = ajv.compile(cleanUrlsSchema);
const validateHeaders = ajv.compile(headersSchema);
const validateRedirects = ajv.compile(redirectsSchema);
const validateRewrites = ajv.compile(rewritesSchema);
const validateTrailingSlash = ajv.compile(trailingSlashSchema);
const validateFunctions = ajv.compile(functionsSchema);
export function validateNowConfigBuilds({ builds }: NowConfig) { export function validateNowConfigBuilds(config: NowConfig) {
if (!builds) { return validateKey(config, 'builds', validateBuilds);
}
export function validateNowConfigRoutes(config: NowConfig) {
return validateKey(config, 'routes', validateRoutes);
}
export function validateNowConfigCleanUrls(config: NowConfig) {
return validateKey(config, 'cleanUrls', validateCleanUrls);
}
export function validateNowConfigHeaders(config: NowConfig) {
return validateKey(config, 'headers', validateHeaders);
}
export function validateNowConfigRedirects(config: NowConfig) {
return validateKey(config, 'redirects', validateRedirects);
}
export function validateNowConfigRewrites(config: NowConfig) {
return validateKey(config, 'rewrites', validateRewrites);
}
export function validateNowConfigTrailingSlash(config: NowConfig) {
return validateKey(config, 'trailingSlash', validateTrailingSlash);
}
export function validateNowConfigFunctions(config: NowConfig) {
return validateKey(config, 'functions', validateFunctions);
}
function validateKey(
config: NowConfig,
key: keyof NowConfig,
validate: Ajv.ValidateFunction
) {
const value = config[key];
if (!value) {
return null; return null;
} }
if (!validateBuilds(builds)) { if (!validate(value)) {
if (!validateBuilds.errors) { if (!validate.errors) {
return null; return null;
} }
const error = validateBuilds.errors[0]; const error = validate.errors[0];
return `Invalid \`builds\` property: ${error.dataPath} ${error.message}`; return `Invalid \`${key}\` property: ${error.dataPath} ${error.message}`;
}
return null;
}
export function validateNowConfigRoutes({ routes }: NowConfig) {
if (!routes) {
return null;
}
if (!validateRoutes(routes)) {
if (!validateRoutes.errors) {
return null;
}
const error = validateRoutes.errors[0];
return `Invalid \`routes\` property: ${error.dataPath} ${error.message}`;
} }
return null; return null;

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

@@ -9,6 +9,7 @@ import maybeGetDomainByName from './maybe-get-domain-by-name';
import purchaseDomainIfAvailable from './purchase-domain-if-available'; import purchaseDomainIfAvailable from './purchase-domain-if-available';
import verifyDomain from './verify-domain'; import verifyDomain from './verify-domain';
import extractDomain from '../alias/extract-domain'; import extractDomain from '../alias/extract-domain';
import isWildcardAlias from '../alias/is-wildcard-alias';
export default async function setupDomain( export default async function setupDomain(
output: Output, output: Output,
@@ -34,7 +35,7 @@ export default async function setupDomain(
if (info) { if (info) {
output.debug(`Domain ${domain} found for the given context`); output.debug(`Domain ${domain} found for the given context`);
if (!info.verified) { if (!info.verified || (!info.nsVerifiedAt && isWildcardAlias(alias))) {
output.debug( output.debug(
`Domain ${domain} is not verified, trying to perform a verification` `Domain ${domain} is not verified, trying to perform a verification`
); );
@@ -47,8 +48,17 @@ export default async function setupDomain(
output.debug(`Domain ${domain} verification failed`); output.debug(`Domain ${domain} verification failed`);
return verificationResult; return verificationResult;
} }
if (!verificationResult.nsVerifiedAt && isWildcardAlias(alias)) {
return new ERRORS.DomainNsNotVerifiedForWildcard({
domain,
nsVerification: {
intendedNameservers: verificationResult.intendedNameservers,
nameservers: verificationResult.nameservers
}
});
}
output.debug(`Domain ${domain} successfuly verified`); output.debug(`Domain ${domain} successfuly verified`);
return maybeGetDomainByName(client, contextName, domain) as Promise< return maybeGetDomainByName(client, contextName, domain) as Promise<
Domain Domain
>; >;

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.`,
}); });
} }
} }
@@ -252,6 +252,31 @@ export type TXTVerificationError = {
values: string[]; values: string[];
}; };
/**
* This error is returned when the domain is not verified by nameservers for wildcard alias.
*/
export class DomainNsNotVerifiedForWildcard extends NowError<
'DOMAIN_NS_NOT_VERIFIED_FOR_WILDCARD',
{
domain: string;
nsVerification: NSVerificationError;
}
> {
constructor({
domain,
nsVerification,
}: {
domain: string;
nsVerification: NSVerificationError;
}) {
super({
code: 'DOMAIN_NS_NOT_VERIFIED_FOR_WILDCARD',
meta: { domain, nsVerification },
message: `The domain ${domain} is not verified by nameservers for wildcard alias.`,
});
}
}
/** /**
* Used when a domain is validated because we tried to add it to an account * Used when a domain is validated because we tried to add it to an account
* via API or for any other reason. * via API or for any other reason.
@@ -264,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,
}); });
} }
} }
@@ -277,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.`,
}); });
} }
} }
@@ -294,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.`,
}); });
} }
} }
@@ -311,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.`,
}); });
} }
} }
@@ -328,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.`,
}); });
} }
} }
@@ -345,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.`,
}); });
} }
} }
@@ -361,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.`,
}); });
} }
} }
@@ -374,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.`,
}); });
} }
} }
@@ -391,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.`,
}); });
} }
} }
@@ -405,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.`,
}); });
} }
} }
@@ -415,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.`,
}); });
} }
} }
@@ -428,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}.`,
}); });
} }
} }
@@ -441,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(' ,')}`,
}); });
} }
} }
@@ -459,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.`,
}); });
} }
} }
@@ -493,7 +528,7 @@ export class CertError extends NowError<
cns, cns,
code, code,
message, message,
helpUrl helpUrl,
}: { }: {
cns: string[]; cns: string[];
code: CertErrorCode; code: CertErrorCode;
@@ -503,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,
}); });
} }
} }
@@ -522,7 +557,7 @@ export class CertConfigurationError extends NowError<
message, message,
external, external,
type, type,
helpUrl helpUrl,
}: { }: {
cns: string[]; cns: string[];
message: string; message: string;
@@ -533,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,
}); });
} }
} }
@@ -550,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}`,
}); });
} }
} }
@@ -561,13 +596,13 @@ export class DeploymentNotFound extends NowError<
*/ */
export class DeploymentNotReady extends NowError< export class DeploymentNotReady extends NowError<
'DEPLOYMENT_NOT_READY', 'DEPLOYMENT_NOT_READY',
{ url: string; } { url: string }
> { > {
constructor({ url = '' }: { url: string }) { constructor({ url = '' }: { url: string }) {
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.`,
}); });
} }
} }
@@ -580,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`,
}); });
} }
} }
@@ -597,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}.`,
}); });
} }
} }
@@ -610,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`,
}); });
} }
} }
@@ -624,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`,
}); });
} }
} }
@@ -638,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`,
}); });
} }
} }
@@ -653,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`,
}); });
} }
} }
@@ -666,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.`,
}); });
} }
} }
@@ -679,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.`,
}); });
} }
} }
@@ -692,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.`,
}); });
} }
} }
@@ -705,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.`,
}); });
} }
} }
@@ -718,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)`,
}); });
} }
} }
@@ -731,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`,
}); });
} }
} }
@@ -744,7 +779,20 @@ 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.`,
});
}
}
export class WorkingDirectoryDoesNotExist extends NowError<
'CWD_DOES_NOT_EXIST',
{}
> {
constructor() {
super({
code: 'CWD_DOES_NOT_EXIST',
meta: {},
message: 'The current working directory does not exist.',
}); });
} }
} }
@@ -754,7 +802,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}'.`,
}); });
} }
} }
@@ -767,7 +815,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`,
}); });
} }
} }
@@ -777,7 +825,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.`,
}); });
} }
} }
@@ -790,7 +838,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.`,
}); });
} }
} }
@@ -803,7 +851,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`,
}); });
} }
} }
@@ -816,7 +864,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`,
}); });
} }
} }
@@ -829,7 +877,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]`,
}); });
} }
} }
@@ -842,7 +890,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`,
}); });
} }
} }
@@ -852,7 +900,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`,
}); });
} }
} }
@@ -865,7 +913,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}.`,
}); });
} }
} }
@@ -875,7 +923,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`,
}); });
} }
} }
@@ -888,7 +936,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`,
}); });
} }
} }
@@ -901,7 +949,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}".`,
}); });
} }
} }
@@ -924,7 +972,7 @@ export class DomainRemovalConflict extends NowError<
pendingAsyncPurchase, pendingAsyncPurchase,
resolvable, resolvable,
suffix, suffix,
transferring transferring,
}: { }: {
aliases: string[]; aliases: string[];
certs: string[]; certs: string[];
@@ -942,9 +990,9 @@ export class DomainRemovalConflict extends NowError<
pendingAsyncPurchase, pendingAsyncPurchase,
suffix, suffix,
transferring, transferring,
resolvable resolvable,
}, },
message message,
}); });
} }
} }
@@ -957,7 +1005,7 @@ export class DomainMoveConflict extends NowError<
message, message,
pendingAsyncPurchase, pendingAsyncPurchase,
resolvable, resolvable,
suffix suffix,
}: { }: {
message: string; message: string;
pendingAsyncPurchase: boolean; pendingAsyncPurchase: boolean;
@@ -969,9 +1017,9 @@ export class DomainMoveConflict extends NowError<
meta: { meta: {
pendingAsyncPurchase, pendingAsyncPurchase,
resolvable, resolvable,
suffix suffix,
}, },
message message,
}); });
} }
} }
@@ -981,17 +1029,23 @@ export class InvalidEmail extends NowError<'INVALID_EMAIL', { email: string }> {
super({ super({
code: 'INVALID_EMAIL', code: 'INVALID_EMAIL',
message, message,
meta: { email } meta: { email },
}); });
} }
} }
export class AccountNotFound extends NowError<'ACCOUNT_NOT_FOUND', { email: string }> { export class AccountNotFound extends NowError<
constructor(email: string, message: string = `Please sign up: https://zeit.co/signup`) { 'ACCOUNT_NOT_FOUND',
{ email: string }
> {
constructor(
email: string,
message: string = `Please sign up: https://zeit.co/signup`
) {
super({ super({
code: 'ACCOUNT_NOT_FOUND', code: 'ACCOUNT_NOT_FOUND',
message, message,
meta: { email } meta: { email },
}); });
} }
} }
@@ -1004,7 +1058,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 },
}); });
} }
} }
@@ -1017,7 +1071,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 },
}); });
} }
} }
@@ -1027,20 +1081,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: {},
});
}
}
export class BuilderCacheCleanError extends NowError<
'BUILDER_CACHE_CLEAN_FAILED',
{ path: string }
> {
constructor(path: string, message: string) {
super({
code: 'BUILDER_CACHE_CLEAN_FAILED',
message: `Error cleaning builder cache: ${message}`,
meta: { path }
}); });
} }
} }
@@ -1057,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 },
}); });
} }
} }
@@ -1076,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');
} }
@@ -1085,17 +1126,20 @@ export class MissingDotenvVarsError extends NowError<
super({ super({
code: 'MISSING_DOTENV_VARS', code: 'MISSING_DOTENV_VARS',
message, message,
meta: { type, missing } meta: { type, missing },
}); });
} }
} }
export class DeploymentsRateLimited extends NowError<'DEPLOYMENTS_RATE_LIMITED', {}> { export class DeploymentsRateLimited extends NowError<
'DEPLOYMENTS_RATE_LIMITED',
{}
> {
constructor(message: string) { constructor(message: string) {
super({ super({
code: 'DEPLOYMENTS_RATE_LIMITED', code: 'DEPLOYMENTS_RATE_LIMITED',
meta: {}, meta: {},
message message,
}); });
} }
} }
@@ -1105,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,
}); });
} }
} }
@@ -1115,47 +1159,66 @@ 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}"`,
}); });
} }
} }
export class AliasDomainConfigured extends NowError<'DOMAIN_CONFIGURED', {}> { export class AliasDomainConfigured extends NowError<'DOMAIN_CONFIGURED', {}> {
constructor({ message }: { message: string; }) { constructor({ message }: { message: string }) {
super({ super({
code: 'DOMAIN_CONFIGURED', code: 'DOMAIN_CONFIGURED',
meta: {}, meta: {},
message message,
}); });
} }
} }
export class MissingBuildScript extends NowError<'MISSING_BUILD_SCRIPT', {}> { export class MissingBuildScript extends NowError<'MISSING_BUILD_SCRIPT', {}> {
constructor({ message }: { message: string; }) { constructor({ message }: { message: string }) {
super({ super({
code: 'MISSING_BUILD_SCRIPT', code: 'MISSING_BUILD_SCRIPT',
meta: {}, meta: {},
message message,
}); });
} }
} }
export class ConflictingFilePath extends NowError<'CONFLICTING_FILE_PATH', {}> { export class ConflictingFilePath extends NowError<'CONFLICTING_FILE_PATH', {}> {
constructor({ message }: { message: string; }) { constructor({ message }: { message: string }) {
super({ super({
code: 'CONFLICTING_FILE_PATH', code: 'CONFLICTING_FILE_PATH',
meta: {}, meta: {},
message message,
}); });
} }
} }
export class ConflictingPathSegment extends NowError<'CONFLICTING_PATH_SEGMENT', {}> { export class ConflictingPathSegment extends NowError<
constructor({ message }: { message: string; }) { 'CONFLICTING_PATH_SEGMENT',
{}
> {
constructor({ message }: { message: string }) {
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

@@ -1,5 +1,9 @@
import path from 'path'; import path from 'path';
import { CantParseJSONFile, CantFindConfig } from './errors-ts'; import {
CantParseJSONFile,
CantFindConfig,
WorkingDirectoryDoesNotExist,
} from './errors-ts';
import humanizePath from './humanize-path'; import humanizePath from './humanize-path';
import readJSONFile from './read-json-file'; import readJSONFile from './read-json-file';
import readPackage from './read-package'; import readPackage from './read-package';
@@ -8,14 +12,25 @@ import { Output } from './output';
let config: Config; let config: Config;
export default async function getConfig(output: Output, configFile?: string) { export default async function getConfig(
const localPath = process.cwd(); output: Output,
configFile?: string
): Promise<Config | Error> {
// If config was already read, just return it // If config was already read, just return it
if (config) { if (config) {
return config; return config;
} }
let localPath: string;
try {
localPath = process.cwd();
} catch (err) {
if (err.code === 'ENOENT') {
return new WorkingDirectoryDoesNotExist();
}
throw err;
}
// 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 +42,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,153 @@ 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, functions: nowConfig.functions,
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 +228,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 +246,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 +372,7 @@ export default class Now extends EventEmitter {
{ {
retries: 3, retries: 3,
minTimeout: 2500, minTimeout: 2500,
onRetry: this._onRetry onRetry: this._onRetry,
} }
); );
}; };
@@ -597,7 +380,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 +430,7 @@ export default class Now extends EventEmitter {
{ {
retries: 3, retries: 3,
minTimeout: 2500, minTimeout: 2500,
onRetry: this._onRetry onRetry: this._onRetry,
} }
); );
@@ -697,7 +480,7 @@ export default class Now extends EventEmitter {
} }
const url = `/${ const url = `/${
isBuilds ? 'v9' : 'v5' isBuilds ? 'v11' : 'v5'
}/now/deployments/${encodeURIComponent(id)}`; }/now/deployments/${encodeURIComponent(id)}`;
return this.retry( return this.retry(
@@ -727,7 +510,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 +531,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 +539,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 +583,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 +615,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 +663,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

@@ -1,5 +1,4 @@
import test from 'ava'; import test from 'ava';
import { filterPackage } from '../src/util/dev/builder-cache'; import { filterPackage } from '../src/util/dev/builder-cache';
test('[dev-builder] filter install "latest", cached canary', async t => { test('[dev-builder] filter install "latest", cached canary', async t => {

View File

@@ -1,10 +1,9 @@
import test from 'ava'; import test from 'ava';
import devRouter from '../src/util/dev/router'; import devRouter from '../src/util/dev/router';
test('[dev-router] 301 redirection', async t => { test('[dev-router] 301 redirection', async t => {
const routesConfig = [ const routesConfig = [
{ src: '/redirect', status: 301, headers: { Location: 'https://zeit.co' } } { src: '/redirect', status: 301, headers: { Location: 'https://zeit.co' } },
]; ];
const result = await devRouter('/redirect', 'GET', routesConfig); const result = await devRouter('/redirect', 'GET', routesConfig);
@@ -16,7 +15,7 @@ test('[dev-router] 301 redirection', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: false userDest: false,
}); });
}); });
@@ -32,7 +31,7 @@ test('[dev-router] captured groups', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: true userDest: true,
}); });
}); });
@@ -48,7 +47,7 @@ test('[dev-router] named groups', async t => {
uri_args: { id: '123' }, uri_args: { id: '123' },
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: true userDest: true,
}); });
}); });
@@ -56,8 +55,8 @@ test('[dev-router] optional named groups', async t => {
const routesConfig = [ const routesConfig = [
{ {
src: '/api/hello(/(?<name>[^/]+))?', src: '/api/hello(/(?<name>[^/]+))?',
dest: '/api/functions/hello/index.js?name=$name' dest: '/api/functions/hello/index.js?name=$name',
} },
]; ];
const result = await devRouter('/api/hello', 'GET', routesConfig); const result = await devRouter('/api/hello', 'GET', routesConfig);
@@ -69,7 +68,7 @@ test('[dev-router] optional named groups', async t => {
uri_args: { name: '' }, uri_args: { name: '' },
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: true userDest: true,
}); });
}); });
@@ -86,14 +85,14 @@ test('[dev-router] proxy_pass', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: false userDest: false,
}); });
}); });
test('[dev-router] methods', async t => { test('[dev-router] methods', async t => {
const routesConfig = [ const routesConfig = [
{ src: '/.*', methods: ['POST'], dest: '/post' }, { src: '/.*', methods: ['POST'], dest: '/post' },
{ src: '/.*', methods: ['GET'], dest: '/get' } { src: '/.*', methods: ['GET'], dest: '/get' },
]; ];
let result = await devRouter('/', 'GET', routesConfig); let result = await devRouter('/', 'GET', routesConfig);
@@ -105,7 +104,7 @@ test('[dev-router] methods', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[1], matched_route: routesConfig[1],
matched_route_idx: 1, matched_route_idx: 1,
userDest: true userDest: true,
}); });
result = await devRouter('/', 'POST', routesConfig); result = await devRouter('/', 'POST', routesConfig);
@@ -117,7 +116,7 @@ test('[dev-router] methods', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: true userDest: true,
}); });
}); });
@@ -133,7 +132,7 @@ test('[dev-router] match without prefix slash', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[0], matched_route: routesConfig[0],
matched_route_idx: 0, matched_route_idx: 0,
userDest: true userDest: true,
}); });
}); });
@@ -141,8 +140,8 @@ test('[dev-router] match with needed prefixed slash', async t => {
const routesConfig = [ const routesConfig = [
{ {
src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$', src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$',
dest: '/some/dest' dest: '/some/dest',
} },
]; ];
const result = await devRouter('/post-1/comments', 'GET', routesConfig); const result = await devRouter('/post-1/comments', 'GET', routesConfig);
@@ -155,9 +154,9 @@ test('[dev-router] match with needed prefixed slash', async t => {
uri_args: {}, uri_args: {},
matched_route: { matched_route: {
src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$', src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$',
dest: '/some/dest' dest: '/some/dest',
}, },
matched_route_idx: 0 matched_route_idx: 0,
}); });
}); });
@@ -167,9 +166,9 @@ test('[dev-router] `continue: true` with fallthrough', async t => {
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+', src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
continue: true, continue: true,
headers: { headers: {
'cache-control': 'immutable,max-age=31536000' 'cache-control': 'immutable,max-age=31536000',
} },
} },
]; ];
const result = await devRouter( const result = await devRouter(
'/_next/static/chunks/0.js', '/_next/static/chunks/0.js',
@@ -182,8 +181,8 @@ test('[dev-router] `continue: true` with fallthrough', async t => {
dest: '/_next/static/chunks/0.js', dest: '/_next/static/chunks/0.js',
uri_args: {}, uri_args: {},
headers: { headers: {
'cache-control': 'immutable,max-age=31536000' 'cache-control': 'immutable,max-age=31536000',
} },
}); });
}); });
@@ -193,13 +192,13 @@ test('[dev-router] `continue: true` with match', async t => {
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+', src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
continue: true, continue: true,
headers: { headers: {
'cache-control': 'immutable,max-age=31536000' 'cache-control': 'immutable,max-age=31536000',
} },
}, },
{ {
src: '/(.*)', src: '/(.*)',
dest: '/hi' dest: '/hi',
} },
]; ];
const result = await devRouter( const result = await devRouter(
'/_next/static/chunks/0.js', '/_next/static/chunks/0.js',
@@ -214,13 +213,13 @@ test('[dev-router] `continue: true` with match', async t => {
userDest: true, userDest: true,
uri_args: {}, uri_args: {},
headers: { headers: {
'cache-control': 'immutable,max-age=31536000' 'cache-control': 'immutable,max-age=31536000',
}, },
matched_route: { matched_route: {
src: '/(.*)', src: '/(.*)',
dest: '/hi' dest: '/hi',
}, },
matched_route_idx: 1 matched_route_idx: 1,
}); });
}); });
@@ -236,7 +235,7 @@ test('[dev-router] match with catch-all with prefix slash', async t => {
headers: {}, headers: {},
uri_args: {}, uri_args: {},
matched_route: { src: '/(.*)', dest: '/www/$1' }, matched_route: { src: '/(.*)', dest: '/www/$1' },
matched_route_idx: 0 matched_route_idx: 0,
}); });
}); });
@@ -252,14 +251,17 @@ test('[dev-router] match with catch-all with no prefix slash', async t => {
headers: {}, headers: {},
uri_args: {}, uri_args: {},
matched_route: { src: '(.*)', dest: '/www$1' }, matched_route: { src: '(.*)', dest: '/www$1' },
matched_route_idx: 0 matched_route_idx: 0,
}); });
}); });
test('[dev-router] `continue: true` with `dest`', async t => { test('[dev-router] `continue: true` with `dest`', async t => {
const routesConfig = [ const routesConfig = [
{ src: '/(.*)', dest: '/www/$1', continue: true }, { src: '/(.*)', dest: '/www/$1', continue: true },
{ src: '^/www/(a\\/([^\\/]+?)(?:\\/)?)$', dest: 'http://localhost:5000/$1' } {
src: '^/www/(a\\/([^\\/]+?)(?:\\/)?)$',
dest: 'http://localhost:5000/$1',
},
]; ];
const result = await devRouter('/a/foo', 'GET', routesConfig); const result = await devRouter('/a/foo', 'GET', routesConfig);
@@ -271,6 +273,6 @@ test('[dev-router] `continue: true` with `dest`', async t => {
uri_args: {}, uri_args: {},
matched_route: routesConfig[1], matched_route: routesConfig[1],
matched_route_idx: 1, matched_route_idx: 1,
userDest: false userDest: false,
}); });
}); });

View File

@@ -1,6 +1,8 @@
import url from 'url'; import url from 'url';
import test from 'ava'; import test from 'ava';
import path from 'path'; import path from 'path';
import execa from 'execa';
import fs from 'fs-extra';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import listen from 'async-listen'; import listen from 'async-listen';
import { request, createServer } from 'http'; import { request, createServer } from 'http';
@@ -9,16 +11,27 @@ import DevServer from '../src/util/dev/server';
import { installBuilders, getBuildUtils } from '../src/util/dev/builder-cache'; import { installBuilders, getBuildUtils } from '../src/util/dev/builder-cache';
import parseListen from '../src/util/dev/parse-listen'; import parseListen from '../src/util/dev/parse-listen';
async function runNpmInstall(fixturePath) {
if (await fs.exists(path.join(fixturePath, 'package.json'))) {
return execa('yarn', ['install'], { cwd: fixturePath });
}
}
function testFixture(name, fn) { function testFixture(name, fn) {
return async t => { return async t => {
let server; let server;
const fixturePath = path.join(__dirname, 'fixtures', 'unit', name);
await runNpmInstall(fixturePath);
try { try {
let readyResolve; let readyResolve;
let readyPromise = new Promise(resolve => { let readyPromise = new Promise(resolve => {
readyResolve = resolve; readyResolve = resolve;
}); });
const debug = false; const debug = true;
const output = createOutput({ debug }); const output = createOutput({ debug });
const origReady = output.ready; const origReady = output.ready;
@@ -29,7 +42,6 @@ function testFixture(name, fn) {
origReady(msg); origReady(msg);
}; };
const fixturePath = path.join(__dirname, `fixtures/unit/${name}`);
server = new DevServer(fixturePath, { output, debug }); server = new DevServer(fixturePath, { output, debug });
await server.start(0); await server.start(0);
@@ -317,8 +329,8 @@ test(
// HTML response // HTML response
const res = await fetch(`${server.address}/does-not-exist`, { const res = await fetch(`${server.address}/does-not-exist`, {
headers: { headers: {
Accept: 'text/html' Accept: 'text/html',
} },
}); });
t.is(res.status, 404); t.is(res.status, 404);
t.is(res.headers.get('content-type'), 'text/html; charset=utf-8'); t.is(res.headers.get('content-type'), 'text/html; charset=utf-8');
@@ -330,8 +342,8 @@ test(
// JSON response // JSON response
const res = await fetch(`${server.address}/does-not-exist`, { const res = await fetch(`${server.address}/does-not-exist`, {
headers: { headers: {
Accept: 'application/json' Accept: 'application/json',
} },
}); });
t.is(res.status, 404); t.is(res.status, 404);
t.is(res.headers.get('content-type'), 'application/json'); t.is(res.headers.get('content-type'), 'application/json');
@@ -389,10 +401,10 @@ test('[DevServer] parseListen()', t => {
t.deepEqual(parseListen('127.0.0.1:3005'), [3005, '127.0.0.1']); t.deepEqual(parseListen('127.0.0.1:3005'), [3005, '127.0.0.1']);
t.deepEqual(parseListen('tcp://127.0.0.1:5000'), [5000, '127.0.0.1']); t.deepEqual(parseListen('tcp://127.0.0.1:5000'), [5000, '127.0.0.1']);
t.deepEqual(parseListen('unix:/home/user/server.sock'), [ t.deepEqual(parseListen('unix:/home/user/server.sock'), [
'/home/user/server.sock' '/home/user/server.sock',
]); ]);
t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [ t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [
'\\\\.\\pipe\\PipeName' '\\\\.\\pipe\\PipeName',
]); ]);
let err; let err;

View File

@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==

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"
} }
} }

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