Compare commits

...

37 Commits

Author SHA1 Message Date
Steven
4e52f8532b Publish Canary
- @vercel/build-utils@2.12.3-canary.7
 - vercel@23.1.3-canary.9
 - @vercel/client@10.2.3-canary.7
 - @vercel/go@1.2.4-canary.3
 - @vercel/node@1.12.2-canary.4
 - @vercel/python@2.0.6-canary.4
 - @vercel/ruby@1.2.8-canary.3
2021-09-21 22:20:00 -04:00
Steven
702cb9e29c [all] Revert to @vercel/ncc@0.24.0 (#6749) 2021-09-21 22:17:06 -04:00
jj@jjsweb.site
d3d5555d79 Publish Canary
- @vercel/build-utils@2.12.3-canary.6
 - vercel@23.1.3-canary.8
 - @vercel/client@10.2.3-canary.6
 - @vercel/frameworks@0.5.1-canary.5
 - @vercel/routing-utils@1.11.4-canary.2
2021-09-20 19:35:05 -05:00
JJ Kasper
2fd3fc73e5 [routing-utils] Allow passing internal params to convertRewrites (#6742)
This adds an argument to allow passing internal param names that should be ignored when considering whether params should be auto-added to a rewrite's destination query. After adding this we should be able to resolve https://github.com/vercel/next.js/issues/27563 in the runtime where `convertRewrites` is called. 

This matches Next.js' handling for internal params which can be seen [here](e90825ad88/packages/next/shared/lib/router/utils/prepare-destination.ts (L203))

### Related Issues

x-ref: https://github.com/vercel/next.js/issues/27563

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-09-21 00:15:29 +00:00
Steven
de0b13a46e Publish Canary
- vercel@23.1.3-canary.7
 - @vercel/node@1.12.2-canary.3
2021-09-16 12:54:47 -04:00
Steven
d0fe85db92 [node] Bump nft to 0.14.0 (#6727) 2021-09-16 12:49:04 -04:00
jj@jjsweb.site
bfbd927320 Publish Canary
- @vercel/build-utils@2.12.3-canary.5
 - vercel@23.1.3-canary.6
 - @vercel/client@10.2.3-canary.5
 - @vercel/frameworks@0.5.1-canary.4
 - @vercel/routing-utils@1.11.4-canary.1
2021-09-14 15:02:32 -05:00
JJ Kasper
90bacf88b8 [routing-utils] Fix host segment replacing (#6713)
* Fix host segment replacing

* Add additional check
2021-09-14 13:20:35 -05:00
Steven
07c369c542 Publish Canary
- @vercel/build-utils@2.12.3-canary.4
 - vercel@23.1.3-canary.5
 - @vercel/client@10.2.3-canary.4
 - @vercel/go@1.2.4-canary.2
 - @vercel/node@1.12.2-canary.2
 - @vercel/python@2.0.6-canary.3
 - @vercel/ruby@1.2.8-canary.2
2021-09-13 16:32:45 -04:00
Steven
a2e4186ccb [all] Bump ncc to 0.31.1 (#6700)
https://github.com/vercel/ncc/releases/tag/0.31.0

https://github.com/vercel/ncc/releases/tag/0.31.1
2021-09-13 20:20:48 +00:00
Steven
6e1d708e3f Publish Canary
- vercel@23.1.3-canary.4
 - @vercel/python@2.0.6-canary.2
2021-09-08 13:56:20 -04:00
Noa
38503103c3 [python] Use pip --upgrade when installing function dependencies (#6683)
Co-authored-by: Steven <steven@ceriously.com>
2021-09-08 13:54:03 -04:00
jj@jjsweb.site
e8fec4b69c Publish Canary
- @vercel/build-utils@2.12.3-canary.3
 - vercel@23.1.3-canary.3
 - @vercel/client@10.2.3-canary.3
 - @vercel/frameworks@0.5.1-canary.3
 - @vercel/routing-utils@1.11.4-canary.0
2021-09-07 11:52:01 -05:00
JJ Kasper
b3ffcdf80d [routing-utils] Ensure headers with only has items are replaced correctly (#6686)
This ensures we replace header values correctly when no named segments are used and only has items are used. 

### Related Issues

Fixes: https://vercel.slack.com/archives/CHTTGQYQ4/p1631023974185700

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-09-07 16:17:01 +00:00
Leo Lamprecht
43c1a93c1d Enabled blank issues temporarily 2021-09-06 18:24:53 +02:00
Leo Lamprecht
5b118fd4e6 Disable blank issues again 2021-09-06 14:47:15 +02:00
Leo Lamprecht
8916b674af Temporarily enable blank issues 2021-09-06 14:41:32 +02:00
Brody McKee
1807f83c69 Update NPM publish token (#6672) 2021-09-03 19:44:59 +03:00
Steven
74e8ec7c64 Fix lerna publish for automation token (#6666)
Related to https://github.com/lerna/lerna/issues/2788
2021-09-03 00:20:20 +00:00
Steven
2644e3127b Publish Canary
- @vercel/build-utils@2.12.3-canary.2
 - vercel@23.1.3-canary.2
 - @vercel/client@10.2.3-canary.2
2021-09-02 08:42:33 -04:00
Leo Lamprecht
d77ac04b0c Disable blank issues again 2021-09-02 13:52:18 +02:00
Leo Lamprecht
0ef9c8df4d Temporarily enable issues 2021-09-02 13:45:20 +02:00
Steven
dfc4c98820 [build-utils] Fix custom 404 route (#6657)
The Custom 404 feature was originally implemented in #4563 but was matching too broadly (see [gist](https://gist.github.com/kjk/4dc57da62d7e715c687cc7914847ffb2))

This PR fixes the custom 404 route and updates the tests.
2021-09-02 00:12:45 +00:00
Steven
0e51884725 Publish Canary
- @vercel/build-utils@2.12.3-canary.1
 - vercel@23.1.3-canary.1
 - @vercel/client@10.2.3-canary.1
 - @vercel/frameworks@0.5.1-canary.2
 - @vercel/go@1.2.4-canary.1
 - @vercel/node-bridge@2.1.1-canary.1
 - @vercel/node@1.12.2-canary.1
 - @vercel/python@2.0.6-canary.1
 - @vercel/ruby@1.2.8-canary.1
2021-08-31 17:18:35 -04:00
Steven
1b264fe60e [build-utils] Add allowQuery to Lambda (#6651) 2021-08-31 17:17:31 -04:00
Nathan Rajlich
f18bca9718 [cli] Refactor legacy Now client to TypeScript (#6650) 2021-08-31 12:47:17 -07:00
Steven
c23dc73f41 [examples] Bump Next.js to 11.1.2 (#6648)
* Bump Next.js to 11.1.1

* Bump Next.js to 11.1.2
2021-08-31 10:53:18 -04:00
Nathan Rajlich
273718e0b7 [cli] Rewrite Unit tests to TypeScript + Jest (#6638)
* Rewrites the CLI unit tests to be TypeScript and use Jest (consistent with the unit tests in the other packages in this repo).
* The file structure of the new test files mirrors that of the `src` directory, so that it's easy to find the relevant tests.
* Code coverage is also properly set up through Jest - you can already see a big increase in coverage from this PR.
* Adds a mock API server framework, with the intention of making it possible to write granular tests of the CLI commands. Using mocks also has the benefit of not requiring `VERCEL_TOKEN` env var to be set, so 3rd party PRs will be able to have their tests run. Ultimately this will also help with test coverage, since we will be writing unit tests that test the commands' code directly.
* Converts `Output` into a proper class (which is subclassed for the mocks).
2021-08-30 22:07:09 +00:00
Nathan Rajlich
230b88bf9b [cli] Remove broken vc downgrade command (#6643)
It maps to an "upgrade" command, which does not exist.

Fixes:

```
$ vc downgrade
Vercel CLI 23.1.2
Error! Cannot find module './upgrade'
```
2021-08-28 18:01:31 +00:00
Nathan Rajlich
676a3d2568 [cli] Refactor src/index to TypeScript (#6602)
Refactors the CLI entrypoint `src/index.js` to TypeScript.
2021-08-27 19:48:31 +00:00
Nathan Rajlich
f221f041d0 Update @vercel/ncc to v0.29.2 (#6605) 2021-08-27 10:03:36 -07:00
Kaitlyn
aca42b2aac [examples] Update angular example to npm 7 (#6636) 2021-08-25 17:24:37 -07:00
Kaitlyn Carter
cf11a8efb5 Revert "delete yarn.lock"
This reverts commit b941715d7b.
2021-08-25 18:43:29 -04:00
Kaitlyn Carter
be09349daf Revert "update npm peer dependencies"
This reverts commit a01372bcbb.
2021-08-25 18:34:21 -04:00
Kaitlyn Carter
a01372bcbb update npm peer dependencies 2021-08-25 17:51:22 -04:00
Kaitlyn Carter
b941715d7b delete yarn.lock 2021-08-25 17:48:44 -04:00
Nathan Rajlich
ee9a8a0415 [cli] Refactor vc teams to TypeScript (#6610) 2021-08-24 11:08:30 -07:00
123 changed files with 17593 additions and 10642 deletions

View File

@@ -1,4 +1,4 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Bug Report
url: https://vercel.com/support/request

View File

@@ -29,6 +29,6 @@ jobs:
- name: Publish
run: yarn publish-from-github
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}

View File

@@ -6,3 +6,5 @@ coverage:
project: off
patch: off
fixes:
- "::packages/cli/" # move root e.g., "path/" => "after/path/"

13218
examples/angular/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "nextjs",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"next": "11.1.0",
"next": "11.1.2",
"react": "17.0.2",
"react-dom": "17.0.2"
},

View File

@@ -43,14 +43,7 @@
core-js-pure "^3.16.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2":
"@babel/runtime@7.15.3", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2":
version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
@@ -119,10 +112,10 @@
resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c"
integrity sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA==
"@next/env@11.1.0":
version "11.1.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-11.1.0.tgz#cae83d8e0a65aa9f2af3368f8269ffd9d911746a"
integrity sha512-zPJkMFRenSf7BLlVee8987G0qQXAhxy7k+Lb/5hLAGkPVHAHm+oFFeL+2ipbI2KTEFlazdmGY0M+AlLQn7pWaw==
"@next/env@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/env/-/env-11.1.2.tgz#27996efbbc54c5f949f5e8c0a156e3aa48369b99"
integrity sha512-+fteyVdQ7C/OoulfcF6vd1Yk0FEli4453gr8kSFbU8sKseNSizYq6df5MKz/AjwLptsxrUeIkgBdAzbziyJ3mA==
"@next/eslint-plugin-next@11.1.0":
version "11.1.0"
@@ -131,15 +124,15 @@
dependencies:
glob "7.1.7"
"@next/polyfill-module@11.1.0":
version "11.1.0"
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.1.0.tgz#ee6b9117a1f9bb137479dfa51d5a9e38e066a62f"
integrity sha512-64EgW8SzJRQls2yJ5DkuljRxgE24o2kYtX/ghTkPUJYsfidHMWzQGwg26IgRbb/uHqTd1G0W5UkKag+Nt8TWaQ==
"@next/polyfill-module@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.1.2.tgz#1fe92c364fdc81add775a16c678f5057c6aace98"
integrity sha512-xZmixqADM3xxtqBV0TpAwSFzWJP0MOQzRfzItHXf1LdQHWb0yofHHC+7eOrPFic8+ZGz5y7BdPkkgR1S25OymA==
"@next/react-dev-overlay@11.1.0":
version "11.1.0"
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.1.0.tgz#8d4e8020a4cbdacbca431a0bf40c4d28187083af"
integrity sha512-h+ry0sTk1W3mJw+TwEf91aqLbBJ5oqAsxfx+QryqEItNtfW6zLSSjxkyTYTqX8DkgSssQQutQfATkzBVgOR+qQ==
"@next/react-dev-overlay@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.1.2.tgz#73795dc5454b7af168bac93df7099965ebb603be"
integrity sha512-rDF/mGY2NC69mMg2vDqzVpCOlWqnwPUXB2zkARhvknUHyS6QJphPYv9ozoPJuoT/QBs49JJd9KWaAzVBvq920A==
dependencies:
"@babel/code-frame" "7.12.11"
anser "1.4.9"
@@ -153,10 +146,30 @@
stacktrace-parser "0.1.10"
strip-ansi "6.0.0"
"@next/react-refresh-utils@11.1.0":
version "11.1.0"
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.1.0.tgz#60c3c7b127a5dab8b0a2889a7dcf8a90d2c4e592"
integrity sha512-g5DtFTpLTGa36iy9DuZawtJeitI11gysFGKPQQqy+mNbSFazguArcJ10gAYFlbqpIi4boUamWNI5mAoSPx3kog==
"@next/react-refresh-utils@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.1.2.tgz#44ea40d8e773e4b77bad85e24f6ac041d5e4b4a5"
integrity sha512-hsoJmPfhVqjZ8w4IFzoo8SyECVnN+8WMnImTbTKrRUHOVJcYMmKLL7xf7T0ft00tWwAl/3f3Q3poWIN2Ueql/Q==
"@next/swc-darwin-arm64@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.2.tgz#93226c38db488c4b62b30a53b530e87c969b8251"
integrity sha512-hZuwOlGOwBZADA8EyDYyjx3+4JGIGjSHDHWrmpI7g5rFmQNltjlbaefAbiU5Kk7j3BUSDwt30quJRFv3nyJQ0w==
"@next/swc-darwin-x64@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.2.tgz#792003989f560c00677b5daeff360b35b510db83"
integrity sha512-PGOp0E1GisU+EJJlsmJVGE+aPYD0Uh7zqgsrpD3F/Y3766Ptfbe1lEPPWnRDl+OzSSrSrX1lkyM/Jlmh5OwNvA==
"@next/swc-linux-x64-gnu@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.2.tgz#8216b2ae1f21f0112958735c39dd861088108f37"
integrity sha512-YcDHTJjn/8RqvyJVB6pvEKXihDcdrOwga3GfMv/QtVeLphTouY4BIcEUfrG5+26Nf37MP1ywN3RRl1TxpurAsQ==
"@next/swc-win32-x64-msvc@11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.2.tgz#e15824405df137129918205e43cb5e9339589745"
integrity sha512-e/pIKVdB+tGQYa1cW3sAeHm8gzEri/HYLZHT4WZojrUxgWXqx8pk7S7Xs47uBcFTqBDRvK3EcQpPLf3XdVsDdg==
"@node-rs/helper@1.2.1":
version "1.2.1"
@@ -1951,17 +1964,17 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next@11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-11.1.0.tgz#767d4c4fa0b9b0c768cdbd6c9f03dd86b5d701c0"
integrity sha512-GHBk/c7Wyr6YbFRFZF37I0X7HKzkHHI8pur/loyXo5AIE8wdkbGPGO0ds3vNAO6f8AxZAKGCRYtAzoGlVLoifA==
next@11.1.2:
version "11.1.2"
resolved "https://registry.yarnpkg.com/next/-/next-11.1.2.tgz#527475787a9a362f1bc916962b0c0655cc05bc91"
integrity sha512-azEYL0L+wFjv8lstLru3bgvrzPvK0P7/bz6B/4EJ9sYkXeW8r5Bjh78D/Ol7VOg0EIPz0CXoe72hzAlSAXo9hw==
dependencies:
"@babel/runtime" "7.12.5"
"@babel/runtime" "7.15.3"
"@hapi/accept" "5.0.2"
"@next/env" "11.1.0"
"@next/polyfill-module" "11.1.0"
"@next/react-dev-overlay" "11.1.0"
"@next/react-refresh-utils" "11.1.0"
"@next/env" "11.1.2"
"@next/polyfill-module" "11.1.2"
"@next/react-dev-overlay" "11.1.2"
"@next/react-refresh-utils" "11.1.2"
"@node-rs/helper" "1.2.1"
assert "2.0.0"
ast-types "0.13.2"
@@ -1999,13 +2012,18 @@ next@11.1.0:
stream-browserify "3.0.0"
stream-http "3.1.1"
string_decoder "1.3.0"
styled-jsx "4.0.0"
styled-jsx "4.0.1"
timers-browserify "2.0.12"
tty-browserify "0.0.1"
use-subscription "1.5.1"
util "0.12.3"
util "0.12.4"
vm-browserify "1.1.2"
watchpack "2.1.1"
optionalDependencies:
"@next/swc-darwin-arm64" "11.1.2"
"@next/swc-darwin-x64" "11.1.2"
"@next/swc-linux-x64-gnu" "11.1.2"
"@next/swc-win32-x64-msvc" "11.1.2"
node-fetch@2.6.1:
version "2.6.1"
@@ -2873,10 +2891,10 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
styled-jsx@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-4.0.0.tgz#f7b90e7889d0a4f4635f8d1ae9ac32f3acaedc57"
integrity sha512-2USeoWMoJ/Lx5s2y1PxuvLy/cz2Yrr8cTySV3ILHU1Vmaw1bnV7suKdblLPjnyhMD+qzN7B1SWyh4UZTARn/WA==
styled-jsx@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-4.0.1.tgz#ae3f716eacc0792f7050389de88add6d5245b9e9"
integrity sha512-Gcb49/dRB1k8B4hdK8vhW27Rlb2zujCk1fISrizCcToIs+55B4vmUM0N9Gi4nnVfFZWe55jRdWpAqH1ldAKWvQ==
dependencies:
"@babel/plugin-syntax-jsx" "7.14.5"
"@babel/types" "7.15.0"
@@ -3073,10 +3091,10 @@ util@0.10.3:
dependencies:
inherits "2.0.1"
util@0.12.3:
version "0.12.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.3.tgz#971bb0292d2cc0c892dab7c6a5d37c2bec707888"
integrity sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==
util@0.12.4, util@^0.12.0:
version "0.12.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
@@ -3092,18 +3110,6 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
util@^0.12.0:
version "0.12.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
is-generator-function "^1.0.7"
is-typed-array "^1.1.3"
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

View File

@@ -17,7 +17,6 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "4.28.0",
"@typescript-eslint/parser": "4.28.0",
"@vercel/ncc": "0.24.0",
"async-retry": "1.2.3",
"buffer-replace": "1.0.0",
"cheerio": "1.0.0-rc.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.12.3-canary.0",
"version": "2.12.3-canary.7",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -23,14 +23,14 @@
"@types/end-of-stream": "^1.4.0",
"@types/fs-extra": "^5.0.5",
"@types/glob": "^7.1.1",
"@types/jest": "26.0.24",
"@types/jest": "27.0.1",
"@types/js-yaml": "3.12.1",
"@types/ms": "0.7.31",
"@types/multistream": "2.1.1",
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.5.1-canary.1",
"@vercel/frameworks": "0.5.1-canary.5",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -1030,7 +1030,7 @@ function getRouteResult(
// https://nextjs.org/docs/advanced-features/custom-error-page
errorRoutes.push({
status: 404,
src: '^/(?!.*api).*$',
src: '^(?!/api).*$',
dest: options.cleanUrls ? '/404' : '/404.html',
});
}

View File

@@ -100,4 +100,4 @@ class FileFsRef implements File {
}
}
export = FileFsRef;
export default FileFsRef;

View File

@@ -19,6 +19,7 @@ interface LambdaOptions {
memory?: number;
maxDuration?: number;
environment: Environment;
allowQuery?: string[];
}
interface CreateLambdaOptions {
@@ -28,6 +29,7 @@ interface CreateLambdaOptions {
memory?: number;
maxDuration?: number;
environment?: Environment;
allowQuery?: string[];
}
interface GetLambdaOptionsFromFunctionOptions {
@@ -43,6 +45,7 @@ export class Lambda {
public memory?: number;
public maxDuration?: number;
public environment: Environment;
public allowQuery?: string[];
constructor({
zipBuffer,
@@ -51,6 +54,7 @@ export class Lambda {
maxDuration,
memory,
environment,
allowQuery,
}: LambdaOptions) {
this.type = 'Lambda';
this.zipBuffer = zipBuffer;
@@ -59,6 +63,7 @@ export class Lambda {
this.memory = memory;
this.maxDuration = maxDuration;
this.environment = environment;
this.allowQuery = allowQuery;
}
}
@@ -72,6 +77,7 @@ export async function createLambda({
memory,
maxDuration,
environment = {},
allowQuery,
}: CreateLambdaOptions): Promise<Lambda> {
assert(typeof files === 'object', '"files" must be an object');
assert(typeof handler === 'string', '"handler" is not a string');
@@ -86,6 +92,14 @@ export async function createLambda({
assert(typeof maxDuration === 'number', '"maxDuration" is not a number');
}
if (allowQuery !== undefined) {
assert(Array.isArray(allowQuery), '"allowQuery" is not an Array');
assert(
allowQuery.every(q => typeof q === 'string'),
'"allowQuery" is not a string Array'
);
}
await sema.acquire();
try {
@@ -131,9 +145,7 @@ export async function createZip(files: Files): Promise<Buffer> {
}
zipFile.end();
streamToBuffer(zipFile.outputStream)
.then(resolve)
.catch(reject);
streamToBuffer(zipFile.outputStream).then(resolve).catch(reject);
});
return zipBuffer;

View File

@@ -2013,15 +2013,11 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
framework: 'redwoodjs',
};
const {
builders,
defaultRoutes,
rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
const { builders, defaultRoutes, rewriteRoutes, errorRoutes } =
await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
expect(builders).toStrictEqual([
{
@@ -2038,7 +2034,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
src: '^(?!/api).*$',
dest: '/404.html',
},
]);
@@ -2050,15 +2046,11 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
framework: 'redwoodjs',
};
const {
builders,
defaultRoutes,
rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
const { builders, defaultRoutes, rewriteRoutes, errorRoutes } =
await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
expect(builders).toStrictEqual([
{
@@ -2096,7 +2088,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
src: '^(?!/api).*$',
dest: '/404.html',
},
]);
@@ -2417,7 +2409,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
src: '^(?!/api).*$',
dest: '/404.html',
},
]);
@@ -2435,6 +2427,11 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
'/another/sub/index.html',
'/another/sub/page.html',
'/another/sub/page',
'/another/api',
'/another/api/page.html',
'/rapid',
'/rapid/page.html',
'/health-api.html',
].forEach(file => {
expect(file).toMatch(pattern);
});
@@ -2443,12 +2440,12 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
'/api',
'/api/',
'/api/index.html',
'/api/page.html',
'/api/page',
'/api/users.js',
'/api/users',
'/api/sub',
'/api/sub/index.html',
'/api/sub/page.html',
'/api/sub/page',
'/api/sub/users.js',
'/api/sub/users',
].forEach(file => {
expect(file).not.toMatch(pattern);
});
@@ -2819,12 +2816,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes, errorRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -2836,7 +2829,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
src: '^(?!/api).*$',
dest: '/404',
},
]);
@@ -2904,11 +2897,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -2936,11 +2926,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
'api/[endpoint]/[id].js',
];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -2974,11 +2961,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
const files = ['public/index.html', 'api/[endpoint].js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, pkg, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, pkg, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3004,11 +2988,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/date/index.js', 'api/date.js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3022,11 +3003,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/date.js', 'api/[date]/index.js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3051,11 +3029,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
'api/food.ts',
'api/ts/gold.ts',
];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3071,11 +3046,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
const files = ['api/user.php'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, { functions, ...options });
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, { functions, ...options });
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3105,11 +3077,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3152,11 +3121,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3184,11 +3150,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
'api/[endpoint]/[id].js',
];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3222,11 +3185,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
const files = ['public/index.html', 'api/[endpoint].js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, pkg, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, pkg, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3245,11 +3205,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/date/index.js', 'api/date.js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3263,11 +3220,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/date.js', 'api/[date]/index.js'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3292,11 +3246,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
'api/food.ts',
'api/ts/gold.ts',
];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, options);
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([
@@ -3312,11 +3263,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
const files = ['api/user.php'];
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
} = await detectBuilders(files, null, { functions, ...options });
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
await detectBuilders(files, null, { functions, ...options });
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([

View File

@@ -1,11 +0,0 @@
declare module 'intercept-stdout' {
export default function (fn?: InterceptFn): UnhookIntercept
}
interface InterceptFn {
(text: string): string | void
}
interface UnhookIntercept {
(): void
}

View File

@@ -1,5 +0,0 @@
declare module 'promisepipe' {
export default function (
...streams: Array<NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream>
): Promise<void>
}

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "23.1.3-canary.0",
"version": "23.1.3-canary.9",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -12,33 +12,15 @@
},
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js test/dev-validate.unit.js --serial --fail-fast --verbose",
"test": "jest",
"test-unit": "jest --coverage --verbose",
"test-integration-cli": "rimraf test/fixtures/integration && ava test/integration.js --serial --fail-fast --verbose",
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"prepublishOnly": "yarn build",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"coverage": "codecov",
"build": "node -r ts-eager/register ./scripts/build.ts",
"build-dev": "node -r ts-eager/register ./scripts/build.ts --dev"
},
"nyc": {
"include": [
"src/**"
],
"extension": [
".js",
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text",
"html"
],
"sourceMap": true,
"instrument": true,
"all": true
},
"bin": {
"vc": "./dist/index.js",
"vercel": "./dist/index.js"
@@ -61,11 +43,11 @@
"node": ">= 12"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.0",
"@vercel/go": "1.2.4-canary.0",
"@vercel/node": "1.12.2-canary.0",
"@vercel/python": "2.0.6-canary.0",
"@vercel/ruby": "1.2.8-canary.0",
"@vercel/build-utils": "2.12.3-canary.7",
"@vercel/go": "1.2.4-canary.3",
"@vercel/node": "1.12.2-canary.4",
"@vercel/python": "2.0.6-canary.4",
"@vercel/ruby": "1.2.8-canary.3",
"update-notifier": "4.1.0"
},
"devDependencies": {
@@ -76,13 +58,16 @@
"@types/ansi-regex": "4.0.0",
"@types/async-retry": "1.2.1",
"@types/bytes": "3.0.0",
"@types/chance": "1.1.3",
"@types/debug": "0.0.31",
"@types/dotenv": "6.1.1",
"@types/escape-html": "0.0.20",
"@types/express": "4.17.13",
"@types/fs-extra": "5.0.5",
"@types/glob": "7.1.1",
"@types/http-proxy": "1.16.2",
"@types/inquirer": "7.3.1",
"@types/jest": "27.0.1",
"@types/load-json-file": "2.0.7",
"@types/mime-types": "2.1.0",
"@types/minimatch": "3.0.3",
@@ -99,9 +84,10 @@
"@types/text-table": "0.2.0",
"@types/title": "3.4.1",
"@types/universal-analytics": "0.4.2",
"@types/update-notifier": "5.1.0",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.5.1-canary.1",
"@vercel/frameworks": "0.5.1-canary.5",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",
@@ -116,6 +102,7 @@
"ava": "2.2.0",
"bytes": "3.0.0",
"chalk": "4.1.0",
"chance": "1.1.7",
"chokidar": "3.3.1",
"clipboardy": "2.1.0",
"codecov": "3.8.2",
@@ -131,6 +118,7 @@
"escape-html": "1.0.3",
"esm": "3.1.4",
"execa": "3.2.0",
"express": "4.17.1",
"fast-deep-equal": "3.1.3",
"fs-extra": "7.0.1",
"get-port": "5.1.1",
@@ -149,7 +137,6 @@
"ms": "2.1.2",
"node-fetch": "2.6.1",
"npm-package-arg": "6.1.0",
"nyc": "13.2.0",
"open": "8.2.0",
"ora": "3.4.0",
"pcre-to-regexp": "1.0.0",
@@ -162,7 +149,6 @@
"rimraf": "3.0.2",
"semver": "5.5.0",
"serve-handler": "6.1.1",
"sinon": "4.4.2",
"strip-ansi": "5.2.0",
"stripe": "5.1.0",
"tar-fs": "1.16.3",
@@ -179,5 +165,19 @@
"which": "2.0.2",
"write-json-file": "2.2.0",
"xdg-app-paths": "5.1.0"
},
"jest": {
"preset": "ts-jest",
"globals": {
"ts-jest": {
"diagnostics": false,
"isolatedModules": true
}
},
"verbose": false,
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test/**/*.test.ts"
]
}
}

View File

@@ -4,6 +4,7 @@ import { join } from 'path';
import { remove, writeFile } from 'fs-extra';
const dirRoot = join(__dirname, '..');
const distRoot = join(dirRoot, 'dist');
async function createConstants() {
console.log('Creating constants.ts');
@@ -48,13 +49,12 @@ async function main() {
// Do the initial `ncc` build
console.log();
const src = join(dirRoot, 'src');
const args = ['ncc', 'build', '--external', 'update-notifier'];
if (isDev) {
args.push('--source-map');
}
args.push(src);
await execa('yarn', args, { stdio: 'inherit' });
args.push('src/index.ts');
await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot });
// `ncc` has some issues with `@zeit/fun`'s runtime files:
// - Executable bits on the `bootstrap` files appear to be lost:
@@ -72,19 +72,13 @@ async function main() {
dirRoot,
'../../node_modules/@zeit/fun/dist/src/runtimes'
);
const dest = join(dirRoot, 'dist/runtimes');
await cpy('**/*', dest, { parents: true, cwd: runtimes });
await cpy('**/*', join(distRoot, 'runtimes'), {
parents: true,
cwd: runtimes,
});
// Band-aid to delete stuff that `ncc` bundles, but it shouldn't:
// TypeScript definition files from `@vercel/build-utils`
await remove(join(dirRoot, 'dist', 'dist'));
// The Readme and `package.json` from "config-chain" module
await remove(join(dirRoot, 'dist', 'config-chain'));
// A bunch of source `.ts` files from CLI's `util` directory
await remove(join(dirRoot, 'dist', 'util'));
// Band-aid to bundle stuff that `ncc` neglects to bundle
await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot);
console.log('Finished building Vercel CLI');
}

View File

@@ -421,7 +421,7 @@ function handleCreateAliasError<T>(
return error;
}
function getTargetsForAlias(args: string[], { alias }: VercelConfig) {
function getTargetsForAlias(args: string[], { alias }: VercelConfig = {}) {
if (args.length) {
return [args[args.length - 1]]
.map(target => (target.indexOf('.') !== -1 ? toHost(target) : target))

View File

@@ -120,10 +120,7 @@ export default async (client: Client) => {
paths = [process.cwd()];
}
let localConfig: VercelConfig | null = client.localConfig;
if (!localConfig || localConfig instanceof Error) {
localConfig = readLocalConfig(paths[0]);
}
let localConfig = client.localConfig || readLocalConfig(paths[0]);
for (const path of paths) {
try {
@@ -439,7 +436,13 @@ export default async (client: Client) => {
}
const currentTeam = org?.type === 'team' ? org.id : undefined;
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam });
const now = new Now({
apiUrl,
token,
debug: debugEnabled,
currentTeam,
output,
});
let deployStamp = stamp();
let deployment = null;

View File

@@ -13,8 +13,7 @@ import setupAndLink from '../../util/link/setup-and-link';
import getSystemEnvValues from '../../util/env/get-system-env-values';
type Options = {
'--debug'?: boolean;
'--listen'?: string;
'--listen': string;
'--confirm': boolean;
};
@@ -27,7 +26,6 @@ export default async function dev(
const [dir = '.'] = args;
let cwd = resolve(dir);
const listen = parseListen(opts['--listen'] || '3000');
const debug = opts['--debug'] || false;
// retrieve dev command
let [link, frameworks] = await Promise.all([
@@ -94,7 +92,6 @@ export default async function dev(
const devServer = new DevServer(cwd, {
output,
debug,
devCommand,
frameworkSlug,
projectSettings,

View File

@@ -12,7 +12,7 @@ import param from '../../util/output/param';
import getDomainAliases from '../../util/alias/get-domain-aliases';
import getDomainByName from '../../util/domains/get-domain-by-name';
import promptBool from '../../util/input/prompt-bool';
import getTeams from '../../util/get-teams';
import getTeams from '../../util/teams/get-teams';
import { getCommandName } from '../../util/pkg-name';
type Options = {

View File

@@ -10,7 +10,6 @@ export default new Map([
['dns', 'dns'],
['domain', 'domains'],
['domains', 'domains'],
['downgrade', 'upgrade'],
['env', 'env'],
['help', 'help'],
['init', 'init'],

View File

@@ -117,9 +117,7 @@ export default async function main(client: Client) {
const { builds } =
deployment.version === 2
? await client.fetch<{ builds: Build[] }>(
`/v1/now/deployments/${id}/builds`
)
? await client.fetch<{ builds: Build[] }>(`/v1/deployments/${id}/builds`)
: { builds: [] };
log(

View File

@@ -92,8 +92,8 @@ export default async function main(client: Client) {
return 1;
}
let app: string | null = argv._[1];
let host: string | null = null;
let app: string | undefined = argv._[1];
let host: string | undefined = undefined;
if (argv['--help']) {
help();
@@ -156,7 +156,7 @@ export default async function main(client: Client) {
return 1;
}
app = null;
app = undefined;
host = asHost;
}

View File

@@ -2,7 +2,6 @@ import { validate as validateEmail } from 'email-validator';
import chalk from 'chalk';
import hp from '../util/humanize-path';
import getArgs from '../util/get-args';
import handleError from '../util/handle-error';
import logo from '../util/output/logo';
import prompt from '../util/login/prompt';
import doSamlLogin from '../util/login/saml';
@@ -52,20 +51,14 @@ const help = () => {
};
export default async function login(client: Client): Promise<number> {
let argv;
const { output } = client;
try {
argv = getArgs(client.argv.slice(2), {
'--oob': Boolean,
'--github': Boolean,
'--gitlab': Boolean,
'--bitbucket': Boolean,
});
} catch (err) {
handleError(err);
return 1;
}
const argv = getArgs(client.argv.slice(2), {
'--oob': Boolean,
'--github': Boolean,
'--gitlab': Boolean,
'--bitbucket': Boolean,
});
if (argv['--help']) {
help();
@@ -115,8 +108,7 @@ export default async function login(client: Client): Promise<number> {
}
}
// When `result` is a string it's the user's authentication token.
// It needs to be saved to the configuration file.
// Save the user's authentication token to the configuration file.
client.authConfig.token = result.token;
writeToAuthConfigFile(client.authConfig);
@@ -124,9 +116,9 @@ export default async function login(client: Client): Promise<number> {
output.debug(`Saved credentials in "${hp(getGlobalPathConfig())}"`);
console.log(
output.print(
`${chalk.cyan('Congratulations!')} ` +
`You are now logged in. In order to deploy something, run ${getCommandName()}.`
`You are now logged in. In order to deploy something, run ${getCommandName()}.\n`
);
output.print(

View File

@@ -1,5 +1,5 @@
import chalk from 'chalk';
import stamp from '../../util/output/stamp.ts';
import stamp from '../../util/output/stamp';
import info from '../../util/output/info';
import eraseLines from '../../util/output/erase-lines';
import chars from '../../util/output/chars';
@@ -7,14 +7,17 @@ import note from '../../util/output/note';
import textInput from '../../util/input/text';
import invite from './invite';
import { writeToConfigFile } from '../../util/config/files';
import { getPkgName, getCommandName } from '../../util/pkg-name.ts';
import { getPkgName, getCommandName } from '../../util/pkg-name';
import Client from '../../util/client';
import createTeam from '../../util/teams/create-team';
import patchTeam from '../../util/teams/patch-team';
const validateSlugKeypress = (data, value) =>
const validateSlugKeypress = (data: string, value: string) =>
// TODO: the `value` here should contain the current value + the keypress
// should be fixed on utils/input/text.js
/^[a-zA-Z]+[a-zA-Z0-9_-]*$/.test(value + data);
const validateNameKeypress = (data, value) =>
const validateNameKeypress = (data: string, value: string) =>
// TODO: the `value` here should contain the current value + the keypress
// should be fixed on utils/input/text.js
/^[ a-zA-Z0-9_-]+$/.test(value + data);
@@ -32,14 +35,14 @@ const gracefulExit = () => {
const teamUrlPrefix = 'Team URL'.padEnd(14) + chalk.gray('vercel.com/');
const teamNamePrefix = 'Team Name'.padEnd(14);
export default async function add(client, teams) {
export default async function add(client: Client): Promise<number> {
let slug;
let team;
let elapsed;
const { output } = client;
output.log(
`Pick a team identifier for its url (e.g.: ${chalk.cyan(
`Pick a team identifier for its URL (e.g.: ${chalk.cyan(
'`vercel.com/acme`'
)})`
);
@@ -65,14 +68,12 @@ export default async function add(client, teams) {
elapsed = stamp();
output.spinner(teamUrlPrefix + slug);
let res;
try {
// eslint-disable-next-line no-await-in-loop
res = await teams.create({ slug });
team = res;
team = await createTeam(client, { slug });
} catch (err) {
output.stopSpinner();
process.stdout.write(eraseLines(2));
output.print(eraseLines(2));
output.error(err.message);
}
} while (!team);
@@ -103,11 +104,12 @@ export default async function add(client, teams) {
elapsed = stamp();
output.spinner(teamNamePrefix + name);
const res = await teams.edit({ id: team.id, name });
const res = await patchTeam(client, team.id, { name });
output.stopSpinner();
process.stdout.write(eraseLines(2));
/*
if (res.error) {
output.error(res.error.message);
output.log(`${chalk.red(`${teamNamePrefix}`)}${name}`);
@@ -116,33 +118,25 @@ export default async function add(client, teams) {
// TODO: maybe we want to ask the user to retry? not sure if
// there's a scenario where that would be wanted
}
*/
team = Object.assign(team, res);
output.success(`Team name saved ${elapsed()}`);
output.log(`${chalk.cyan(`${chars.tick} `) + teamNamePrefix + team.name}\n`);
output.spinner('Saving');
// Update config file
const configCopy = Object.assign({}, client.config);
if (configCopy.sh) {
configCopy.sh.currentTeam = team;
} else {
configCopy.currentTeam = team.id;
}
writeToConfigFile(configCopy);
output.spinner('Saving');
client.config.currentTeam = team.id;
writeToConfigFile(client.config);
output.stopSpinner();
await invite(client, { _: [] }, teams, {
await invite(client, [], {
introMsg: 'Invite your teammates! When done, press enter on an empty field',
noopMsg: `You can invite teammates later by running ${getCommandName(
`teams invite`
)}`,
});
gracefulExit();
return gracefulExit();
}

View File

@@ -1,6 +1,5 @@
import chalk from 'chalk';
import error from '../../util/output/error';
import NowTeams from '../../util/teams';
import logo from '../../util/output/logo';
import list from './list';
import add from './add';
@@ -8,7 +7,6 @@ import change from './switch';
import invite from './invite';
import { getPkgName } from '../../util/pkg-name';
import getArgs from '../../util/get-args';
import handleError from '../../util/handle-error';
import Client from '../../util/client';
const help = () => {
@@ -61,28 +59,11 @@ const help = () => {
`);
};
let argv;
let debug;
let apiUrl;
let subcommand;
export default async (client: Client) => {
try {
argv = getArgs(client.argv.slice(2), {
'--since': String,
'--until': String,
'--next': Number,
'-N': '--next',
});
} catch (error) {
handleError(error);
return 1;
}
let subcommand;
debug = argv['--debug'];
apiUrl = client.apiUrl;
const isSwitch = argv._[0] && argv._[0] === 'switch';
const argv = getArgs(client.argv.slice(2), undefined, { permissive: true });
const isSwitch = argv._[0] === 'switch';
argv._ = argv._.slice(1);
@@ -97,19 +78,11 @@ export default async (client: Client) => {
return 2;
}
const {
authConfig: { token },
config,
} = client;
const { currentTeam } = config;
const teams = new NowTeams({ apiUrl, token, debug, currentTeam });
let exitCode;
let exitCode = 0;
switch (subcommand) {
case 'list':
case 'ls': {
exitCode = await list(client, argv, teams);
exitCode = await list(client);
break;
}
case 'switch':
@@ -119,12 +92,12 @@ export default async (client: Client) => {
}
case 'add':
case 'create': {
exitCode = await add(client, teams);
exitCode = await add(client);
break;
}
case 'invite': {
exitCode = await invite(client, argv, teams);
exitCode = await invite(client, argv._);
break;
}
default: {
@@ -137,6 +110,5 @@ export default async (client: Client) => {
help();
}
}
teams.close();
return exitCode || 0;
return exitCode;
};

View File

@@ -1,15 +1,19 @@
import chalk from 'chalk';
import { email as regexEmail } from '../../util/input/regexes';
import cmd from '../../util/output/cmd.ts';
import stamp from '../../util/output/stamp.ts';
import param from '../../util/output/param.ts';
import Client from '../../util/client';
import cmd from '../../util/output/cmd';
import stamp from '../../util/output/stamp';
import param from '../../util/output/param';
import chars from '../../util/output/chars';
import textInput from '../../util/input/text';
import eraseLines from '../../util/output/erase-lines';
import getUser from '../../util/get-user.ts';
import { getCommandName } from '../../util/pkg-name.ts';
import getUser from '../../util/get-user';
import { getCommandName } from '../../util/pkg-name';
import { email as regexEmail } from '../../util/input/regexes';
import getTeams from '../../util/teams/get-teams';
import inviteUserToTeam from '../../util/teams/invite-user-to-team';
const validateEmail = data => regexEmail.test(data.trim()) || data.length === 0;
const validateEmail = (data: string) =>
regexEmail.test(data.trim()) || data.length === 0;
const domains = Array.from(
new Set([
@@ -28,12 +32,12 @@ const domains = Array.from(
])
);
const emailAutoComplete = (value, teamSlug) => {
const emailAutoComplete = (value: string, teamSlug: string) => {
const parts = value.split('@');
if (parts.length === 2 && parts[1].length > 0) {
const [, host] = parts;
let suggestion = false;
let suggestion: string | false = false;
domains.unshift(teamSlug);
for (const domain of domains) {
@@ -51,17 +55,16 @@ const emailAutoComplete = (value, teamSlug) => {
};
export default async function invite(
client,
argv,
teams,
{ introMsg, noopMsg = 'No changes made' } = {}
) {
client: Client,
emails: string[] = [],
{ introMsg = '', noopMsg = 'No changes made' } = {}
): Promise<number> {
const { config, output } = client;
const { currentTeam: currentTeamId } = config;
output.spinner('Fetching teams');
const list = (await teams.ls()).teams;
const currentTeam = list.find(team => team.id === currentTeamId);
const teams = await getTeams(client);
const currentTeam = teams.find(team => team.id === currentTeamId);
output.spinner('Fetching user information');
let user;
@@ -93,8 +96,8 @@ export default async function invite(
introMsg || `Inviting team members to ${chalk.bold(currentTeam.name)}`
);
if (argv._.length > 0) {
for (const email of argv._) {
if (emails.length > 0) {
for (const email of emails) {
if (regexEmail.test(email)) {
output.spinner(email);
const elapsed = stamp();
@@ -102,8 +105,8 @@ export default async function invite(
try {
// eslint-disable-next-line no-await-in-loop
const res = await teams.inviteUser({ teamId: currentTeam.id, email });
userInfo = res.name || res.username;
const res = await inviteUserToTeam(client, currentTeam.id, email);
userInfo = res.username;
} catch (err) {
if (err.code === 'user_not_found') {
output.error(`No user exists with the email address "${email}".`);
@@ -122,12 +125,11 @@ export default async function invite(
output.log(`${chalk.red(`${email}`)} ${chalk.gray('[invalid]')}`);
}
}
return;
return 0;
}
const inviteUserPrefix = 'Invite User'.padEnd(14);
const sentEmailPrefix = 'Sent Email'.padEnd(14);
const emails = [];
let hasError = false;
let email;
do {
@@ -150,12 +152,12 @@ export default async function invite(
output.spinner(inviteUserPrefix + email);
try {
// eslint-disable-next-line no-await-in-loop
const { name, username } = await teams.inviteUser({
teamId: currentTeam.id,
email,
});
const userInfo = name || username;
email = `${email}${userInfo ? ` (${userInfo})` : ''} ${elapsed()}`;
const { username } = await inviteUserToTeam(
client,
currentTeam.id,
email
);
email = `${email}${username ? ` (${username})` : ''} ${elapsed()}`;
emails.push(email);
output.log(`${chalk.cyan(chars.tick)} ${sentEmailPrefix}${email}`);
if (hasError) {
@@ -193,4 +195,6 @@ export default async function invite(
output.log(`${chalk.cyan(chars.tick)} ${inviteUserPrefix}${email}`);
}
}
return 0;
}

View File

@@ -1,22 +1,41 @@
import chars from '../../util/output/chars';
import table from '../../util/output/table';
import getUser from '../../util/get-user.ts';
import getUser from '../../util/get-user';
import getTeams from '../../util/teams/get-teams';
import getPrefixedFlags from '../../util/get-prefixed-flags';
import { getPkgName } from '../../util/pkg-name.ts';
import { getPkgName } from '../../util/pkg-name';
import getCommandFlags from '../../util/get-command-flags';
import cmd from '../../util/output/cmd.ts';
import cmd from '../../util/output/cmd';
import Client from '../../util/client';
import getArgs from '../../util/get-args';
export default async function list(client, argv, teams) {
export default async function list(client: Client): Promise<number> {
const { config, output } = client;
const { next } = argv;
const argv = getArgs(client.argv.slice(2), {
'--since': String,
'--until': String,
'--count': Number,
'--next': Number,
'-C': '--count',
'-N': '--next',
});
const next = argv['--next'];
const count = argv['--count'];
if (typeof next !== 'undefined' && !Number.isInteger(next)) {
output.error('Please provide a number for flag --next');
output.error('Please provide a number for flag `--next`');
return 1;
}
if (typeof count !== 'undefined' && !Number.isInteger(next)) {
output.error('Please provide a number for flag `--count`');
return 1;
}
output.spinner('Fetching teams');
const { teams: list, pagination } = await teams.ls({
const { teams, pagination } = await getTeams(client, {
next,
apiVersion: 2,
});
@@ -37,40 +56,31 @@ export default async function list(client, argv, teams) {
}
if (accountIsCurrent) {
currentTeam = {
slug: user.username || user.email,
};
currentTeam = user.uid;
}
const teamList = list.map(({ slug, name }) => ({
const teamList = teams.map(({ id, slug, name }) => ({
id,
name,
value: slug,
current: slug === currentTeam.slug ? chars.tick : '',
current: id === currentTeam ? chars.tick : '',
}));
teamList.unshift({
id: user.uid,
name: user.email,
value: user.username || user.email,
current: (accountIsCurrent && chars.tick) || '',
current: accountIsCurrent ? chars.tick : '',
});
// Let's bring the current team to the beginning of the list
// Bring the current Team to the beginning of the list
if (!accountIsCurrent) {
const index = teamList.findIndex(
choice => choice.value === currentTeam.slug
);
const index = teamList.findIndex(choice => choice.id === currentTeam);
const choice = teamList.splice(index, 1)[0];
teamList.unshift(choice);
}
// Printing
const count = teamList.length;
if (!count) {
// Maybe should not happen
output.error(`No teams found`);
return 1;
}
output.stopSpinner();
console.log(); // empty line
@@ -80,7 +90,7 @@ export default async function list(client, argv, teams) {
[1, 5]
);
if (pagination && pagination.count === 20) {
if (pagination?.count === 20) {
const prefixedArgs = getPrefixedFlags(argv);
const flags = getCommandFlags(prefixedArgs, ['_', '--next', '-N', '-d']);
const nextCmd = `${getPkgName()} teams ls${flags} --next ${
@@ -89,4 +99,6 @@ export default async function list(client, argv, teams) {
console.log(); // empty line
output.log(`To display the next page run ${cmd(nextCmd)}`);
}
return 0;
}

View File

@@ -5,7 +5,7 @@ import chalk from 'chalk';
import Client from '../../util/client';
import { emoji } from '../../util/emoji';
import getUser from '../../util/get-user';
import getTeams from '../../util/get-teams';
import getTeams from '../../util/teams/get-teams';
import listInput from '../../util/input/list';
import { Team, GlobalConfig } from '../../types';
import { writeToConfigFile } from '../../util/config/files';

View File

@@ -3,7 +3,6 @@ import logo from '../util/output/logo';
import getScope from '../util/get-scope';
import { getPkgName } from '../util/pkg-name';
import getArgs from '../util/get-args';
import handleError from '../util/handle-error';
import Client from '../util/client';
const help = () => {
@@ -32,16 +31,9 @@ const help = () => {
`);
};
export default async (client: Client) => {
export default async (client: Client): Promise<number> => {
const { output } = client;
let argv;
try {
argv = getArgs(client.argv.slice(2), {});
} catch (error) {
handleError(error);
return 1;
}
const argv = getArgs(client.argv.slice(2), {});
argv._ = argv._.slice(1);
if (argv['--help'] || argv._[0] === 'help') {
@@ -62,9 +54,13 @@ export default async (client: Client) => {
throw err;
}
if (process.stdout.isTTY) {
process.stdout.write('> ');
if (output.isTTY) {
output.log(contextName);
} else {
// If stdout is not a TTY, then only print the username
// to support piping the output to another file / exe
output.print(`${contextName}\n`, { w: process.stdout });
}
console.log(contextName);
return 0;
};

View File

@@ -20,10 +20,9 @@ import epipebomb from 'epipebomb';
import updateNotifier from 'update-notifier';
import { URL } from 'url';
import * as Sentry from '@sentry/node';
import { NowBuildError } from '@vercel/build-utils';
import hp from './util/humanize-path';
import commands from './commands/index.ts';
import pkg from './util/pkg.ts';
import commands from './commands';
import pkg from './util/pkg';
import createOutput from './util/output';
import cmd from './util/output/cmd';
import info from './util/output/info';
@@ -31,9 +30,9 @@ import error from './util/output/error';
import param from './util/output/param';
import highlight from './util/output/highlight';
import getArgs from './util/get-args';
import getUser from './util/get-user.ts';
import Client from './util/client.ts';
import NowTeams from './util/teams';
import getUser from './util/get-user';
import getTeams from './util/teams/get-teams';
import Client from './util/client';
import { handleError } from './util/error';
import reportError from './util/report-error';
import getConfig from './util/get-config';
@@ -44,13 +43,14 @@ import {
getDefaultAuthConfig,
} from './util/config/get-default';
import * as ERRORS from './util/errors-ts';
import { NowError } from './util/now-error';
import { APIError } from './util/errors-ts.ts';
import { SENTRY_DSN } from './util/constants.ts';
import { APIError } from './util/errors-ts';
import { SENTRY_DSN } from './util/constants';
import getUpdateCommand from './util/get-update-command';
import { metrics, shouldCollectMetrics } from './util/metrics.ts';
import { getCommandName, getTitleName } from './util/pkg-name.ts';
import doLoginPrompt from './util/login/prompt.ts';
import { metrics, shouldCollectMetrics } from './util/metrics';
import { getCommandName, getTitleName } from './util/pkg-name';
import doLoginPrompt from './util/login/prompt';
import { GlobalConfig } from './types';
import { VercelConfig } from '@vercel/client';
const isCanary = pkg.version.includes('canary');
@@ -77,8 +77,8 @@ Sentry.init({
environment: isCanary ? 'canary' : 'stable',
});
let client;
let debug = () => {};
let client: Client;
let debug: (s: string) => void = () => {};
let apiUrl = 'https://api.vercel.com';
const main = async () => {
@@ -108,26 +108,30 @@ const main = async () => {
debug = output.debug;
const localConfigPath = argv['--local-config'];
const localConfig = await getConfig(output, localConfigPath);
if (localConfigPath && localConfig instanceof ERRORS.CantFindConfig) {
output.error(
`Couldn't find a project configuration file at \n ${localConfig.meta.paths.join(
' or\n '
)}`
);
return 1;
}
let localConfig: VercelConfig | Error | undefined = await getConfig(
output,
localConfigPath
);
if (localConfig instanceof ERRORS.CantParseJSONFile) {
output.error(`Couldn't parse JSON file ${localConfig.meta.file}.`);
return 1;
}
if (
(localConfig instanceof NowError || localConfig instanceof NowBuildError) &&
!(localConfig instanceof ERRORS.CantFindConfig)
) {
if (localConfig instanceof ERRORS.CantFindConfig) {
if (localConfigPath) {
output.error(
`Couldn't find a project configuration file at \n ${localConfig.meta.paths.join(
' or\n '
)}`
);
return 1;
} else {
localConfig = undefined;
}
}
if (localConfig instanceof Error) {
output.prettyError(localConfig);
return 1;
}
@@ -207,7 +211,7 @@ const main = async () => {
return 0;
}
let config;
let config: GlobalConfig | null = null;
if (configExists) {
try {
@@ -229,8 +233,11 @@ const main = async () => {
// multiple providers. In that case, we really
// need to migrate.
if (
// @ts-ignore
config.sh ||
// @ts-ignore
config.user ||
// @ts-ignore
typeof config.user === 'object' ||
typeof config.currentTeam === 'object'
) {
@@ -300,6 +307,7 @@ const main = async () => {
// This is from when Vercel CLI supported
// multiple providers. In that case, we really
// need to migrate.
// @ts-ignore
if (authConfig.credentials) {
authConfigExists = false;
}
@@ -346,6 +354,11 @@ const main = async () => {
return 1;
}
if (!config) {
output.error(`Vercel global config was not loaded.`);
return 1;
}
// Shared API `Client` instance for all sub-commands to utilize
client = new Client({
apiUrl,
@@ -397,7 +410,7 @@ const main = async () => {
}
if (subcommandExists) {
debug('user supplied known subcommand', targetOrSubcommand);
debug(`user supplied known subcommand: "${targetOrSubcommand}"`);
subcommand = targetOrSubcommand;
} else {
debug('user supplied a possible target for deployment');
@@ -457,14 +470,12 @@ const main = async () => {
}
if (typeof argv['--token'] === 'string' && subcommand === 'switch') {
console.error(
error({
message: `This command doesn't work with ${param(
'--token'
)}. Please use ${param('--scope')}.`,
slug: 'no-token-allowed',
})
);
output.prettyError({
message: `This command doesn't work with ${param(
'--token'
)}. Please use ${param('--scope')}.`,
link: 'https://err.sh/vercel/no-token-allowed',
});
return 1;
}
@@ -473,12 +484,10 @@ const main = async () => {
const token = argv['--token'];
if (token.length === 0) {
console.error(
error({
message: `You defined ${param('--token')}, but it's missing a value`,
slug: 'missing-token-value',
})
);
output.prettyError({
message: `You defined ${param('--token')}, but it's missing a value`,
link: 'https://err.sh/vercel/missing-token-value',
});
return 1;
}
@@ -486,16 +495,14 @@ const main = async () => {
const invalid = token.match(/(\W)/g);
if (invalid) {
const notContain = Array.from(new Set(invalid)).sort();
console.error(
error({
message: `You defined ${param(
'--token'
)}, but its contents are invalid. Must not contain: ${notContain
.map(c => JSON.stringify(c))
.join(', ')}`,
slug: 'invalid-token-value',
})
);
output.prettyError({
message: `You defined ${param(
'--token'
)}, but its contents are invalid. Must not contain: ${notContain
.map(c => JSON.stringify(c))
.join(', ')}`,
link: 'https://err.sh/vercel/invalid-token-value',
});
return 1;
}
@@ -516,13 +523,8 @@ const main = async () => {
);
}
const {
authConfig: { token },
} = client;
let scope = argv['--scope'] || argv['--team'] || localConfig.scope;
const targetCommand = commands.get(subcommand);
const scope = argv['--scope'] || argv['--team'] || localConfig?.scope;
if (
typeof scope === 'string' &&
@@ -536,12 +538,10 @@ const main = async () => {
user = await getUser(client);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED') {
console.error(
error({
message: `You do not have access to the specified account`,
slug: 'scope-not-accessible',
})
);
output.prettyError({
message: `You do not have access to the specified account`,
link: 'https://err.sh/vercel/scope-not-accessible',
});
return 1;
}
@@ -553,19 +553,16 @@ const main = async () => {
if (user.uid === scope || user.email === scope || user.username === scope) {
delete client.config.currentTeam;
} else {
let list = [];
let teams = [];
try {
const teams = new NowTeams({ apiUrl, token, debug: isDebugging });
list = (await teams.ls()).teams;
teams = await getTeams(client);
} catch (err) {
if (err.code === 'not_authorized') {
console.error(
error({
message: `You do not have access to the specified team`,
slug: 'scope-not-accessible',
})
);
output.prettyError({
message: `You do not have access to the specified team`,
link: 'https://err.sh/vercel/scope-not-accessible',
});
return 1;
}
@@ -575,15 +572,13 @@ const main = async () => {
}
const related =
list && list.find(item => item.id === scope || item.slug === scope);
teams && teams.find(team => team.id === scope || team.slug === scope);
if (!related) {
console.error(
error({
message: 'The specified scope does not exist',
slug: 'scope-not-existent',
})
);
output.prettyError({
message: 'The specified scope does not exist',
link: 'https://err.sh/vercel/scope-not-existent',
});
return 1;
}
@@ -592,20 +587,93 @@ const main = async () => {
}
}
if (!targetCommand) {
const sub = param(subcommand);
console.error(error(`The ${sub} subcommand does not exist`));
return 1;
}
const metric = metrics();
let exitCode;
const eventCategory = 'Exit Code';
try {
const start = Date.now();
const full = require(`./commands/${targetCommand}`).default;
exitCode = await full(client);
let func: any;
switch (targetCommand) {
case 'alias':
func = await import('./commands/alias');
break;
case 'billing':
func = await import('./commands/billing');
break;
case 'certs':
func = await import('./commands/certs');
break;
case 'deploy':
func = await import('./commands/deploy');
break;
case 'dev':
func = await import('./commands/dev');
break;
case 'dns':
func = await import('./commands/dns');
break;
case 'domains':
func = await import('./commands/domains');
break;
case 'env':
func = await import('./commands/env');
break;
case 'init':
func = await import('./commands/init');
break;
case 'inspect':
func = await import('./commands/inspect');
break;
case 'link':
func = await import('./commands/link');
break;
case 'list':
func = await import('./commands/list');
break;
case 'logs':
func = await import('./commands/logs');
break;
case 'login':
func = await import('./commands/login');
break;
case 'logout':
func = await import('./commands/logout');
break;
case 'projects':
func = await import('./commands/projects');
break;
case 'remove':
func = await import('./commands/remove');
break;
case 'secrets':
func = await import('./commands/secrets');
break;
case 'teams':
func = await import('./commands/teams');
break;
case 'update':
func = await import('./commands/update');
break;
case 'whoami':
func = await import('./commands/whoami');
break;
default:
func = null;
break;
}
if (!func || !targetCommand) {
const sub = param(subcommand);
output.error(`The ${sub} subcommand does not exist`);
return 1;
}
if (func.default) {
func = func.default;
}
exitCode = await func(client);
const end = Date.now() - start;
if (shouldCollectMetrics) {
@@ -678,7 +746,7 @@ const main = async () => {
return exitCode;
};
const handleRejection = async err => {
const handleRejection = async (err: any) => {
debug('handling rejection');
if (err) {
@@ -695,7 +763,7 @@ const handleRejection = async err => {
process.exit(1);
};
const handleUnexpected = async err => {
const handleUnexpected = async (err: Error) => {
const { message } = err;
// We do not want to render errors about Sentry not being reachable
@@ -704,9 +772,8 @@ const handleUnexpected = async err => {
return;
}
await reportError(Sentry, client, err);
console.error(error(`An unexpected error occurred!\n${err.stack}`));
await reportError(Sentry, client, err);
process.exit(1);
};
@@ -717,6 +784,7 @@ process.on('uncaughtException', handleUnexpected);
main()
.then(exitCode => {
process.exitCode = exitCode;
// @ts-ignore - "nowExit" is a non-standard event name
process.emit('nowExit');
})
.catch(handleUnexpected);

View File

@@ -16,13 +16,13 @@ export interface JSONObject {
}
export interface AuthConfig {
_: string;
_?: string;
token?: string;
skipWrite?: boolean;
}
export interface GlobalConfig {
_: string;
_?: string;
currentTeam?: string;
includeScheme?: string;
collectMetrics?: boolean;

View File

@@ -35,7 +35,7 @@ export async function getDeploymentForAlias(
localConfigPath: string | undefined,
user: User,
contextName: string,
localConfig: VercelConfig
localConfig?: VercelConfig
) {
output.spinner(`Fetching deployment to alias in ${chalk.bold(contextName)}`);
@@ -52,7 +52,7 @@ export async function getDeploymentForAlias(
}
const appName =
(localConfig && localConfig.name) ||
localConfig?.name ||
path.basename(path.resolve(process.cwd(), localConfigPath || ''));
if (!appName) {

View File

@@ -34,10 +34,10 @@ export interface ClientOptions {
authConfig: AuthConfig;
output: Output;
config: GlobalConfig;
localConfig: VercelConfig;
localConfig?: VercelConfig;
}
const isJSONObject = (v: any): v is JSONObject => {
export const isJSONObject = (v: any): v is JSONObject => {
return v && typeof v == 'object' && v.constructor === Object;
};
@@ -47,7 +47,7 @@ export default class Client extends EventEmitter {
authConfig: AuthConfig;
output: Output;
config: GlobalConfig;
localConfig: VercelConfig;
localConfig?: VercelConfig;
private requestIdCounter: number;
constructor(opts: ClientOptions) {
@@ -69,7 +69,7 @@ export default class Client extends EventEmitter {
});
}
_fetch(_url: string, opts: FetchOptions = {}) {
private _fetch(_url: string, opts: FetchOptions = {}) {
const parsedUrl = parseUrl(_url, true);
const apiUrl = parsedUrl.host
? `${parsedUrl.protocol}//${parsedUrl.host}`

View File

@@ -100,8 +100,8 @@ export function getAuthConfigFilePath() {
export function readLocalConfig(
prefix: string = process.cwd()
): VercelConfig | null {
let config: VercelConfig | null = null;
): VercelConfig | undefined {
let config: VercelConfig | undefined = undefined;
let target = '';
try {
@@ -116,7 +116,7 @@ export function readLocalConfig(
}
if (!target) {
return null;
return;
}
try {
@@ -134,7 +134,7 @@ export function readLocalConfig(
}
if (!config) {
return null;
return;
}
config[fileNameSymbol] = basename(target);

View File

@@ -1,6 +1,6 @@
import { AuthConfig, GlobalConfig } from '../../types';
export const getDefaultConfig = async (existingCopy: GlobalConfig) => {
export const getDefaultConfig = async (existingCopy?: GlobalConfig | null) => {
let migrated = false;
const config: GlobalConfig = {
@@ -51,7 +51,7 @@ export const getDefaultConfig = async (existingCopy: GlobalConfig) => {
return { config, migrated };
};
export const getDefaultAuthConfig = async (existing?: AuthConfig) => {
export const getDefaultAuthConfig = async (existing?: AuthConfig | null) => {
let migrated = false;
const config: AuthConfig = {

View File

@@ -4,7 +4,7 @@ import * as ERRORS from '../errors';
import { NowError } from '../now-error';
import mapCertError from '../certs/map-cert-error';
import { Org } from '../../types';
import Now from '..';
import Now, { CreateOptions } from '..';
import Client from '../client';
import { DeploymentError } from '../../../../client/dist';
@@ -13,8 +13,8 @@ export default async function createDeploy(
now: Now,
contextName: string,
paths: string[],
createArgs: any,
org: Org | null,
createArgs: CreateOptions,
org: Org,
isSettingUpProject: boolean,
cwd?: string
): Promise<any | DeploymentError> {

View File

@@ -69,10 +69,15 @@ export default async function processDeployment({
const { env = {} } = requestBody;
const token = now._token;
if (!token) {
throw new Error('Missing authentication token');
}
const clientOptions: VercelClientOptions = {
teamId: org.type === 'team' ? org.id : undefined,
apiUrl: now._apiUrl,
token: now._token,
token,
debug: now._debug,
userAgent: ua,
path: paths[0],
@@ -149,7 +154,6 @@ export default async function processDeployment({
org.slug
);
// @ts-ignore
now.url = event.payload.url;
output.stopSpinner();

View File

@@ -19,7 +19,7 @@ import { BuilderWithPackage } from './types';
type CliPackageJson = typeof cliPkg;
declare const __non_webpack_require__: typeof require;
const require_: typeof require = eval('require');
const registryTypes = new Set(['version', 'tag', 'range']);
@@ -104,7 +104,7 @@ export function filterPackage(
builderSpec: string,
distTag: string,
buildersPkg: PackageJson,
cliPkg: CliPackageJson
cliPkg: Partial<CliPackageJson>
) {
if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec);
@@ -355,8 +355,8 @@ export async function getBuilder(
try {
output.debug(`Requiring runtime: "${requirePath}"`);
const mod = require(requirePath);
const pkg = require(join(requirePath, 'package.json'));
const mod = require_(requirePath);
const pkg = require_(join(requirePath, 'package.json'));
builderWithPkg = {
requirePath,
builder: Object.freeze(mod),
@@ -432,18 +432,13 @@ function purgeRequireCache(
builderDir: string,
output: Output
) {
const _require =
typeof __non_webpack_require__ === 'function'
? __non_webpack_require__
: require;
// The `require()` cache for the builder's assets must be purged
const packagesPaths = packages.map(b => join(builderDir, 'node_modules', b));
for (const id of Object.keys(_require.cache)) {
for (const id of Object.keys(require_.cache)) {
for (const path of packagesPaths) {
if (id.startsWith(path)) {
output.debug(`Purging require cache for "${id}"`);
delete _require.cache[id];
delete require_.cache[id];
}
}
}

View File

@@ -109,7 +109,8 @@ export async function executeBuild(
builderWithPkg: { runInProcess, requirePath, builder, package: pkg },
} = match;
const { entrypoint } = match;
const { debug, envConfigs, cwd: workPath, devCacheDir } = devServer;
const { envConfigs, cwd: workPath, devCacheDir } = devServer;
const debug = devServer.output.isDebugEnabled();
const startTime = Date.now();
const showBuildTimestamp =

View File

@@ -117,7 +117,6 @@ function sortBuilders(buildA: Builder, buildB: Builder) {
export default class DevServer {
public cwd: string;
public debug: boolean;
public output: Output;
public proxy: httpProxy;
public envConfigs: EnvConfigs;
@@ -157,7 +156,6 @@ export default class DevServer {
constructor(cwd: string, options: DevServerOptions) {
this.cwd = cwd;
this.debug = options.debug;
this.output = options.output;
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
this.systemEnvValues = options.systemEnvValues || [];

View File

@@ -23,7 +23,6 @@ export { VercelConfig };
export interface DevServerOptions {
output: Output;
debug: boolean;
devCommand?: string;
frameworkSlug?: string;
projectSettings?: ProjectSettings;

View File

@@ -1,14 +1,22 @@
import { Response } from 'node-fetch';
import errorOutput from './output/error';
export { default as handleError } from './handle-error';
export const error = errorOutput;
export interface ResponseError extends Error {
status: number;
serverMessage: string;
retryAfter?: number;
[key: string]: any;
}
export async function responseError(
res,
fallbackMessage = null,
res: Response,
fallbackMessage: string | null = null,
parsedBody = {}
) {
let message;
let message = '';
let bodyError;
if (res.status >= 400 && res.status < 500) {
@@ -25,11 +33,11 @@ export async function responseError(
message = bodyError.message;
}
if (message == null) {
if (!message) {
message = fallbackMessage === null ? 'Response Error' : fallbackMessage;
}
const err = new Error(`${message} (${res.status})`);
const err = new Error(`${message} (${res.status})`) as ResponseError;
err.status = res.status;
err.serverMessage = message;
@@ -54,7 +62,10 @@ export async function responseError(
return err;
}
export async function responseErrorMessage(res, fallbackMessage = null) {
export async function responseErrorMessage(
res: Response,
fallbackMessage: string | null = null
) {
let message;
if (res.status >= 400 && res.status < 500) {

View File

@@ -15,6 +15,7 @@ export class APIError extends Error {
status: number;
serverMessage: string;
link?: string;
slug?: string;
action?: string;
retryAfter: number | null | 'never';
[key: string]: any;

View File

@@ -1,18 +1,17 @@
import arg from 'arg';
import { basename } from 'path';
import { VercelConfig } from '@vercel/client';
export interface GetProjectNameOptions {
argv: arg.Result<{ '--name': StringConstructor }>;
argv: { '--name'?: string };
nowConfig?: VercelConfig;
isFile: boolean;
paths: string[];
isFile?: boolean;
paths?: string[];
}
export default function getProjectName({
argv,
nowConfig = {},
isFile,
isFile = false,
paths = [],
}: GetProjectNameOptions) {
const nameCli = argv['--name'];

View File

@@ -1,6 +1,6 @@
import Client from './client';
import getUser from './get-user';
import getTeamById from './get-team-by-id';
import getTeamById from './teams/get-team-by-id';
import { TeamDeleted } from './errors-ts';
import { Team } from '../types';

View File

@@ -1,22 +0,0 @@
import Client from './client';
import { Team } from '../types';
import { APIError, InvalidToken } from './errors-ts';
let teams: Team[] | undefined;
export default async function getTeams(client: Client): Promise<Team[]> {
if (teams) return teams;
try {
const body = await client.fetch<{ teams: Team[] }>('/v1/teams', {
useCurrentTeam: false,
});
teams = body.teams || [];
return teams;
} catch (error) {
if (error instanceof APIError && error.status === 403) {
throw new InvalidToken();
}
throw error;
}
}

View File

@@ -2,11 +2,7 @@ import Client from './client';
import { User } from '../types';
import { APIError, InvalidToken, MissingUser } from './errors-ts';
let user: User | undefined;
export default async function getUser(client: Client) {
if (user) return user;
try {
const res = await client.fetch<{ user: User }>('/www/user', {
useCurrentTeam: false,
@@ -16,8 +12,7 @@ export default async function getUser(client: Client) {
throw new MissingUser();
}
user = res.user;
return user;
return res.user;
} catch (error) {
if (error instanceof APIError && error.status === 403) {
throw new InvalidToken();

View File

@@ -3,20 +3,79 @@ import qs from 'querystring';
import { parse as parseUrl } from 'url';
import retry from 'async-retry';
import ms from 'ms';
import fetch from 'node-fetch';
import fetch, { Headers } from 'node-fetch';
import { URLSearchParams } from 'url';
import bytes from 'bytes';
import chalk from 'chalk';
import ua from './ua.ts';
import processDeployment from './deploy/process-deployment.ts';
import ua from './ua';
import processDeployment from './deploy/process-deployment';
import highlight from './output/highlight';
import createOutput from './output';
import createOutput, { Output } from './output';
import { responseError } from './error';
import stamp from './output/stamp';
import { BuildError } from './errors-ts';
import printIndications from './print-indications.ts';
import { APIError, BuildError } from './errors-ts';
import printIndications from './print-indications';
import { Org } from '../types';
import { VercelConfig } from './dev/types';
import { FetchOptions, isJSONObject } from './client';
import { Dictionary } from '@vercel/client';
export interface NowOptions {
apiUrl: string;
token?: string;
url?: string | null;
currentTeam?: string | null;
output: Output;
forceNew?: boolean;
withCache?: boolean;
debug?: boolean;
}
export interface CreateOptions {
// Legacy
nowConfig?: VercelConfig;
isFile?: boolean;
// Latest
name: string;
project?: string;
wantsPublic: boolean;
meta: Dictionary<string>;
regions?: string[];
quiet?: boolean;
env: Dictionary<string>;
build: { env: Dictionary<string> };
forceNew?: boolean;
withCache?: boolean;
target?: string | null;
deployStamp: () => string;
projectSettings?: any;
skipAutoDetectionConfirmation?: boolean;
}
export interface RemoveOptions {
hard?: boolean;
}
export interface ListOptions {
version?: number;
meta?: Dictionary<string>;
nextTimestamp?: number;
}
export default class Now extends EventEmitter {
url: string | null;
currentTeam: string | null;
_apiUrl: string;
_token?: string;
_debug: boolean;
_forceNew: boolean;
_withCache: boolean;
_output: Output;
_syncAmount?: number;
_files?: any[];
_missing?: string[];
constructor({
apiUrl,
token,
@@ -26,7 +85,7 @@ export default class Now extends EventEmitter {
withCache = false,
debug = false,
output = createOutput({ debug }),
}) {
}: NowOptions) {
super();
this.url = url;
@@ -41,10 +100,10 @@ export default class Now extends EventEmitter {
}
async create(
paths,
paths: string[],
{
// Legacy
nowConfig = {},
nowConfig: nowConfig = {},
// Latest
name,
@@ -61,12 +120,12 @@ export default class Now extends EventEmitter {
deployStamp,
projectSettings,
skipAutoDetectionConfirmation,
},
org,
isSettingUpProject,
cwd
}: CreateOptions,
org: Org,
isSettingUpProject: boolean,
cwd?: string
) {
let hashes = {};
let hashes: any = {};
const uploadStamp = stamp();
let requestBody = {
@@ -109,7 +168,7 @@ export default class Now extends EventEmitter {
let sizeExceeded = 0;
const { log, warn } = this._output;
deployment.warnings.forEach(warning => {
deployment.warnings.forEach((warning: any) => {
if (warning.reason === 'size_limit_exceeded') {
const { sha, limit } = warning;
const n = hashes[sha].names.pop();
@@ -135,14 +194,14 @@ export default class Now extends EventEmitter {
return deployment;
}
async handleDeploymentError(error, { env }) {
async handleDeploymentError(error: any, { env }: any) {
if (error.status === 429) {
if (error.code === 'builds_rate_limited') {
const err = new Error(error.message);
const err = Object.create(APIError.prototype);
err.message = error.message;
err.status = error.status;
err.retryAfter = 'never';
err.code = error.code;
return err;
}
@@ -157,8 +216,8 @@ export default class Now extends EventEmitter {
msg += 'Please slow down.';
}
const err = new Error(msg);
const err = Object.create(APIError.prototype);
err.message = msg;
err.status = error.status;
err.retryAfter = 'never';
@@ -172,7 +231,6 @@ export default class Now extends EventEmitter {
if (error.status === 400 && error.code === 'missing_files') {
this._missing = error.missing || [];
return error;
}
@@ -199,7 +257,7 @@ export default class Now extends EventEmitter {
'.vercelignore'
)}):` +
`\n- ${unreferencedBuildSpecs
.map(item => JSON.stringify(item))
.map((item: any) => JSON.stringify(item))
.join('\n- ')}`;
} else {
Object.assign(err, error);
@@ -211,6 +269,7 @@ export default class Now extends EventEmitter {
// Handle build errors
if (error.id && error.id.startsWith('bld_')) {
return new BuildError({
message: 'Build failed',
meta: {
entrypoint: error.entrypoint,
},
@@ -230,7 +289,7 @@ export default class Now extends EventEmitter {
return new Error(error.message);
}
async listSecrets(next, testWarningFlag) {
async listSecrets(next?: number, testWarningFlag?: boolean) {
const payload = await this.retry(async bail => {
let secretsUrl = '/v3/now/secrets?limit=20';
@@ -259,8 +318,11 @@ export default class Now extends EventEmitter {
return payload;
}
async list(app, { version = 4, meta = {}, nextTimestamp } = {}) {
const fetchRetry = async (url, options = {}) => {
async list(
app?: string,
{ version = 4, meta = {}, nextTimestamp }: ListOptions = {}
) {
const fetchRetry = async (url: string, options: FetchOptions = {}) => {
return this.retry(
async bail => {
const res = await this._fetch(url, options);
@@ -296,8 +358,8 @@ export default class Now extends EventEmitter {
);
const deployments = await Promise.all(
projects.map(async ({ id: projectId }) => {
const query = new URLSearchParams({ limit: 1, projectId });
projects.map(async ({ id: projectId }: any) => {
const query = new URLSearchParams({ limit: '1', projectId });
const { deployments } = await fetchRetry(
`/v${version}/now/deployments?${query}`
);
@@ -326,7 +388,7 @@ export default class Now extends EventEmitter {
return response;
}
async findDeployment(hostOrId) {
async findDeployment(hostOrId: string) {
const { debug } = this._output;
let id = hostOrId && !hostOrId.includes('.');
@@ -390,7 +452,7 @@ export default class Now extends EventEmitter {
);
}
async remove(deploymentId, { hard }) {
async remove(deploymentId: string, { hard = false }: RemoveOptions) {
const url = `/now/deployments/${deploymentId}?hard=${hard ? 1 : 0}`;
await this.retry(async bail => {
@@ -412,56 +474,51 @@ export default class Now extends EventEmitter {
return true;
}
retry(fn, { retries = 3, maxTimeout = Infinity } = {}) {
return retry(fn, {
retry<T>(
fn: retry.RetryFunction<T>,
{ retries = 3, maxTimeout = Infinity }: retry.Options = {}
) {
return retry<T>(fn, {
retries,
maxTimeout,
onRetry: this._onRetry,
});
}
_onRetry(err) {
_onRetry(err: Error) {
this._output.debug(`Retrying: ${err}\n${err.stack}`);
}
close() {}
get syncAmount() {
if (!this._syncAmount) {
this._syncAmount = this._missing
.map(sha => this._files.get(sha).data.length)
.reduce((a, b) => a + b, 0);
}
return this._syncAmount;
}
async _fetch(_url, opts = {}) {
async _fetch(_url: string, opts: FetchOptions = {}) {
if (opts.useCurrentTeam !== false && this.currentTeam) {
const parsedUrl = parseUrl(_url, true);
const query = parsedUrl.query;
query.teamId = this.currentTeam;
_url = `${parsedUrl.pathname}?${qs.encode(query)}`;
_url = `${parsedUrl.pathname}?${qs.stringify(query)}`;
delete opts.useCurrentTeam;
}
opts.headers = opts.headers || {};
opts.headers.accept = 'application/json';
opts.headers.authorization = `Bearer ${this._token}`;
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; charset=utf-8';
opts.headers = new Headers(opts.headers);
opts.headers.set('accept', 'application/json');
if (this._token) {
opts.headers.set('authorization', `Bearer ${this._token}`);
}
opts.headers.set('user-agent', ua);
let body;
if (isJSONObject(opts.body)) {
body = JSON.stringify(opts.body);
opts.headers.set('content-type', 'application/json; charset=utf8');
} else {
body = opts.body;
}
const res = await this._output.time(
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
fetch(`${this._apiUrl}${_url}`, opts)
fetch(`${this._apiUrl}${_url}`, { ...opts, body })
);
printIndications(res);
return res;
@@ -475,7 +532,7 @@ export default class Now extends EventEmitter {
// which automatically returns the json response body
// if the response is ok and content-type json
// it does the same for JSON` body` in opts
async fetch(url, opts = {}) {
async fetch(url: string, opts: FetchOptions = {}) {
return this.retry(async bail => {
if (opts.json !== false && opts.body && typeof opts.body === 'object') {
opts = Object.assign({}, opts, {
@@ -495,7 +552,7 @@ export default class Now extends EventEmitter {
return null;
}
return res.headers.get('content-type').includes('application/json')
return res.headers.get('content-type')?.includes('application/json')
? res.json()
: res;
}

View File

@@ -1,10 +1,11 @@
import inquirer from 'inquirer';
import chalk from 'chalk';
import inquirer from 'inquirer';
import Prompt from 'inquirer/lib/prompts/base';
// Here we patch inquirer to use a `>` instead of the ugly green `?`
/* eslint-disable no-multiple-empty-lines, no-var, no-undef, no-eq-null, eqeqeq, semi */
const getQuestion = function() {
const getQuestion = function (this: Prompt) {
var message = `${chalk.bold(`> ${this.opt.message}`)} `;
// Append the default if available, and if question isn't answered

View File

@@ -1,5 +1,8 @@
import inquirer from 'inquirer';
import chalk from 'chalk';
import inquirer from 'inquirer';
import Prompt from 'inquirer/lib/prompts/base';
import Choice from 'inquirer/lib/objects/choice';
import Separator from 'inquirer/lib/objects/separator';
/**
* Here we patch inquirer with some tweaks:
@@ -10,7 +13,7 @@ import chalk from 'chalk';
*/
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/base.js#L126
const getQuestion = function () {
const getQuestion = function (this: Prompt) {
let message = `${chalk.gray('?')} ${this.opt.message} `;
if (this.opt.type === 'confirm') {
@@ -57,7 +60,7 @@ inquirer.prompt.prompts.list.prototype.render = function () {
this.screen.render(message);
};
function listRender(choices, pointer) {
function listRender(choices: (Choice | Separator)[], pointer: number) {
let output = '';
let separatorOffset = 0;
@@ -89,7 +92,7 @@ function listRender(choices, pointer) {
}
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/checkbox.js#L84
inquirer.prompt.prompts.checkbox.prototype.render = function (error) {
inquirer.prompt.prompts.checkbox.prototype.render = function (error?: string) {
// Render question
let message = this.getQuestion();
let bottomContent = '';
@@ -125,7 +128,7 @@ inquirer.prompt.prompts.checkbox.prototype.render = function (error) {
this.screen.render(message, bottomContent);
};
function renderChoices(choices, pointer) {
function renderChoices(choices: (Choice | Separator)[], pointer: number) {
let output = '';
let separatorOffset = 0;
@@ -162,7 +165,7 @@ function renderChoices(choices, pointer) {
}
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/input.js#L44
inquirer.prompt.prompts.input.prototype.render = function (error) {
inquirer.prompt.prompts.input.prototype.render = function (error?: string) {
let bottomContent = '';
let appendContent = '';
let message = this.getQuestion();
@@ -189,7 +192,7 @@ inquirer.prompt.prompts.input.prototype.render = function (error) {
};
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/confirm.js#L64
inquirer.prompt.prompts.confirm.prototype.render = function (answer) {
inquirer.prompt.prompts.confirm.prototype.render = function (answer?: boolean) {
let message = this.getQuestion();
if (this.status === 'answered') {

View File

@@ -1,7 +1,7 @@
import Client from '../client';
import inquirer from 'inquirer';
import Client from '../client';
import getUser from '../get-user';
import getTeams from '../get-teams';
import getTeams from '../teams/get-teams';
import { User, Team, Org } from '../../types';
type Choice = { name: string; value: Org };

View File

@@ -2,7 +2,6 @@ import { join, basename } from 'path';
import chalk from 'chalk';
import { remove } from 'fs-extra';
import { ProjectLinkResult, ProjectSettings } from '../../types';
import { VercelConfig } from '../dev/types';
import {
getLinkedProject,
linkFolderToProject,
@@ -23,7 +22,7 @@ import editProjectSettings from '../input/edit-project-settings';
import stamp from '../output/stamp';
import { EmojiLabel } from '../emoji';
import createDeploy from '../deploy/create-deploy';
import Now from '../index';
import Now, { CreateOptions } from '../index';
export interface SetupAndLinkOptions {
forceDelete?: boolean;
@@ -46,6 +45,7 @@ export default async function setupAndLink(
): Promise<ProjectLinkResult> {
const {
authConfig: { token },
localConfig,
apiUrl,
output,
config,
@@ -144,13 +144,9 @@ export default async function setupAndLink(
return { status: 'error', exitCode: 1 };
}
let localConfig: VercelConfig = {};
if (client.localConfig && !(client.localConfig instanceof Error)) {
localConfig = client.localConfig;
}
config.currentTeam = org.type === 'team' ? org.id : undefined;
const isZeroConfig = !localConfig.builds || localConfig.builds.length === 0;
const isZeroConfig =
!localConfig || !localConfig.builds || localConfig.builds.length === 0;
try {
let settings: ProjectSettings = {};
@@ -163,16 +159,15 @@ export default async function setupAndLink(
output,
currentTeam: config.currentTeam,
});
const createArgs: any = {
const createArgs: CreateOptions = {
name: newProjectName,
env: {},
build: { env: {} },
forceNew: undefined,
withCache: undefined,
quiet,
wantsPublic: localConfig.public,
wantsPublic: localConfig?.public || false,
isFile,
type: null,
nowConfig: localConfig,
regions: undefined,
meta: {},
@@ -181,7 +176,7 @@ export default async function setupAndLink(
skipAutoDetectionConfirmation: false,
};
if (!localConfig.builds || localConfig.builds.length === 0) {
if (isZeroConfig) {
// Only add projectSettings for zero config deployments
createArgs.projectSettings = { sourceFilesOutsideRootDirectory };
}

View File

@@ -1,8 +1,6 @@
const chars = {
// in some setups now.exe crashes if we use
// the normal tick unicode character :|
tick: process.platform === 'win32' ? '√' : '✔',
cross: process.platform === 'win32' ? '☓' : '✘'
};
cross: process.platform === 'win32' ? '☓' : '✘',
} as const;
export default chars;

View File

@@ -2,45 +2,54 @@ import chalk from 'chalk';
import boxen from 'boxen';
import renderLink from './link';
import wait, { StopSpinner } from './wait';
export type Output = ReturnType<typeof _createOutput>;
import { Writable } from 'stream';
export interface OutputOptions {
debug?: boolean;
}
// Singleton
let instance: Output | null = null;
export default function createOutput(opts?: OutputOptions) {
if (!instance) {
instance = _createOutput(opts);
}
return instance;
export interface PrintOptions {
w?: Writable;
}
function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
let spinnerMessage = '';
let spinner: StopSpinner | null = null;
export interface LogOptions extends PrintOptions {
color?: typeof chalk;
}
function isDebugEnabled() {
return debugEnabled;
export class Output {
private debugEnabled: boolean;
private spinnerMessage: string;
private _spinner: StopSpinner | null;
constructor({ debug: debugEnabled = false }: OutputOptions = {}) {
this.debugEnabled = debugEnabled;
this.spinnerMessage = '';
this._spinner = null;
}
function print(str: string) {
stopSpinner();
process.stderr.write(str);
get isTTY() {
return process.stdout.isTTY;
}
function log(str: string, color = chalk.grey) {
print(`${color('>')} ${str}\n`);
}
isDebugEnabled = () => {
return this.debugEnabled;
};
function dim(str: string, color = chalk.grey) {
print(`${color(`> ${str}`)}\n`);
}
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
this.stopSpinner();
const stream: Writable = w || process.stderr;
stream.write(str);
};
function warn(
log = (str: string, color = chalk.grey) => {
this.print(`${color('>')} ${str}\n`);
};
dim = (str: string, color = chalk.grey) => {
this.print(`${color(`> ${str}`)}\n`);
};
warn = (
str: string,
slug: string | null = null,
link: string | null = null,
@@ -48,10 +57,10 @@ function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
options?: {
boxen?: boxen.Options;
}
) {
) => {
const details = slug ? `https://err.sh/vercel/${slug}` : link;
print(
this.print(
boxen(
chalk.bold.yellow('WARN! ') +
str +
@@ -68,110 +77,99 @@ function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
}
)
);
print('\n');
}
this.print('\n');
};
function note(str: string) {
log(chalk`{yellow.bold NOTE:} ${str}`);
}
note = (str: string) => {
this.log(chalk`{yellow.bold NOTE:} ${str}`);
};
function error(
error = (
str: string,
slug?: string,
link?: string,
action = 'Learn More'
) {
print(`${chalk.red(`Error!`)} ${str}\n`);
) => {
this.print(`${chalk.red(`Error!`)} ${str}\n`);
const details = slug ? `https://err.sh/vercel/${slug}` : link;
if (details) {
print(`${chalk.bold(action)}: ${renderLink(details)}\n`);
this.print(`${chalk.bold(action)}: ${renderLink(details)}\n`);
}
}
};
function prettyError(err: Error & { link?: string; action?: string }) {
return error(err.message, undefined, err.link, err.action);
}
prettyError = (
err: Pick<Error, 'message'> & { link?: string; action?: string }
) => {
return this.error(err.message, undefined, err.link, err.action);
};
function ready(str: string) {
print(`${chalk.cyan('> Ready!')} ${str}\n`);
}
ready = (str: string) => {
this.print(`${chalk.cyan('> Ready!')} ${str}\n`);
};
function success(str: string) {
print(`${chalk.cyan('> Success!')} ${str}\n`);
}
success = (str: string) => {
this.print(`${chalk.cyan('> Success!')} ${str}\n`);
};
function debug(str: string) {
if (debugEnabled) {
log(
debug = (str: string) => {
if (this.debugEnabled) {
this.log(
`${chalk.bold('[debug]')} ${chalk.gray(
`[${new Date().toISOString()}]`
)} ${str}`
);
}
}
};
function setSpinner(message: string, delay: number = 300): void {
spinnerMessage = message;
if (debugEnabled) {
debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
spinner = (message: string, delay: number = 300): void => {
this.spinnerMessage = message;
if (this.debugEnabled) {
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
return;
}
if (spinner) {
spinner.text = message;
if (this._spinner) {
this._spinner.text = message;
} else {
spinner = wait(message, delay);
this._spinner = wait(message, delay);
}
}
};
function stopSpinner() {
if (debugEnabled && spinnerMessage) {
const msg = `Spinner stopped (${spinnerMessage})`;
spinnerMessage = '';
debug(msg);
stopSpinner = () => {
if (this.debugEnabled && this.spinnerMessage) {
const msg = `Spinner stopped (${this.spinnerMessage})`;
this.spinnerMessage = '';
this.debug(msg);
}
if (spinner) {
spinner();
spinner = null;
spinnerMessage = '';
if (this._spinner) {
this._spinner();
this._spinner = null;
this.spinnerMessage = '';
}
}
};
async function time<T>(
time = async <T>(
label: string | ((r?: T) => string),
fn: Promise<T> | (() => Promise<T>)
) {
) => {
const promise = typeof fn === 'function' ? fn() : fn;
if (debugEnabled) {
if (this.debugEnabled) {
const startLabel = typeof label === 'function' ? label() : label;
debug(startLabel);
this.debug(startLabel);
const start = Date.now();
const r = await promise;
const endLabel = typeof label === 'function' ? label(r) : label;
const duration = Date.now() - start;
const durationPretty =
duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(2)}s`;
debug(`${endLabel} ${chalk.gray(`[${durationPretty}]`)}`);
this.debug(`${endLabel} ${chalk.gray(`[${durationPretty}]`)}`);
return r;
}
return promise;
}
return {
isDebugEnabled,
print,
log,
warn,
error,
prettyError,
ready,
success,
debug,
dim,
time,
note,
spinner: setSpinner,
stopSpinner,
};
}
export default function createOutput(opts?: OutputOptions) {
return new Output(opts);
}

View File

@@ -5,7 +5,9 @@ import renderLink from './link';
const metric = metrics();
export default function error(...input: string[] | [APIError]) {
export default function error(
...input: string[] | [Pick<APIError, 'slug' | 'message' | 'link' | 'action'>]
) {
let messages = input;
if (typeof input[0] === 'object') {
const { slug, message, link, action = 'Learn More' } = input[0];

View File

@@ -1,19 +1,20 @@
import { join } from 'path';
import fs from 'fs';
import os from 'os';
import AJV from 'ajv';
import chalk from 'chalk';
import { join } from 'path';
import { ensureDir } from 'fs-extra';
import { promisify } from 'util';
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
import Client from '../client';
import { ProjectNotFound } from '../errors-ts';
import getUser from '../get-user';
import getTeamById from '../get-team-by-id';
import getTeamById from '../teams/get-team-by-id';
import { Output } from '../output';
import { Project, ProjectLinkResult } from '../../types';
import { Org, ProjectLink } from '../../types';
import chalk from 'chalk';
import { prependEmoji, emoji, EmojiLabel } from '../emoji';
import AJV from 'ajv';
import { isDirectory } from '../config/global-path';
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
import outputCode from '../output/code';

View File

@@ -22,6 +22,5 @@ export default async function responseError(
}
const msg = bodyError?.message || fallbackMessage || 'Response Error';
return new APIError(msg, res, bodyError);
}

View File

@@ -1,125 +0,0 @@
import Now from './index';
import { URLSearchParams } from 'url';
export default class Teams extends Now {
async create({ slug }) {
return this.retry(async bail => {
const res = await this._fetch(`/teams`, {
method: 'POST',
body: {
slug,
},
});
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status === 400) {
const e = new Error(body.error.message);
e.code = body.error.code;
return bail(e);
}
if (res.status !== 200) {
const e = new Error(body.error.message);
e.code = body.error.code;
throw e;
}
return body;
});
}
async edit({ id, slug, name }) {
return this.retry(async bail => {
const payload = {};
if (name) {
payload.name = name;
}
if (slug) {
payload.slug = slug;
}
const res = await this._fetch(`/teams/${id}`, {
method: 'PATCH',
body: payload,
});
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status === 400) {
const e = new Error(body.error.message);
e.code = body.error.code;
return bail(e);
}
if (res.status !== 200) {
const e = new Error(body.error.message);
e.code = body.error.code;
throw e;
}
return body;
});
}
async inviteUser({ teamId, email }) {
return this.retry(async bail => {
const publicRes = await this._fetch(`/www/user/public?email=${email}`);
const { name, username } = await publicRes.json();
const res = await this._fetch(`/teams/${teamId}/members`, {
method: 'POST',
body: {
email,
},
});
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status === 400) {
const e = new Error(body.error.message);
e.code = body.error.code;
return bail(e);
}
if (res.status !== 200) {
const e = new Error(body.error.message);
e.code = body.error.code;
throw e;
}
return { ...body, name, username };
});
}
async ls({ next, apiVersion = 1 } = {}) {
return this.retry(async bail => {
const query = new URLSearchParams();
if (next) {
query.set('limit', 20);
query.set('until', next);
}
const res = await this._fetch(`/v${apiVersion}/teams?${query}`);
if (res.status === 403) {
const error = new Error('Unauthorized');
error.code = 'not_authorized';
return bail(error);
}
return res.json();
});
}
}

View File

@@ -0,0 +1,13 @@
import { Team } from '../../types';
import Client from '../client';
export default async function createTeam(
client: Client,
{ slug }: Pick<Team, 'slug'>
) {
const body = await client.fetch<Team>(`/teams`, {
method: 'POST',
body: { slug },
});
return body;
}

View File

@@ -1,5 +1,5 @@
import Client from './client';
import { Team } from '../types';
import Client from '../client';
import { Team } from '../../types';
const teamCache = new Map<string, Team>();

View File

@@ -0,0 +1,69 @@
import { URLSearchParams } from 'url';
import Client from '../client';
import { Team } from '../../types';
import { APIError, InvalidToken } from '../errors-ts';
export interface GetTeamsV1Options {
apiVersion?: 1;
}
export interface GetTeamsV2Options {
next?: number;
limit?: number;
apiVersion: 2;
}
export interface GetTeamsV2Response {
teams: Team[];
pagination: {
count: number;
next: number;
prev: number;
};
}
export default function getTeams(
client: Client,
opts?: GetTeamsV1Options
): Promise<Team[]>;
export default function getTeams(
client: Client,
opts: GetTeamsV2Options
): Promise<GetTeamsV2Response>;
export default async function getTeams(
client: Client,
opts: GetTeamsV1Options | GetTeamsV2Options = {}
): Promise<Team[] | GetTeamsV2Response> {
const { apiVersion = 1 } = opts;
let query = '';
if (opts.apiVersion === 2) {
// Enable pagination
const params = new URLSearchParams({
limit: String(typeof opts.limit === 'number' ? opts.limit : 20),
});
if (opts.next) {
params.set('next', String(opts.next));
}
query = `?${params}`;
}
try {
const body = await client.fetch<GetTeamsV2Response>(
`/v${apiVersion}/teams${query}`,
{
useCurrentTeam: false,
}
);
if (apiVersion === 1) {
return body.teams || [];
}
return body;
} catch (error) {
if (error instanceof APIError && error.status === 403) {
throw new InvalidToken();
}
throw error;
}
}

View File

@@ -0,0 +1,23 @@
import Client from '../client';
interface InviteResponse {
uid: string;
username: string;
email: string;
role: string;
}
export default async function inviteUserToTeam(
client: Client,
teamId: string,
email: string
) {
const body = await client.fetch<InviteResponse>(
`/teams/${encodeURIComponent(teamId)}/members`,
{
method: 'POST',
body: { email },
}
);
return body;
}

View File

@@ -0,0 +1,17 @@
import { Team } from '../../types';
import Client from '../client';
export default async function patchTeam(
client: Client,
teamId: string,
payload: Partial<Pick<Team, 'name' | 'slug'>>
) {
const body = await client.fetch<Team>(
`/teams/${encodeURIComponent(teamId)}`,
{
method: 'PATCH',
body: payload,
}
);
return body;
}

View File

@@ -0,0 +1,30 @@
import { client } from '../mocks/client';
import { useUser } from '../mocks/user';
import { useDeployment } from '../mocks/deployment';
import inspect from '../../src/commands/inspect';
describe('inspect', () => {
it('should print out deployment information', async () => {
const user = useUser();
const deployment = useDeployment({ creator: user });
client.setArgv('inspect', deployment.url);
const exitCode = await inspect(client);
expect(exitCode).toEqual(0);
expect(
client.mockOutput.mock.calls[0][0].startsWith(
`> Fetched deployment "${deployment.url}" in ${user.username}`
)
).toBeTruthy();
});
it('should print error when deployment not found', async () => {
const user = useUser();
useDeployment({ creator: user });
client.setArgv('inspect', 'bad.com');
const exitCode = await inspect(client);
expect(exitCode).toEqual(1);
expect(client.mockOutput.mock.calls[0][0]).toEqual(
`Error! Failed to find deployment "bad.com" in ${user.username}\n`
);
});
});

View File

@@ -0,0 +1,16 @@
import { client } from '../mocks/client';
import login from '../../src/commands/login';
describe('login', () => {
it('should not allow the `--token` flag', async () => {
client.setArgv('login', '--token', 'foo');
const exitCode = await login(client);
expect(exitCode).toEqual(2);
expect(client.mockOutput.mock.calls.length).toEqual(1);
expect(
client.mockOutput.mock.calls[0][0].includes(
'`--token` may not be used with the "login" command'
)
).toEqual(true);
});
});

View File

@@ -0,0 +1,20 @@
import { client } from '../mocks/client';
import { useUser } from '../mocks/user';
import whoami from '../../src/commands/whoami';
describe('whoami', () => {
it('should reject invalid arguments', async () => {
client.setArgv('--invalid');
await expect(whoami(client)).rejects.toThrow(
'unknown or unexpected option: --invalid'
);
});
it('should print the Vercel username', async () => {
const user = useUser();
const exitCode = await whoami(client);
expect(exitCode).toEqual(0);
expect(client.mockOutput.mock.calls.length).toEqual(1);
expect(client.mockOutput.mock.calls[0][0]).toEqual(`${user.username}\n`);
});
});

View File

@@ -1,264 +0,0 @@
import test from 'ava';
import npa from 'npm-package-arg';
import { filterPackage, isBundledBuilder } from '../src/util/dev/builder-cache';
test('[dev-builder] filter install "latest", cached canary', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@vercel/build-utils',
'canary',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install "canary", cached stable', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@vercel/build-utils@canary',
'latest',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install "latest", cached stable', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@vercel/build-utils',
'latest',
buildersPkg,
{}
);
t.is(result, false);
});
test('[dev-builder] filter install "canary", cached canary', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@vercel/build-utils@canary',
'canary',
buildersPkg,
{}
);
t.is(result, false);
});
test('[dev-builder] filter install URL, cached stable', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'https://tarball.now.sh',
'latest',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install URL, cached canary', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'https://tarball.now.sh',
'canary',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install "latest", cached URL - stable', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage(
'@vercel/build-utils',
'latest',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install "latest", cached URL - canary', t => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage(
'@vercel/build-utils',
'canary',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install not bundled version, cached same version', t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage(
'not-bundled-package@0.0.1',
'_',
buildersPkg,
{}
);
t.is(result, false);
});
test('[dev-builder] filter install not bundled version, cached different version', t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.9',
},
};
const result = filterPackage(
'not-bundled-package@0.0.1',
'_',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] filter install not bundled stable, cached version', t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package', '_', buildersPkg, {});
t.is(result, true);
});
test('[dev-builder] filter install not bundled tagged, cached tagged', t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '16.9.0-alpha.0',
},
};
const result = filterPackage(
'not-bundled-package@alpha',
'_',
buildersPkg,
{}
);
t.is(result, true);
});
test('[dev-builder] isBundledBuilder() - stable', t => {
const cliPkg = {
dependencies: {
'@vercel/node': '1.6.1',
},
};
// "canary" tag
{
const parsed = npa('@vercel/node@canary');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, false);
}
// "latest" tag
{
const parsed = npa('@vercel/node');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, true);
}
// specific matching version
{
const parsed = npa('@vercel/node@1.6.1');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, true);
}
// specific non-matching version
{
const parsed = npa('@vercel/node@1.6.0');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, false);
}
// URL
{
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, false);
}
});
test('[dev-builder] isBundledBuilder() - canary', t => {
const cliPkg = {
dependencies: {
'@vercel/node': '1.6.1-canary.0',
},
};
// "canary" tag
{
const parsed = npa('@vercel/node@canary');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, true);
}
// "latest" tag
{
const parsed = npa('@vercel/node');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, false);
}
// specific matching version
{
const parsed = npa('@vercel/node@1.6.1-canary.0');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, true);
}
// specific non-matching version
{
const parsed = npa('@vercel/node@1.5.2-canary.9');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, false);
}
// URL
{
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, cliPkg);
t.is(result, false);
}
});

View File

@@ -1,325 +0,0 @@
import test from 'ava';
import { devRouter } from '../src/util/dev/router';
test('[dev-router] 301 redirection', async t => {
const routesConfig = [
{
src: '/redirect',
status: 301,
headers: { Location: 'https://vercel.com' },
},
];
const result = await devRouter('/redirect', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/redirect',
continue: false,
status: 301,
headers: { location: 'https://vercel.com' },
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: false,
isDestUrl: false,
phase: undefined,
});
});
test('[dev-router] captured groups', async t => {
const routesConfig = [{ src: '/api/(.*)', dest: '/endpoints/$1.js' }];
const result = await devRouter('/api/user', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/endpoints/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
test('[dev-router] named groups', async t => {
const routesConfig = [{ src: '/user/(?<id>.+)', dest: '/user.js?id=$id' }];
const result = await devRouter('/user/123', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: { id: '123' },
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
test('[dev-router] optional named groups', async t => {
const routesConfig = [
{
src: '/api/hello(/(?<name>[^/]+))?',
dest: '/api/functions/hello/index.js?name=$name',
},
];
const result = await devRouter('/api/hello', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/api/functions/hello/index.js',
continue: false,
status: undefined,
headers: {},
uri_args: { name: '' },
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
test('[dev-router] proxy_pass', async t => {
const routesConfig = [{ src: '/proxy', dest: 'https://vercel.com' }];
const result = await devRouter('/proxy', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: 'https://vercel.com',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: false,
isDestUrl: true,
phase: undefined,
});
});
test('[dev-router] methods', async t => {
const routesConfig = [
{ src: '/.*', methods: ['POST'], dest: '/post' },
{ src: '/.*', methods: ['GET'], dest: '/get' },
];
let result = await devRouter('/', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/get',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[1],
matched_route_idx: 1,
userDest: true,
isDestUrl: false,
phase: undefined,
});
result = await devRouter('/', 'POST', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/post',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
test('[dev-router] match without prefix slash', async t => {
const routesConfig = [{ src: 'api/(.*)', dest: 'endpoints/$1.js' }];
const result = await devRouter('/api/user', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/endpoints/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
test('[dev-router] match with needed prefixed slash', async t => {
const routesConfig = [
{
src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$',
dest: '/some/dest',
},
];
const result = await devRouter('/post-1/comments', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/some/dest',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
status: undefined,
headers: {},
uri_args: {},
matched_route: {
src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$',
dest: '/some/dest',
},
matched_route_idx: 0,
});
});
test('[dev-router] `continue: true` with fallthrough', async t => {
const routesConfig = [
{
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
continue: true,
headers: {
'cache-control': 'immutable,max-age=31536000',
},
},
];
const result = await devRouter(
'/_next/static/chunks/0.js',
'GET',
routesConfig
);
t.deepEqual(result, {
found: false,
dest: '/_next/static/chunks/0.js',
continue: true,
isDestUrl: false,
phase: undefined,
status: undefined,
uri_args: {},
headers: {
'cache-control': 'immutable,max-age=31536000',
},
});
});
test('[dev-router] `continue: true` with match', async t => {
const routesConfig = [
{
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
continue: true,
headers: {
'cache-control': 'immutable,max-age=31536000',
},
},
{
src: '/(.*)',
dest: '/hi',
},
];
const result = await devRouter(
'/_next/static/chunks/0.js',
'GET',
routesConfig
);
t.deepEqual(result, {
found: true,
dest: '/hi',
continue: false,
status: undefined,
userDest: true,
isDestUrl: false,
phase: undefined,
uri_args: {},
headers: {
'cache-control': 'immutable,max-age=31536000',
},
matched_route: {
src: '/(.*)',
dest: '/hi',
},
matched_route_idx: 1,
});
});
test('[dev-router] match with catch-all with prefix slash', async t => {
const routesConfig = [{ src: '/(.*)', dest: '/www/$1' }];
const result = await devRouter('/', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/www/',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
status: undefined,
headers: {},
uri_args: {},
matched_route: { src: '/(.*)', dest: '/www/$1' },
matched_route_idx: 0,
});
});
test('[dev-router] match with catch-all with no prefix slash', async t => {
const routesConfig = [{ src: '(.*)', dest: '/www$1' }];
const result = await devRouter('/', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: '/www/',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
status: undefined,
headers: {},
uri_args: {},
matched_route: { src: '(.*)', dest: '/www$1' },
matched_route_idx: 0,
});
});
test('[dev-router] `continue: true` with `dest`', async t => {
const routesConfig = [
{ src: '/(.*)', dest: '/www/$1', continue: true },
{
src: '^/www/(a\\/([^\\/]+?)(?:\\/)?)$',
dest: 'http://localhost:5000/$1',
},
];
const result = await devRouter('/a/foo', 'GET', routesConfig);
t.deepEqual(result, {
found: true,
dest: 'http://localhost:5000/a/foo',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[1],
matched_route_idx: 1,
userDest: false,
isDestUrl: true,
phase: undefined,
});
});

View File

@@ -1,500 +0,0 @@
import url from 'url';
import test from 'ava';
import path from 'path';
import execa from 'execa';
import fs from 'fs-extra';
import fetch from 'node-fetch';
import listen from 'async-listen';
import { createServer } from 'http';
import createOutput from '../src/util/output';
import DevServer from '../src/util/dev/server';
import { installBuilders, getBuildUtils } from '../src/util/dev/builder-cache';
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, shell: true });
}
}
const skipOnWindows = new Set([
'now-dev-default-builds-and-routes',
'now-dev-static-routes',
'now-dev-static-build-routing',
'now-dev-directory-listing',
'now-dev-api-with-public',
'now-dev-api-with-static',
'now-dev-custom-404',
]);
function testFixture(name, fn) {
return async t => {
if (process.platform === 'win32' && skipOnWindows.has(name)) {
console.log(`Skipping test "${name}" on Windows.`);
t.is(true, true);
return;
}
let server;
const fixturePath = path.join(__dirname, 'fixtures', 'unit', name);
await runNpmInstall(fixturePath);
try {
let readyResolve;
let readyPromise = new Promise(resolve => {
readyResolve = resolve;
});
const debug = true;
const output = createOutput({ debug });
const origReady = output.ready;
output.ready = msg => {
if (msg.toString().match(/Available at/)) {
readyResolve();
}
origReady(msg);
};
server = new DevServer(fixturePath, { output, debug });
await server.start(0);
await readyPromise;
await fn(t, server);
} finally {
await server.stop();
}
};
}
function validateResponseHeaders(t, res, podId = null) {
t.is(res.headers.get('server'), 'Vercel');
t.truthy(res.headers.get('cache-control').length > 0);
t.truthy(
/^dev1::(dev1::)?[0-9a-z]{5}-[1-9][0-9]+-[a-f0-9]{12}$/.test(
res.headers.get('x-vercel-id')
)
);
if (podId) {
t.truthy(
res.headers.get('x-vercel-id').startsWith(`dev1::${podId}`) ||
res.headers.get('x-vercel-id').startsWith(`dev1::dev1::${podId}`)
);
}
}
test(
'[DevServer] Test request body',
testFixture('now-dev-request-body', async (t, server) => {
{
// Test that `req.body` works in dev
const res = await fetch(`${server.address}/api/req-body`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ hello: 'world' }),
});
const body = await res.json();
t.is(body.hello, 'world');
}
{
// Test that `req` "data" events work in dev
const res = await fetch(`${server.address}/api/data-events`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ hello: 'world' }),
});
const body = await res.json();
t.is(body.hello, 'world');
}
})
);
test(
'[DevServer] Maintains query when invoking lambda',
testFixture('now-dev-query-invoke', async (t, server) => {
const res = await fetch(`${server.address}/something?url-param=a`);
validateResponseHeaders(t, res);
const text = await res.text();
const parsed = url.parse(text, true);
t.is(parsed.pathname, '/something');
t.is(parsed.query['url-param'], 'a');
t.is(parsed.query['route-param'], 'b');
})
);
test(
'[DevServer] Maintains query when proxy passing',
testFixture('now-dev-query-proxy', async (t, server) => {
const dest = createServer((req, res) => {
res.end(req.url);
});
await listen(dest, 0);
const { port } = dest.address();
try {
const res = await fetch(`${server.address}/${port}?url-param=a`);
validateResponseHeaders(t, res);
const text = await res.text();
const parsed = url.parse(text, true);
t.is(parsed.pathname, '/something');
t.is(parsed.query['url-param'], 'a');
t.is(parsed.query['route-param'], 'b');
} finally {
dest.close();
}
})
);
test(
'[DevServer] Maintains query when builder defines routes',
testFixture('now-dev-next', async (t, server) => {
const res = await fetch(`${server.address}/something?url-param=a`);
validateResponseHeaders(t, res);
const text = await res.text();
// Hacky way of getting the page payload from the response
// HTML since we don't have a HTML parser handy.
const json = text
.match(/<div>(.*)<\/div>/)[1]
.replace('</div>', '')
.replace(/&quot;/g, '"');
const parsed = JSON.parse(json);
t.is(parsed.query['url-param'], 'a');
t.is(parsed.query['route-param'], 'b');
})
);
test(
'[DevServer] Allow `cache-control` to be overwritten',
testFixture('now-dev-headers', async (t, server) => {
const res = await fetch(
`${server.address}/?name=cache-control&value=immutable`
);
t.is(res.headers.get('cache-control'), 'immutable');
})
);
test(
'[DevServer] Sends `etag` header for static files',
testFixture('now-dev-headers', async (t, server) => {
if (process.platform === 'win32') {
console.log(
'Skipping "etag" test on windows since it yields a different result.'
);
t.is(true, true);
return;
}
const res = await fetch(`${server.address}/foo.txt`);
t.is(res.headers.get('etag'), '"d263af8ab880c0b97eb6c5c125b5d44f9e5addd9"');
t.is(await res.text(), 'hi\n');
})
);
test('[DevServer] Does not install builders if there are no builds', async t => {
const handler = data => {
if (data.includes('installing')) {
t.fail();
}
};
process.stdout.addListener('data', handler);
process.stderr.addListener('data', handler);
const output = createOutput({ debug: false });
await installBuilders(new Set(), undefined, output);
process.stdout.removeListener('data', handler);
process.stderr.removeListener('data', handler);
t.pass();
});
test('[DevServer] Installs canary build-utils if one more more builders is canary', t => {
t.is(
getBuildUtils(['@vercel/static', '@vercel/node@canary'], 'vercel'),
'@vercel/build-utils@canary'
);
t.is(
getBuildUtils(['@vercel/static', '@vercel/node@0.7.4-canary.0'], 'vercel'),
'@vercel/build-utils@canary'
);
t.is(
getBuildUtils(['@vercel/static', '@vercel/node@0.8.0'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(
getBuildUtils(['@vercel/static', '@vercel/node'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(
getBuildUtils(['@vercel/static'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(
getBuildUtils(['@vercel/md@canary'], 'vercel'),
'@vercel/build-utils@canary'
);
t.is(
getBuildUtils(['custom-builder'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(
getBuildUtils(['custom-builder@canary'], 'vercel'),
'@vercel/build-utils@canary'
);
t.is(getBuildUtils(['canary-bird'], 'vercel'), '@vercel/build-utils@latest');
t.is(
getBuildUtils(['canary-bird@4.0.0'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(
getBuildUtils(['canary-bird@canary'], 'vercel'),
'@vercel/build-utils@canary'
);
t.is(getBuildUtils(['@canary/bird'], 'vercel'), '@vercel/build-utils@latest');
t.is(
getBuildUtils(['@canary/bird@0.1.0'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(
getBuildUtils(['@canary/bird@canary'], 'vercel'),
'@vercel/build-utils@canary'
);
t.is(
getBuildUtils(['https://example.com'], 'vercel'),
'@vercel/build-utils@latest'
);
t.is(getBuildUtils([''], 'vercel'), '@vercel/build-utils@latest');
});
test(
'[DevServer] Test default builds and routes',
testFixture('now-dev-default-builds-and-routes', async (t, server) => {
let podId;
{
const res = await fetch(`${server.address}/`);
validateResponseHeaders(t, res);
podId = res.headers.get('x-vercel-id').match(/:(\w+)-/)[1];
const body = await res.text();
t.is(body.includes('hello, this is the frontend'), true);
}
{
const res = await fetch(`${server.address}/api/users`);
validateResponseHeaders(t, res, podId);
const body = await res.text();
t.is(body, 'users');
}
{
const res = await fetch(`${server.address}/api/users/1`);
validateResponseHeaders(t, res, podId);
const body = await res.text();
t.is(body, 'users/1');
}
{
const res = await fetch(`${server.address}/api/welcome`);
validateResponseHeaders(t, res, podId);
const body = await res.text();
t.is(body, 'hello and welcome');
}
})
);
test(
'[DevServer] Test `@vercel/static` routing',
testFixture('now-dev-static-routes', async (t, server) => {
{
const res = await fetch(`${server.address}/`);
t.is(res.status, 200);
const body = await res.text();
t.is(body, '<body>Hello!</body>\n');
}
})
);
test(
'[DevServer] Test `@vercel/static-build` routing',
testFixture('now-dev-static-build-routing', async (t, server) => {
{
const res = await fetch(`${server.address}/api/date`);
t.is(res.status, 200);
const body = await res.text();
t.is(body.startsWith('The current date:'), true);
}
})
);
test(
'[DevServer] Test directory listing',
testFixture('now-dev-directory-listing', async (t, server) => {
{
// Get directory listing
let res = await fetch(`${server.address}/`);
let body = await res.text();
t.is(res.status, 200);
t.truthy(body.includes('Index of'));
// Get a file
res = await fetch(`${server.address}/file.txt`);
body = await res.text();
t.is(res.status, 200);
t.is(body, 'Hello from file!\n');
// Invoke a lambda
res = await fetch(`${server.address}/lambda.js`);
body = await res.text();
t.is(res.status, 200);
t.is(body, 'Hello from Lambda!');
// Trigger a 404
res = await fetch(`${server.address}/does-not-exist`);
t.is(res.status, 404);
}
})
);
test(
'[DevServer] Test `public` directory with zero config',
testFixture('now-dev-api-with-public', async (t, server) => {
{
const res = await fetch(`${server.address}/api/user`);
const body = await res.text();
t.is(body, 'hello:user');
}
{
const res = await fetch(`${server.address}/`);
const body = await res.text();
t.is(body.startsWith('<h1>hello world</h1>'), true);
}
})
);
test(
'[DevServer] Test static files with zero config',
testFixture('now-dev-api-with-static', async (t, server) => {
{
const res = await fetch(`${server.address}/api/user`);
const body = await res.text();
t.is(body, 'bye:user');
}
{
const res = await fetch(`${server.address}/`);
const body = await res.text();
t.is(body.startsWith('<h1>goodbye world</h1>'), true);
}
})
);
test(
'[DevServer] 404 listing',
testFixture('now-dev-directory-listing', async (t, server) => {
{
// HTML response
const res = await fetch(`${server.address}/does-not-exist`, {
headers: {
Accept: 'text/html',
},
});
t.is(res.status, 404);
t.is(res.headers.get('content-type'), 'text/html; charset=utf-8');
const body = await res.text();
t.truthy(body.startsWith('<!DOCTYPE html>'));
}
{
// JSON response
const res = await fetch(`${server.address}/does-not-exist`, {
headers: {
Accept: 'application/json',
},
});
t.is(res.status, 404);
t.is(res.headers.get('content-type'), 'application/json');
const body = await res.text();
t.is(
body,
'{"error":{"code":404,"message":"The page could not be found."}}\n'
);
}
{
// Plain text response
const res = await fetch(`${server.address}/does-not-exist`);
t.is(res.status, 404);
const body = await res.text();
t.is(res.headers.get('content-type'), 'text/plain; charset=utf-8');
t.is(body, 'The page could not be found.\n\nNOT_FOUND\n');
}
})
);
test(
'[DevServer] custom 404 routes',
testFixture('now-dev-custom-404', async (t, server) => {
{
// Test custom 404 with static dest
const res = await fetch(`${server.address}/error.html`);
t.is(res.status, 404);
const body = await res.text();
t.is(body, '<div>Custom 404 page</div>\n');
}
{
// Test custom 404 with lambda dest
const res = await fetch(`${server.address}/error.js`);
t.is(res.status, 404);
const body = await res.text();
t.is(body, 'Custom 404 Lambda\n');
}
{
// Test regular 404 still works
const res = await fetch(`${server.address}/does-not-exist`);
t.is(res.status, 404);
const body = await res.text();
t.is(body, 'The page could not be found.\n\nNOT_FOUND\n');
}
})
);
test('[DevServer] parseListen()', t => {
t.deepEqual(parseListen('0'), [0]);
t.deepEqual(parseListen('3000'), [3000]);
t.deepEqual(parseListen('0.0.0.0'), [3000, '0.0.0.0']);
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']);
if (process.platform !== 'win32') {
t.deepEqual(parseListen('unix:/home/user/server.sock'), [
'/home/user/server.sock',
]);
t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [
'\\\\.\\pipe\\PipeName',
]);
}
let err;
try {
parseListen('bad://url');
} catch (_err) {
err = _err;
}
t.truthy(err);
t.is(err.message, 'Unknown `--listen` scheme (protocol): bad:');
});

View File

@@ -1,283 +0,0 @@
import test from 'ava';
import { validateConfig } from '../src/util/dev/validate';
test('[dev-validate] should not error with empty config', async (t) => {
const config = {};
const error = validateConfig(config);
t.deepEqual(error, null);
});
test('[dev-validate] should not error with complete config', async (t) => {
const config = {
version: 2,
public: true,
regions: ['sfo1', 'iad1'],
cleanUrls: true,
headers: [{ source: '/', headers: [{ key: 'x-id', value: '123' }] }],
rewrites: [{ source: '/help', destination: '/support' }],
redirects: [{ source: '/kb', destination: 'https://example.com' }],
trailingSlash: false,
functions: { 'api/user.go': { memory: 128, maxDuration: 5 } },
};
const error = validateConfig(config);
t.deepEqual(error, null);
});
test('[dev-validate] should not error with builds and routes', async (t) => {
const config = {
builds: [{ src: 'api/index.js', use: '@vercel/node' }],
routes: [{ src: '/(.*)', dest: '/api/index.js' }],
};
const error = validateConfig(config);
t.deepEqual(error, null);
});
test('[dev-validate] should error with invalid rewrites due to additional property and offer suggestion', async (t) => {
const config = {
rewrites: [{ src: '/(.*)', dest: '/api/index.js' }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `rewrites[0]` should NOT have additional property `src`. Did you mean `source`?'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/rewrites'
);
});
test('[dev-validate] should error with invalid routes due to additional property and offer suggestion', async (t) => {
const config = {
routes: [{ source: '/(.*)', destination: '/api/index.js' }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `routes[0]` should NOT have additional property `source`. Did you mean `src`?'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/routes'
);
});
test('[dev-validate] should error with invalid routes array type', async (t) => {
const config = {
routes: { src: '/(.*)', dest: '/api/index.js' },
};
const error = validateConfig(config);
t.deepEqual(error.message, 'Invalid vercel.json - `routes` should be array.');
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/routes'
);
});
test('[dev-validate] should error with invalid redirects array object', async (t) => {
const config = {
redirects: [
{
/* intentionally empty */
},
],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `redirects[0]` missing required property `source`.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/redirects'
);
});
test('[dev-validate] should error with invalid redirects.permanent poperty', async (t) => {
const config = {
redirects: [{ source: '/', destination: '/go', permanent: 'yes' }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `redirects[0].permanent` should be boolean.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/redirects'
);
});
test('[dev-validate] should error with invalid cleanUrls type', async (t) => {
const config = {
cleanUrls: 'true',
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `cleanUrls` should be boolean.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/cleanurls'
);
});
test('[dev-validate] should error with invalid trailingSlash type', async (t) => {
const config = {
trailingSlash: [true],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `trailingSlash` should be boolean.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/trailingslash'
);
});
test('[dev-validate] should error with invalid headers property', async (t) => {
const config = {
headers: [{ 'Content-Type': 'text/html' }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `headers[0]` should NOT have additional property `Content-Type`. Please remove it.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with invalid headers.source type', async (t) => {
const config = {
headers: [{ source: [{ 'Content-Type': 'text/html' }] }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `headers[0].source` should be string.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with invalid headers additional property', async (t) => {
const config = {
headers: [{ source: '/', stuff: [{ 'Content-Type': 'text/html' }] }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `headers[0]` should NOT have additional property `stuff`. Please remove it.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with invalid headers wrong nested headers type', async (t) => {
const config = {
headers: [{ source: '/', headers: [{ 'Content-Type': 'text/html' }] }],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `headers[0].headers[0]` should NOT have additional property `Content-Type`. Please remove it.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with invalid headers wrong nested headers additional property', async (t) => {
const config = {
headers: [
{ source: '/', headers: [{ key: 'Content-Type', val: 'text/html' }] },
],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `headers[0].headers[0]` should NOT have additional property `val`. Please remove it.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with too many redirects', async (t) => {
const config = {
redirects: Array.from({ length: 5000 }).map((_, i) => ({
source: `/${i}`,
destination: `/v/${i}`,
})),
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `redirects` should NOT have more than 1024 items.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/redirects'
);
});
test('[dev-validate] should error with too many nested headers', async (t) => {
const config = {
headers: [
{
source: '/',
headers: [{ key: `x-id`, value: `123` }],
},
{
source: '/too-many',
headers: Array.from({ length: 5000 }).map((_, i) => ({
key: `${i}`,
value: `${i}`,
})),
},
],
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'Invalid vercel.json - `headers[1].headers` should NOT have more than 1024 items.'
);
t.deepEqual(
error.link,
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with "functions" and "builds"', async (t) => {
const config = {
builds: [
{
src: 'index.html',
use: '@vercel/static',
},
],
functions: {
'api/test.js': {
memory: 1024,
},
},
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.'
);
t.deepEqual(error.link, 'https://vercel.link/functions-and-builds');
});

View File

@@ -307,7 +307,7 @@ test('login', async t => {
t.is(loginOutput.exitCode, 0, formatOutput(loginOutput));
t.regex(
loginOutput.stdout,
loginOutput.stderr,
/You are now logged in\./gm,
formatOutput(loginOutput)
);

View File

@@ -0,0 +1,109 @@
import chalk from 'chalk';
import { createServer, Server } from 'http';
import express, { Express, Router } from 'express';
import listen from 'async-listen';
import Client from '../../src/util/client';
import { Output } from '../../src/util/output';
// Disable colors in `chalk` so that tests don't need
// to worry about ANSI codes
chalk.level = 0;
export type Scenario = Router;
export class MockClient extends Client {
mockServer?: Server;
mockOutput: jest.Mock<void, Parameters<Output['print']>>;
private app: Express;
scenario: Scenario;
constructor() {
super({
argv: [],
// Gets populated in `startMockServer()`
apiUrl: '',
authConfig: {},
output: new Output(),
config: {},
localConfig: {},
});
this.mockOutput = jest.fn();
this.app = express();
this.app.use(express.json());
// play scenario
this.app.use((req, res, next) => {
this.scenario(req, res, next);
});
// catch requests that were not intercepted
this.app.use((req, res) => {
const message = `[Vercel API Mock] \`${req.method} ${req.path}\` was not handled.`;
console.warn(message);
res.status(404).json({
error: {
code: 'not_found',
message,
},
});
});
this.scenario = Router();
}
reset() {
this.output = new Output();
this.mockOutput = jest.fn();
this.output.print = s => {
//process.stdout.write(s);
return this.mockOutput(s);
};
this.argv = [];
this.authConfig = {};
this.config = {};
this.localConfig = {};
// Just make this one silent
this.output.spinner = () => {};
this.scenario = Router();
}
async startMockServer() {
this.mockServer = createServer(this.app);
await listen(this.mockServer, 0);
const address = this.mockServer.address();
if (!address || typeof address === 'string') {
throw new Error('Unexpected http server address');
}
this.apiUrl = `http://127.0.0.1:${address.port}`;
}
stopMockServer() {
this.mockServer?.close();
}
setArgv(...argv: string[]) {
this.argv = [process.execPath, 'cli.js', ...argv];
}
useScenario(scenario: Scenario) {
this.scenario = scenario;
}
}
export const client = new MockClient();
beforeAll(async () => {
await client.startMockServer();
});
beforeEach(() => {
client.reset();
});
afterAll(() => {
client.stopMockServer();
});

View File

@@ -0,0 +1,80 @@
import { URL } from 'url';
import chance from 'chance';
import { Deployment } from '@vercel/client';
import { client } from './client';
import { Build, User } from '../../src/types';
let deployments = new Map<string, Deployment>();
let deploymentBuilds = new Map<Deployment, Build[]>();
export function useDeployment({ creator }: { creator: Pick<User, 'uid'> }) {
const createdAt = Date.now();
const url = new URL(chance().url());
const deployment: Deployment = {
id: `dpl_${chance().guid()}`,
url: url.hostname,
name: '',
meta: {},
regions: [],
routes: [],
plan: 'hobby',
public: false,
version: 2,
createdAt,
createdIn: 'sfo1',
ownerId: creator.uid,
readyState: 'READY',
env: {},
build: { env: {} },
target: 'production',
alias: [],
aliasAssigned: true,
aliasError: null,
};
deployments.set(deployment.id, deployment);
deploymentBuilds.set(deployment, []);
return deployment;
}
beforeEach(() => {
deployments = new Map();
deploymentBuilds = new Map();
client.scenario.get('/:version/deployments/:id', (req, res) => {
const { id } = req.params;
const { url } = req.query;
let deployment;
if (id === 'get') {
if (typeof url !== 'string') {
res.statusCode = 400;
return res.json({ error: { code: 'bad_request' } });
}
deployment = Array.from(deployments.values()).find(d => {
return d.url === url;
});
} else {
// lookup by ID
deployment = deployments.get(id);
}
if (!deployment) {
res.statusCode = 404;
return res.json({
error: { code: 'not_found', message: 'Deployment not found', id },
});
}
res.json(deployment);
});
client.scenario.get('/:version/deployments/:id/builds', (req, res) => {
const { id } = req.params;
const deployment = deployments.get(id);
if (!deployment) {
res.statusCode = 404;
return res.json({ error: { code: 'not_found' } });
}
const builds = deploymentBuilds.get(deployment);
res.json({ builds });
});
});

View File

@@ -0,0 +1,21 @@
import chance from 'chance';
import { client } from './client';
export function useUser() {
const userLimited = {
uid: chance().guid(),
email: chance().email(),
name: chance().name(),
username: chance().first().toLowerCase(),
};
client.scenario.get('/www/user', (_req, res) => {
res.json({
user: userLimited,
});
});
return {
...userLimited,
};
}

4
packages/cli/test/tsconfig.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.test.ts"]
}

View File

@@ -1,485 +0,0 @@
import { basename, join, sep } from 'path';
import test from 'ava';
import sinon from 'sinon';
import { asc as alpha } from 'alpha-sort';
import fetch from 'node-fetch';
import createOutput from '../src/util/output';
import getProjectName from '../src/util/get-project-name';
import toHost from '../src/util/to-host';
import wait from '../src/util/output/wait';
import { responseError, responseErrorMessage } from '../src/util/error';
import getURL from './helpers/get-url';
import { staticFiles as getStaticFiles_ } from '../src/util/get-files';
import didYouMean from '../src/util/init/did-you-mean';
import { isValidName } from '../src/util/is-valid-name';
import getUpdateCommand from '../src/util/get-update-command';
import { isCanary } from '../src/util/is-canary';
import { getVercelDirectory } from '../src/util/projects/link';
const output = createOutput({ debug: false });
const prefix = `${join(__dirname, 'fixtures', 'unit')}${sep}`;
const base = path => path.replace(prefix, '');
const fixture = name => join(prefix, name);
const send = (res, statusCode, body) => {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'application/json; charset=utf8');
res.end(JSON.stringify(body));
};
const getStaticFiles = async dir => {
const files = await getStaticFiles_(dir, {
output,
});
return normalizeWindowsPaths(files);
};
const normalizeWindowsPaths = files => {
if (process.platform === 'win32') {
const prefix = 'D:/a/vercel/vercel/packages/cli/test/fixtures/unit/';
return files.map(f => f.replace(/\\/g, '/').slice(prefix.length));
}
return files;
};
test('discover files for builds deployment', async t => {
const path = 'now-json-static-no-files';
let files = await getStaticFiles(fixture(path), true);
files = files.sort(alpha);
t.is(files.length, 4);
t.is(base(files[0]), `${path}/a.js`);
t.is(base(files[1]), `${path}/b.js`);
t.is(base(files[2]), `${path}/build/a/c.js`);
t.is(base(files[3]), `${path}/package.json`);
});
test('should observe .vercelignore file', async t => {
const path = 'vercelignore';
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
t.is(files.length, 6);
t.is(base(files[0]), `${path}/.vercelignore`);
t.is(base(files[1]), `${path}/a.js`);
t.is(base(files[2]), `${path}/build/sub/a.js`);
t.is(base(files[3]), `${path}/build/sub/c.js`);
t.is(base(files[4]), `${path}/c.js`);
t.is(base(files[5]), `${path}/package.json`);
});
test('simple to host', t => {
t.is(toHost('vercel.com'), 'vercel.com');
});
test('leading // to host', t => {
t.is(
toHost('//zeit-logos-rnemgaicnc.now.sh'),
'zeit-logos-rnemgaicnc.now.sh'
);
});
test('leading http:// to host', t => {
t.is(
toHost('http://zeit-logos-rnemgaicnc.now.sh'),
'zeit-logos-rnemgaicnc.now.sh'
);
});
test('leading https:// to host', t => {
t.is(
toHost('https://zeit-logos-rnemgaicnc.now.sh'),
'zeit-logos-rnemgaicnc.now.sh'
);
});
test('leading https:// and path to host', t => {
t.is(
toHost('https://zeit-logos-rnemgaicnc.now.sh/path'),
'zeit-logos-rnemgaicnc.now.sh'
);
});
test('simple and path to host', t => {
t.is(toHost('vercel.com/test'), 'vercel.com');
});
test('`wait` utility does not invoke spinner before n miliseconds', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {},
});
const timeOut = 200;
const stop = wait('test', timeOut, oraStub);
stop();
t.truthy(oraStub.notCalled);
});
test('`wait` utility invokes spinner after n miliseconds', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {},
});
const timeOut = 200;
const delayedWait = () =>
new Promise(resolve => {
const stop = wait('test', timeOut, oraStub);
setTimeout(() => {
resolve();
stop();
}, timeOut + 100);
});
await delayedWait();
t.is(oraStub.calledOnce, true);
});
test('`wait` utility does not invoke spinner when stopped before delay', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {},
});
const timeOut = 200;
const delayedWait = () =>
new Promise(resolve => {
const stop = wait('test', timeOut, oraStub);
stop();
setTimeout(() => {
resolve();
}, timeOut + 100);
});
await delayedWait();
t.is(oraStub.notCalled, true);
});
test('4xx response error with fallback message', async t => {
const fn = (req, res) => {
send(res, 404, {});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res, 'Failed to load data');
t.is(formatted.message, 'Failed to load data (404)');
});
test('4xx response error without fallback message', async t => {
const fn = (req, res) => {
send(res, 404, {});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res);
t.is(formatted.message, 'Response Error (404)');
});
test('5xx response error without fallback message', async t => {
const fn = (req, res) => {
send(res, 500, '');
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res);
t.is(formatted.message, 'Response Error (500)');
});
test('4xx response error as correct JSON', async t => {
const fn = (req, res) => {
send(res, 400, {
error: {
message: 'The request is not correct',
},
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res);
t.is(formatted.message, 'The request is not correct (400)');
});
test('5xx response error as HTML', async t => {
const fn = (req, res) => {
send(res, 500, 'This is a malformed error');
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res, 'Failed to process data');
t.is(formatted.message, 'Failed to process data (500)');
});
test('5xx response error with random JSON', async t => {
const fn = (req, res) => {
send(res, 500, {
wrong: 'property',
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res, 'Failed to process data');
t.is(formatted.message, 'Failed to process data (500)');
});
test('getProjectName with argv', t => {
const project = getProjectName({
argv: {
'--name': 'abc',
},
});
t.is(project, 'abc');
});
test('getProjectName with now.json', t => {
const project = getProjectName({
argv: {},
nowConfig: { name: 'abc' },
});
t.is(project, 'abc');
});
test('getProjectName with a file', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
isFile: true,
});
t.is(project, 'files');
});
test('getProjectName with a multiple files', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png'],
});
t.is(project, 'files');
});
test('getProjectName with a directory', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa'],
});
t.is(project, 'aa');
});
test('4xx error message with broken JSON', async t => {
const fn = (req, res) => {
send(res, 403, `32puuuh2332`);
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseErrorMessage(res, 'Not authenticated');
t.is(formatted, 'Not authenticated (403)');
});
test('4xx error message with proper message', async t => {
const fn = (req, res) => {
send(res, 403, {
error: {
message: 'This is a test',
},
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseErrorMessage(res);
t.is(formatted, 'This is a test (403)');
});
test('5xx error message with proper message', async t => {
const fn = (req, res) => {
send(res, 500, {
error: {
message: 'This is a test',
},
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseErrorMessage(res);
t.is(formatted, 'Response Error (500)');
});
test('4xx response error with broken JSON', async t => {
const fn = (req, res) => {
send(res, 403, `122{"sss"`);
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res, 'Not authenticated');
t.is(formatted.message, 'Not authenticated (403)');
});
test('4xx response error as correct JSON with more properties', async t => {
const fn = (req, res) => {
send(res, 403, {
error: {
message: 'The request is not correct',
additionalProperty: 'test',
},
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res);
t.is(formatted.message, 'The request is not correct (403)');
t.is(formatted.additionalProperty, 'test');
});
test('429 response error with retry header', async t => {
const fn = (req, res) => {
res.setHeader('Retry-After', '20');
send(res, 429, {
error: {
message: 'You were rate limited',
},
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res);
t.is(formatted.message, 'You were rate limited (429)');
t.is(formatted.retryAfter, 20);
});
test('429 response error without retry header', async t => {
const fn = (req, res) => {
send(res, 429, {
error: {
message: 'You were rate limited',
},
});
};
const url = await getURL(fn);
const res = await fetch(url);
const formatted = await responseError(res);
t.is(formatted.message, 'You were rate limited (429)');
t.is(formatted.retryAfter, undefined);
});
test("guess user's intention with custom didYouMean", async t => {
const examples = [
'apollo',
'create-react-app',
'docz',
'gatsby',
'go',
'gridsome',
'html-minifier',
'mdx-deck',
'monorepo',
'nextjs',
'nextjs-news',
'nextjs-static',
'node-server',
'nodejs',
'nodejs-canvas-partyparrot',
'nodejs-coffee',
'nodejs-express',
'nodejs-hapi',
'nodejs-koa',
'nodejs-koa-ts',
'nodejs-pdfkit',
'nuxt-static',
'optipng',
'php-7',
'puppeteer-screenshot',
'python',
'redirect',
'serverless-ssr-reddit',
'static',
'vue',
'vue-ssr',
'vuepress',
];
t.is(didYouMean('md', examples, 0.7), 'mdx-deck');
t.is(didYouMean('koa', examples, 0.7), 'nodejs-koa');
t.is(didYouMean('node', examples, 0.7), 'nodejs');
t.is(didYouMean('12345', examples, 0.7), undefined);
});
test('check valid name', async t => {
t.is(isValidName('hello world'), true);
t.is(isValidName('käse'), true);
t.is(isValidName('ねこ'), true);
t.is(isValidName('/'), false);
t.is(isValidName('/#'), false);
t.is(isValidName('//'), false);
t.is(isValidName('/ねこ'), true);
t.is(isValidName('привет'), true);
t.is(isValidName('привет#'), true);
});
test('detect update command', async t => {
const updateCommand = await getUpdateCommand();
t.is(updateCommand, `yarn add vercel@${isCanary() ? 'canary' : 'latest'}`);
});
test('`getVercelDirectory()` returns ".vercel"', t => {
const cwd = fixture('get-vercel-directory');
const dir = getVercelDirectory(cwd);
t.is(basename(dir), '.vercel');
});
test('`getVercelDirectory()` returns ".now"', t => {
const cwd = fixture('get-vercel-directory-legacy');
const dir = getVercelDirectory(cwd);
t.is(basename(dir), '.now');
});
test('`getVercelDirectory()` throws an error if ".vercel" and ".now" exist', t => {
let err;
const cwd = fixture('get-vercel-directory-error');
try {
getVercelDirectory(cwd);
} catch (_err) {
err = _err;
}
t.is(
err.message,
'Both `.vercel` and `.now` directories exist. Please remove the `.now` directory.'
);
});

View File

@@ -0,0 +1,301 @@
import npa from 'npm-package-arg';
import {
filterPackage,
getBuildUtils,
isBundledBuilder,
} from '../../../src/util/dev/builder-cache';
describe('filterPackage', () => {
it('should filter install "latest", cached canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@vercel/build-utils',
'canary',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install "canary", cached stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@vercel/build-utils@canary',
'latest',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install "latest", cached stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@vercel/build-utils',
'latest',
buildersPkg,
{}
);
expect(result).toEqual(false);
});
it('should filter install "canary", cached canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@vercel/build-utils@canary',
'canary',
buildersPkg,
{}
);
expect(result).toEqual(false);
});
it('should filter install URL, cached stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'https://tarball.now.sh',
'latest',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install URL, cached canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'https://tarball.now.sh',
'canary',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install "latest", cached URL - stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage(
'@vercel/build-utils',
'latest',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install "latest", cached URL - canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage(
'@vercel/build-utils',
'canary',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install not bundled version, cached same version', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage(
'not-bundled-package@0.0.1',
'_',
buildersPkg,
{}
);
expect(result).toEqual(false);
});
it('should filter install not bundled version, cached different version', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.9',
},
};
const result = filterPackage(
'not-bundled-package@0.0.1',
'_',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
it('should filter install not bundled stable, cached version', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package', '_', buildersPkg, {});
expect(result).toEqual(true);
});
it('should filter install not bundled tagged, cached tagged', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '16.9.0-alpha.0',
},
};
const result = filterPackage(
'not-bundled-package@alpha',
'_',
buildersPkg,
{}
);
expect(result).toEqual(true);
});
});
describe('getBuildUtils', () => {
const tests: [string[], string][] = [
[['@vercel/static', '@vercel/node@canary'], 'canary'],
[['@vercel/static', '@vercel/node@0.7.4-canary.0'], 'canary'],
[['@vercel/static', '@vercel/node@0.8.0'], 'latest'],
[['@vercel/static', '@vercel/node'], 'latest'],
[['@vercel/static'], 'latest'],
[['@vercel/md@canary'], 'canary'],
[['custom-builder'], 'latest'],
[['custom-builder@canary'], 'canary'],
[['canary-bird'], 'latest'],
[['canary-bird@4.0.0'], 'latest'],
[['canary-bird@canary'], 'canary'],
[['@canary/bird'], 'latest'],
[['@canary/bird@0.1.0'], 'latest'],
[['@canary/bird@canary'], 'canary'],
[['https://example.com'], 'latest'],
[[''], 'latest'],
];
for (const [input, expected] of tests) {
it(`should install "${expected}" with input ${JSON.stringify(
input
)}`, () => {
const result = getBuildUtils(input);
expect(result).toEqual(`@vercel/build-utils@${expected}`);
});
}
});
describe('isBundledBuilder', () => {
it('should work with "stable" releases', () => {
const cliPkg = {
dependencies: {
'@vercel/node': '1.6.1',
},
};
// "canary" tag
{
const parsed = npa('@vercel/node@canary');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
}
// "latest" tag
{
const parsed = npa('@vercel/node');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
}
// specific matching version
{
const parsed = npa('@vercel/node@1.6.1');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
}
// specific non-matching version
{
const parsed = npa('@vercel/node@1.6.0');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
}
// URL
{
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
}
});
it('should work with "canary" releases', () => {
const cliPkg = {
dependencies: {
'@vercel/node': '1.6.1-canary.0',
},
};
// "canary" tag
{
const parsed = npa('@vercel/node@canary');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
}
// "latest" tag
{
const parsed = npa('@vercel/node');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
}
// specific matching version
{
const parsed = npa('@vercel/node@1.6.1-canary.0');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
}
// specific non-matching version
{
const parsed = npa('@vercel/node@1.5.2-canary.9');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
}
// URL
{
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
}
});
});

View File

@@ -0,0 +1,61 @@
import parseListen from '../../../src/util/dev/parse-listen';
describe('parseListen', () => {
it('should parse "0" as port 0', () => {
const result = parseListen('0');
expect(result).toHaveLength(1);
expect(result[0]).toEqual(0);
});
it('should parse "3000" as port 3000', () => {
const result = parseListen('3000');
expect(result).toHaveLength(1);
expect(result[0]).toEqual(3000);
});
it('should parse "0.0.0.0" as IP address', () => {
const result = parseListen('0.0.0.0');
expect(result).toHaveLength(2);
expect(result[0]).toEqual(3000);
expect(result[1]).toEqual('0.0.0.0');
});
it('should parse "127.0.0.1:4000" as IP address and port', () => {
const result = parseListen('127.0.0.1:4000');
expect(result).toHaveLength(2);
expect(result[0]).toEqual(4000);
expect(result[1]).toEqual('127.0.0.1');
});
it('should parse "tcp://127.0.0.1:5000" as IP address and port', () => {
const result = parseListen('tcp://127.0.0.1:5000');
expect(result).toHaveLength(2);
expect(result[0]).toEqual(5000);
expect(result[1]).toEqual('127.0.0.1');
});
if (process.platform !== 'win32') {
it('should parse "unix:/home/user/server.sock" as UNIX socket file', () => {
const result = parseListen('unix:/home/user/server.sock');
expect(result).toHaveLength(1);
expect(result[0]).toEqual('/home/user/server.sock');
});
it('should parse "pipe:\\\\.\\pipe\\PipeName" as UNIX pipe', () => {
const result = parseListen('pipe:\\\\.\\pipe\\PipeName');
expect(result).toHaveLength(1);
expect(result[0]).toEqual('\\\\.\\pipe\\PipeName');
});
}
it('should fail to parse "bad://url"', () => {
let err: Error;
try {
parseListen('bad://url');
throw new Error('Should not happen');
} catch (_err) {
err = _err;
}
expect(err.message).toEqual('Unknown `--listen` scheme (protocol): bad:');
});
});

View File

@@ -0,0 +1,326 @@
import { devRouter } from '../../../src/util/dev/router';
describe('devRouter', () => {
it('should handle 301 redirection', async () => {
const routesConfig = [
{
src: '/redirect',
status: 301,
headers: { Location: 'https://vercel.com' },
},
];
const result = await devRouter('/redirect', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/redirect',
continue: false,
status: 301,
headers: { location: 'https://vercel.com' },
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: false,
isDestUrl: false,
phase: undefined,
});
});
it('should match captured groups', async () => {
const routesConfig = [{ src: '/api/(.*)', dest: '/endpoints/$1.js' }];
const result = await devRouter('/api/user', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/endpoints/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
it('should match named groups', async () => {
const routesConfig = [{ src: '/user/(?<id>.+)', dest: '/user.js?id=$id' }];
const result = await devRouter('/user/123', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: { id: '123' },
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
it('should match optional named groups', async () => {
const routesConfig = [
{
src: '/api/hello(/(?<name>[^/]+))?',
dest: '/api/functions/hello/index.js?name=$name',
},
];
const result = await devRouter('/api/hello', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/api/functions/hello/index.js',
continue: false,
status: undefined,
headers: {},
uri_args: { name: '' },
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
it('should match proxy_pass', async () => {
const routesConfig = [{ src: '/proxy', dest: 'https://vercel.com' }];
const result = await devRouter('/proxy', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: 'https://vercel.com',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: false,
isDestUrl: true,
phase: undefined,
});
});
it('should match `methods`', async () => {
const routesConfig = [
{ src: '/.*', methods: ['POST'], dest: '/post' },
{ src: '/.*', methods: ['GET'], dest: '/get' },
];
let result = await devRouter('/', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/get',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[1],
matched_route_idx: 1,
userDest: true,
isDestUrl: false,
phase: undefined,
});
result = await devRouter('/', 'POST', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/post',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
it('should match without prefix slash', async () => {
const routesConfig = [{ src: 'api/(.*)', dest: 'endpoints/$1.js' }];
const result = await devRouter('/api/user', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/endpoints/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[0],
matched_route_idx: 0,
userDest: true,
isDestUrl: false,
phase: undefined,
});
});
it('should match with needed prefixed slash', async () => {
const routesConfig = [
{
src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$',
dest: '/some/dest',
},
];
const result = await devRouter('/post-1/comments', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/some/dest',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
status: undefined,
headers: {},
uri_args: {},
matched_route: {
src: '^\\/([^\\/]+?)\\/comments(?:\\/)?$',
dest: '/some/dest',
},
matched_route_idx: 0,
});
});
it('should match `continue: true` with fallthrough', async () => {
const routesConfig = [
{
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
continue: true,
headers: {
'cache-control': 'immutable,max-age=31536000',
},
},
];
const result = await devRouter(
'/_next/static/chunks/0.js',
'GET',
routesConfig
);
expect(result).toMatchObject({
found: false,
dest: '/_next/static/chunks/0.js',
continue: true,
isDestUrl: false,
phase: undefined,
status: undefined,
uri_args: {},
headers: {
'cache-control': 'immutable,max-age=31536000',
},
});
});
it('should match `continue: true` with match', async () => {
const routesConfig = [
{
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
continue: true,
headers: {
'cache-control': 'immutable,max-age=31536000',
},
},
{
src: '/(.*)',
dest: '/hi',
},
];
const result = await devRouter(
'/_next/static/chunks/0.js',
'GET',
routesConfig
);
expect(result).toMatchObject({
found: true,
dest: '/hi',
continue: false,
status: undefined,
userDest: true,
isDestUrl: false,
phase: undefined,
uri_args: {},
headers: {
'cache-control': 'immutable,max-age=31536000',
},
matched_route: {
src: '/(.*)',
dest: '/hi',
},
matched_route_idx: 1,
});
});
it('should match with catch-all with prefix slash', async () => {
const routesConfig = [{ src: '/(.*)', dest: '/www/$1' }];
const result = await devRouter('/', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/www/',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
status: undefined,
headers: {},
uri_args: {},
matched_route: { src: '/(.*)', dest: '/www/$1' },
matched_route_idx: 0,
});
});
it('should match with catch-all with no prefix slash', async () => {
const routesConfig = [{ src: '(.*)', dest: '/www$1' }];
const result = await devRouter('/', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: '/www/',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
status: undefined,
headers: {},
uri_args: {},
matched_route: { src: '(.*)', dest: '/www$1' },
matched_route_idx: 0,
});
});
it('should match `continue: true` with `dest`', async () => {
const routesConfig = [
{ src: '/(.*)', dest: '/www/$1', continue: true },
{
src: '^/www/(a\\/([^\\/]+?)(?:\\/)?)$',
dest: 'http://localhost:5000/$1',
},
];
const result = await devRouter('/a/foo', 'GET', routesConfig);
expect(result).toMatchObject({
found: true,
dest: 'http://localhost:5000/a/foo',
continue: false,
status: undefined,
headers: {},
uri_args: {},
matched_route: routesConfig[1],
matched_route_idx: 1,
userDest: false,
isDestUrl: true,
phase: undefined,
});
});
});

View File

@@ -0,0 +1,338 @@
import ms from 'ms';
import url from 'url';
import path from 'path';
import execa from 'execa';
import fs from 'fs-extra';
import fetch, { Response } from 'node-fetch';
import listen from 'async-listen';
import { createServer } from 'http';
import { client } from '../../mocks/client';
import DevServer from '../../../src/util/dev/server';
async function runNpmInstall(fixturePath: string) {
if (await fs.pathExists(path.join(fixturePath, 'package.json'))) {
return execa('yarn', ['install'], { cwd: fixturePath, shell: true });
}
}
const testFixture =
(name: string, fn: (server: DevServer) => Promise<void>) => async () => {
let server: DevServer | null = null;
const fixturePath = path.join(__dirname, '../../fixtures/unit', name);
await runNpmInstall(fixturePath);
try {
server = new DevServer(fixturePath, { output: client.output });
await server.start(0);
await fn(server);
} finally {
if (server) {
await server.stop();
}
}
};
function validateResponseHeaders(res: Response, podId?: string) {
expect(res.headers.get('server')).toEqual('Vercel');
expect(res.headers.get('cache-control')!.length > 0).toBeTruthy();
expect(
/^dev1::(dev1::)?[0-9a-z]{5}-[1-9][0-9]+-[a-f0-9]{12}$/.test(
res.headers.get('x-vercel-id')!
)
).toBeTruthy();
if (podId) {
expect(
res.headers.get('x-vercel-id')!.startsWith(`dev1::${podId}`) ||
res.headers.get('x-vercel-id')!.startsWith(`dev1::dev1::${podId}`)
).toBeTruthy();
}
}
describe('DevServer', () => {
jest.setTimeout(ms('2m'));
it(
'should support request body',
testFixture('now-dev-request-body', async server => {
const body = { hello: 'world' };
// Test that `req.body` works in dev
let res = await fetch(`${server.address}/api/req-body`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
validateResponseHeaders(res);
expect(await res.json()).toMatchObject(body);
// Test that `req` "data" events work in dev
res = await fetch(`${server.address}/api/data-events`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
expect(await res.json()).toMatchObject(body);
})
);
it(
'should maintain query when invoking serverless function',
testFixture('now-dev-query-invoke', async server => {
const res = await fetch(`${server.address}/something?url-param=a`);
validateResponseHeaders(res);
const text = await res.text();
const parsed = url.parse(text, true);
expect(parsed.pathname).toEqual('/something');
expect(parsed.query['url-param']).toEqual('a');
expect(parsed.query['route-param']).toEqual('b');
})
);
it(
'should maintain query when proxy passing',
testFixture('now-dev-query-proxy', async server => {
const dest = createServer((req, res) => {
res.end(req.url);
});
await listen(dest, 0);
const addr = dest.address();
if (!addr || typeof addr === 'string') {
throw new Error('Unexpected HTTP address');
}
const { port } = addr;
try {
const res = await fetch(`${server.address}/${port}?url-param=a`);
validateResponseHeaders(res);
const text = await res.text();
const parsed = url.parse(text, true);
expect(parsed.pathname).toEqual('/something');
expect(parsed.query['url-param']).toEqual('a');
expect(parsed.query['route-param']).toEqual('b');
} finally {
dest.close();
}
})
);
it(
'should maintain query when builder defines routes',
testFixture('now-dev-next', async server => {
const res = await fetch(`${server.address}/something?url-param=a`);
validateResponseHeaders(res);
const text = await res.text();
// Hacky way of getting the page payload from the response
// HTML since we don't have a HTML parser handy.
const json = text
.match(/<div>(.*)<\/div>/)![1]
.replace('</div>', '')
.replace(/&quot;/g, '"');
const parsed = JSON.parse(json);
expect(parsed.query['url-param']).toEqual('a');
expect(parsed.query['route-param']).toEqual('b');
})
);
it(
'should allow `cache-control` to be overwritten',
testFixture('now-dev-headers', async server => {
const res = await fetch(
`${server.address}/?name=cache-control&value=immutable`
);
expect(res.headers.get('cache-control')).toEqual('immutable');
})
);
it(
'should send `etag` header for static files',
testFixture('now-dev-headers', async server => {
const res = await fetch(`${server.address}/foo.txt`);
const expected =
process.platform === 'win32'
? '9dc423ab77c2e0446cd355256efff2ea1be27cbf'
: 'd263af8ab880c0b97eb6c5c125b5d44f9e5addd9';
expect(res.headers.get('etag')).toEqual(`"${expected}"`);
const body = await res.text();
expect(body.trim()).toEqual('hi');
})
);
it(
'should support default builds and routes',
testFixture('now-dev-default-builds-and-routes', async server => {
let podId: string;
let res = await fetch(`${server.address}/`);
validateResponseHeaders(res);
podId = res.headers.get('x-vercel-id')!.match(/:(\w+)-/)![1];
let body = await res.text();
expect(body.includes('hello, this is the frontend')).toBeTruthy();
res = await fetch(`${server.address}/api/users`);
validateResponseHeaders(res, podId);
body = await res.text();
expect(body).toEqual('users');
res = await fetch(`${server.address}/api/users/1`);
validateResponseHeaders(res, podId);
body = await res.text();
expect(body).toEqual('users/1');
res = await fetch(`${server.address}/api/welcome`);
validateResponseHeaders(res, podId);
body = await res.text();
expect(body).toEqual('hello and welcome');
})
);
it(
'should support `@vercel/static` routing',
testFixture('now-dev-static-routes', async server => {
const res = await fetch(`${server.address}/`);
expect(res.status).toEqual(200);
const body = await res.text();
expect(body.trim()).toEqual('<body>Hello!</body>');
})
);
if (process.platform !== 'win32') {
// This test is currently failing on Windows, so skip for now:
// > Creating initial build
// $ serve -l $PORT src
// 'serve' is not recognized as an internal or external command,
// https://github.com/vercel/vercel/pull/6638/checks?check_run_id=3449662836
it(
'should support `@vercel/static-build` routing',
testFixture('now-dev-static-build-routing', async server => {
const res = await fetch(`${server.address}/api/date`);
expect(res.status).toEqual(200);
const body = await res.text();
expect(body.startsWith('The current date:')).toBeTruthy();
})
);
}
it(
'should support directory listing',
testFixture('now-dev-directory-listing', async server => {
// Get directory listing
let res = await fetch(`${server.address}/`);
let body = await res.text();
expect(res.status).toEqual(200);
expect(body.includes('Index of')).toBeTruthy();
// Get a file
res = await fetch(`${server.address}/file.txt`);
body = await res.text();
expect(res.status).toEqual(200);
expect(body.trim()).toEqual('Hello from file!');
// Invoke a lambda
res = await fetch(`${server.address}/lambda.js`);
body = await res.text();
expect(res.status).toEqual(200);
expect(body).toEqual('Hello from Lambda!');
// Trigger a 404
res = await fetch(`${server.address}/does-not-exist`);
expect(res.status).toEqual(404);
})
);
it(
'should support `public` directory with zero config',
testFixture('now-dev-api-with-public', async server => {
let res = await fetch(`${server.address}/api/user`);
let body = await res.text();
expect(body).toEqual('hello:user');
res = await fetch(`${server.address}/`);
body = await res.text();
expect(body.startsWith('<h1>hello world</h1>')).toBeTruthy();
})
);
it(
'should support static files with zero config',
testFixture('now-dev-api-with-static', async server => {
let res = await fetch(`${server.address}/api/user`);
let body = await res.text();
expect(body).toEqual('bye:user');
res = await fetch(`${server.address}/`);
body = await res.text();
expect(body.startsWith('<h1>goodbye world</h1>')).toBeTruthy();
})
);
it(
'should respond with 404 listing with Accept header support',
testFixture('now-dev-directory-listing', async server => {
// HTML response
let res = await fetch(`${server.address}/does-not-exist`, {
headers: {
Accept: 'text/html',
},
});
expect(res.status).toEqual(404);
expect(res.headers.get('content-type')).toEqual(
'text/html; charset=utf-8'
);
let body = await res.text();
expect(body.startsWith('<!DOCTYPE html>')).toBeTruthy();
// JSON response
res = await fetch(`${server.address}/does-not-exist`, {
headers: {
Accept: 'application/json',
},
});
expect(res.status).toEqual(404);
expect(res.headers.get('content-type')).toEqual('application/json');
body = await res.text();
expect(body).toEqual(
'{"error":{"code":404,"message":"The page could not be found."}}\n'
);
// Plain text response
res = await fetch(`${server.address}/does-not-exist`);
expect(res.status).toEqual(404);
body = await res.text();
expect(res.headers.get('content-type')).toEqual(
'text/plain; charset=utf-8'
);
expect(body).toEqual('The page could not be found.\n\nNOT_FOUND\n');
})
);
it(
'should support custom 404 routes',
testFixture('now-dev-custom-404', async server => {
// Test custom 404 with static dest
let res = await fetch(`${server.address}/error.html`);
expect(res.status).toEqual(404);
let body = await res.text();
expect(body.trim()).toEqual('<div>Custom 404 page</div>');
// Test custom 404 with lambda dest
res = await fetch(`${server.address}/error.js`);
expect(res.status).toEqual(404);
body = await res.text();
expect(body).toEqual('Custom 404 Lambda\n');
// Test regular 404 still works
res = await fetch(`${server.address}/does-not-exist`);
expect(res.status).toEqual(404);
body = await res.text();
expect(body).toEqual('The page could not be found.\n\nNOT_FOUND\n');
})
);
});

View File

@@ -0,0 +1,256 @@
import { validateConfig } from '../../../src/util/dev/validate';
describe('validateConfig', () => {
it('should not error with empty config', async () => {
const config = {};
const error = validateConfig(config);
expect(error).toBeNull();
});
it('should not error with complete config', async () => {
const config = {
version: 2,
public: true,
regions: ['sfo1', 'iad1'],
cleanUrls: true,
headers: [{ source: '/', headers: [{ key: 'x-id', value: '123' }] }],
rewrites: [{ source: '/help', destination: '/support' }],
redirects: [{ source: '/kb', destination: 'https://example.com' }],
trailingSlash: false,
functions: { 'api/user.go': { memory: 128, maxDuration: 5 } },
};
const error = validateConfig(config);
expect(error).toBeNull();
});
it('should not error with builds and routes', async () => {
const config = {
builds: [{ src: 'api/index.js', use: '@vercel/node' }],
routes: [{ src: '/(.*)', dest: '/api/index.js' }],
};
const error = validateConfig(config);
expect(error).toBeNull();
});
it('should error with invalid rewrites due to additional property and offer suggestion', async () => {
const error = validateConfig({
// @ts-ignore
rewrites: [{ src: '/(.*)', dest: '/api/index.js' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `rewrites[0]` should NOT have additional property `src`. Did you mean `source`?'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/rewrites'
);
});
it('should error with invalid routes due to additional property and offer suggestion', async () => {
const error = validateConfig({
// @ts-ignore
routes: [{ source: '/(.*)', destination: '/api/index.js' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `routes[0]` should NOT have additional property `source`. Did you mean `src`?'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/routes'
);
});
it('should error with invalid routes array type', async () => {
const error = validateConfig({
// @ts-ignore
routes: { src: '/(.*)', dest: '/api/index.js' },
});
expect(error!.message).toEqual(
'Invalid vercel.json - `routes` should be array.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/routes'
);
});
it('should error with invalid redirects array object', async () => {
const error = validateConfig({
redirects: [
// @ts-ignore
{
/* intentionally empty */
},
],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `redirects[0]` missing required property `source`.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/redirects'
);
});
it('should error with invalid redirects.permanent poperty', async () => {
const error = validateConfig({
// @ts-ignore
redirects: [{ source: '/', destination: '/go', permanent: 'yes' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `redirects[0].permanent` should be boolean.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/redirects'
);
});
it('should error with invalid cleanUrls type', async () => {
const error = validateConfig({
// @ts-ignore
cleanUrls: 'true',
});
expect(error!.message).toEqual(
'Invalid vercel.json - `cleanUrls` should be boolean.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/cleanurls'
);
});
it('should error with invalid trailingSlash type', async () => {
const error = validateConfig({
// @ts-ignore
trailingSlash: [true],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `trailingSlash` should be boolean.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/trailingslash'
);
});
it('should error with invalid headers property', async () => {
const error = validateConfig({
// @ts-ignore
headers: [{ 'Content-Type': 'text/html' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `headers[0]` should NOT have additional property `Content-Type`. Please remove it.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/headers'
);
});
it('should error with invalid headers.source type', async () => {
const error = validateConfig({
// @ts-ignore
headers: [{ source: [{ 'Content-Type': 'text/html' }] }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `headers[0].source` should be string.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/headers'
);
});
it('should error with invalid headers additional property', async () => {
const error = validateConfig({
// @ts-ignore
headers: [{ source: '/', stuff: [{ 'Content-Type': 'text/html' }] }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `headers[0]` should NOT have additional property `stuff`. Please remove it.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/headers'
);
});
it('should error with invalid headers wrong nested headers type', async () => {
const error = validateConfig({
// @ts-ignore
headers: [{ source: '/', headers: [{ 'Content-Type': 'text/html' }] }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `headers[0].headers[0]` should NOT have additional property `Content-Type`. Please remove it.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/headers'
);
});
it('should error with invalid headers wrong nested headers additional property', async () => {
const error = validateConfig({
headers: [
// @ts-ignore
{ source: '/', headers: [{ key: 'Content-Type', val: 'text/html' }] },
],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `headers[0].headers[0]` should NOT have additional property `val`. Please remove it.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/headers'
);
});
it('should error with too many redirects', async () => {
const error = validateConfig({
redirects: Array.from({ length: 5000 }).map((_, i) => ({
source: `/${i}`,
destination: `/v/${i}`,
})),
});
expect(error!.message).toEqual(
'Invalid vercel.json - `redirects` should NOT have more than 1024 items.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/redirects'
);
});
it('should error with too many nested headers', async () => {
const error = validateConfig({
headers: [
{
source: '/',
headers: [{ key: `x-id`, value: `123` }],
},
{
source: '/too-many',
headers: Array.from({ length: 5000 }).map((_, i) => ({
key: `${i}`,
value: `${i}`,
})),
},
],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `headers[1].headers` should NOT have more than 1024 items.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/configuration#project/headers'
);
});
it('should error with "functions" and "builds"', async () => {
const error = validateConfig({
builds: [
{
src: 'index.html',
use: '@vercel/static',
},
],
functions: {
'api/test.js': {
memory: 1024,
},
},
});
expect(error!.message).toEqual(
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.'
);
expect(error!.link).toEqual('https://vercel.link/functions-and-builds');
});
});

View File

@@ -0,0 +1,189 @@
import fetch from 'node-fetch';
import listen from 'async-listen';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { JSONValue } from '../../src/types';
import { responseError, responseErrorMessage } from '../../src/util/error';
const send = (res: ServerResponse, statusCode: number, body: JSONValue) => {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'application/json; charset=utf8');
res.end(JSON.stringify(body));
};
describe('responseError', () => {
let url: string;
let server: Server;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let handler = (_req: IncomingMessage, _res: ServerResponse) => {};
beforeAll(async () => {
server = createServer((req, res) => handler(req, res));
url = await listen(server);
});
afterAll(() => {
server.close();
});
it('should parse 4xx response error with fallback message', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 404, {});
};
const res = await fetch(url);
const formatted = await responseError(res, 'Failed to load data');
expect(formatted.message).toEqual('Failed to load data (404)');
});
it('should parse 4xx response error without fallback message', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 404, {});
};
const res = await fetch(url);
const formatted = await responseError(res);
expect(formatted.message).toEqual('Response Error (404)');
});
it('should parse 5xx response error without fallback message', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 500, '');
};
const res = await fetch(url);
const formatted = await responseError(res);
expect(formatted.message).toEqual('Response Error (500)');
});
it('should parse 4xx response error as correct JSON', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 400, {
error: {
message: 'The request is not correct',
},
});
};
const res = await fetch(url);
const formatted = await responseError(res);
expect(formatted.message).toEqual('The request is not correct (400)');
});
it('should parse 5xx response error as HTML', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 500, 'This is a malformed error');
};
const res = await fetch(url);
const formatted = await responseError(res, 'Failed to process data');
expect(formatted.message).toEqual('Failed to process data (500)');
});
it('should parse 5xx response error with random JSON', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 500, {
wrong: 'property',
});
};
const res = await fetch(url);
const formatted = await responseError(res, 'Failed to process data');
expect(formatted.message).toEqual('Failed to process data (500)');
});
it('should parse 4xx error message with broken JSON', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 403, `32puuuh2332`);
};
const res = await fetch(url);
const formatted = await responseErrorMessage(res, 'Not authenticated');
expect(formatted).toEqual('Not authenticated (403)');
});
it('should parse 4xx error message with proper message', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 403, {
error: {
message: 'This is a test',
},
});
};
const res = await fetch(url);
const formatted = await responseErrorMessage(res);
expect(formatted).toEqual('This is a test (403)');
});
it('should parse 5xx error message with proper message', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 500, {
error: {
message: 'This is a test',
},
});
};
const res = await fetch(url);
const formatted = await responseErrorMessage(res);
expect(formatted).toEqual('Response Error (500)');
});
it('should parse 4xx response error with broken JSON', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 403, `122{"sss"`);
};
const res = await fetch(url);
const formatted = await responseError(res, 'Not authenticated');
expect(formatted.message).toEqual('Not authenticated (403)');
});
it('should parse 4xx response error as correct JSON with more properties', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 403, {
error: {
message: 'The request is not correct',
additionalProperty: 'test',
},
});
};
const res = await fetch(url);
const formatted = await responseError(res);
expect(formatted.message).toEqual('The request is not correct (403)');
expect(formatted.additionalProperty).toEqual('test');
});
it('should parse 429 response error with retry header', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
res.setHeader('Retry-After', '20');
send(res, 429, {
error: {
message: 'You were rate limited',
},
});
};
const res = await fetch(url);
const formatted = await responseError(res);
expect(formatted.message).toEqual('You were rate limited (429)');
expect(formatted.retryAfter).toEqual(20);
});
it('should parse 429 response error without retry header', async () => {
handler = (_req: IncomingMessage, res: ServerResponse) => {
send(res, 429, {
error: {
message: 'You were rate limited',
},
});
};
const res = await fetch(url);
const formatted = await responseError(res);
expect(formatted.message).toEqual('You were rate limited (429)');
expect(formatted.retryAfter).toEqual(undefined);
});
});

View File

@@ -0,0 +1,53 @@
import { join, sep } from 'path';
// @ts-ignore - Missing types for "alpha-sort"
import { asc as alpha } from 'alpha-sort';
import createOutput from '../../src/util/output';
import { staticFiles as getStaticFiles_ } from '../../src/util/get-files';
const output = createOutput({ debug: false });
const prefix = `${join(__dirname, '../fixtures/unit')}${sep}`;
const base = (path: string) => path.replace(prefix, '');
const fixture = (name: string) => join(prefix, name);
const getStaticFiles = async (dir: string) => {
const files = await getStaticFiles_(dir, {
output,
});
return normalizeWindowsPaths(files);
};
const normalizeWindowsPaths = (files: string[]) => {
if (process.platform === 'win32') {
const prefix = 'D:/a/vercel/vercel/packages/cli/test/fixtures/unit/';
return files.map(f => f.replace(/\\/g, '/').slice(prefix.length));
}
return files;
};
describe('staticFiles', () => {
it('should discover files for builds deployment', async () => {
const path = 'now-json-static-no-files';
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
expect(files).toHaveLength(4);
expect(base(files[0])).toEqual(`${path}/a.js`);
expect(base(files[1])).toEqual(`${path}/b.js`);
expect(base(files[2])).toEqual(`${path}/build/a/c.js`);
expect(base(files[3])).toEqual(`${path}/package.json`);
});
it('should respect `.vercelignore` file rules', async () => {
const path = 'vercelignore';
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
expect(files).toHaveLength(6);
expect(base(files[0])).toEqual(`${path}/.vercelignore`);
expect(base(files[1])).toEqual(`${path}/a.js`);
expect(base(files[2])).toEqual(`${path}/build/sub/a.js`);
expect(base(files[3])).toEqual(`${path}/build/sub/c.js`);
expect(base(files[4])).toEqual(`${path}/c.js`);
expect(base(files[5])).toEqual(`${path}/package.json`);
});
});

View File

@@ -0,0 +1,47 @@
import getProjectName from '../../src/util/get-project-name';
describe('getProjectName', () => {
it('should work with argv', () => {
const project = getProjectName({
argv: {
'--name': 'abc',
},
});
expect(project).toEqual('abc');
});
it('should work with now.json', () => {
const project = getProjectName({
argv: {},
nowConfig: { name: 'abc' },
});
expect(project).toEqual('abc');
});
it('should work with a file', () => {
const project = getProjectName({
argv: {},
nowConfig: {},
isFile: true,
});
expect(project).toEqual('files');
});
it('should work with a multiple files', () => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png'],
});
expect(project).toEqual('files');
});
it('should work with a directory', () => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa'],
});
expect(project).toEqual('aa');
});
});

View File

@@ -0,0 +1,11 @@
import { isCanary } from '../../src/util/is-canary';
import getUpdateCommand from '../../src/util/get-update-command';
describe('getUpdateCommand', () => {
it('should detect update command', async () => {
const updateCommand = await getUpdateCommand();
expect(updateCommand).toEqual(
`yarn add vercel@${isCanary() ? 'canary' : 'latest'}`
);
});
});

View File

@@ -0,0 +1,54 @@
import didYouMean from '../../../src/util/init/did-you-mean';
describe('didYouMean', () => {
const examples = [
'apollo',
'create-react-app',
'docz',
'gatsby',
'go',
'gridsome',
'html-minifier',
'mdx-deck',
'monorepo',
'nextjs',
'nextjs-news',
'nextjs-static',
'node-server',
'nodejs',
'nodejs-canvas-partyparrot',
'nodejs-coffee',
'nodejs-express',
'nodejs-hapi',
'nodejs-koa',
'nodejs-koa-ts',
'nodejs-pdfkit',
'nuxt-static',
'optipng',
'php-7',
'puppeteer-screenshot',
'python',
'redirect',
'serverless-ssr-reddit',
'static',
'vue',
'vue-ssr',
'vuepress',
];
it('should guess "mdx-deck"', () => {
expect(didYouMean('md', examples, 0.7)).toEqual('mdx-deck');
});
it('should guess "nodejs-koa"', () => {
expect(didYouMean('koa', examples, 0.7)).toEqual('nodejs-koa');
});
it('should guess "nodejs"', () => {
expect(didYouMean('node', examples, 0.7)).toEqual('nodejs');
});
it('should fail to guess with bad input', () => {
expect(didYouMean('12345', examples, 0.7)).toBeUndefined();
});
});

View File

@@ -0,0 +1,21 @@
import { isValidName } from '../../src/util/is-valid-name';
const tests = {
'hello world': true,
käse: true,
ねこ: true,
'/': false,
'/#': false,
'//': false,
'/ねこ': true,
привет: true,
'привет#': true,
};
describe('isValidName', () => {
for (const [value, expected] of Object.entries(tests)) {
it(`should detect "${value}" as \`${expected}\``, () => {
expect(isValidName(value)).toEqual(expected);
});
}
});

View File

@@ -0,0 +1,32 @@
import { basename, join } from 'path';
import { getVercelDirectory } from '../../../src/util/projects/link';
const fixture = (name: string) => join(__dirname, '../../fixtures/unit', name);
describe('getVercelDirectory', () => {
it('should return ".vercel"', () => {
const cwd = fixture('get-vercel-directory');
const dir = getVercelDirectory(cwd);
expect(basename(dir)).toEqual('.vercel');
});
it('should return ".now"', () => {
const cwd = fixture('get-vercel-directory-legacy');
const dir = getVercelDirectory(cwd);
expect(basename(dir)).toEqual('.now');
});
it('should throw an error if both ".vercel" and ".now" exist', () => {
let err: Error;
const cwd = fixture('get-vercel-directory-error');
try {
getVercelDirectory(cwd);
throw new Error('Should not happen');
} catch (_err) {
err = _err;
}
expect(err.message).toEqual(
'Both `.vercel` and `.now` directories exist. Please remove the `.now` directory.'
);
});
});

View File

@@ -0,0 +1,35 @@
import toHost from '../../src/util/to-host';
describe('toHost', () => {
it('should parse simple to host', () => {
expect(toHost('vercel.com')).toEqual('vercel.com');
});
it('should parse leading // to host', () => {
expect(toHost('//zeit-logos-rnemgaicnc.now.sh')).toEqual(
'zeit-logos-rnemgaicnc.now.sh'
);
});
it('should parse leading http:// to host', () => {
expect(toHost('http://zeit-logos-rnemgaicnc.now.sh')).toEqual(
'zeit-logos-rnemgaicnc.now.sh'
);
});
it('should parse leading https:// to host', () => {
expect(toHost('https://zeit-logos-rnemgaicnc.now.sh')).toEqual(
'zeit-logos-rnemgaicnc.now.sh'
);
});
it('should parse leading https:// and path to host', () => {
expect(toHost('https://zeit-logos-rnemgaicnc.now.sh/path')).toEqual(
'zeit-logos-rnemgaicnc.now.sh'
);
});
it('should parse simple and path to host', () => {
expect(toHost('vercel.com/test')).toEqual('vercel.com');
});
});

View File

@@ -12,7 +12,7 @@
"resolveJsonModule": true,
"sourceMap": true,
"outDir": "./dist",
"typeRoots": ["./@types", "./node_modules/@types"]
"typeRoots": ["./types", "./node_modules/@types"]
},
"include": ["src/**/*"]
"include": ["./types", "src/**/*"]
}

View File

@@ -0,0 +1,3 @@
declare module 'epipebomb' {
export default function (): void;
}

View File

@@ -0,0 +1,11 @@
declare module 'intercept-stdout' {
export default function (fn?: InterceptFn): UnhookIntercept;
}
interface InterceptFn {
(text: string): string | void;
}
interface UnhookIntercept {
(): void;
}

View File

@@ -3,7 +3,7 @@ declare module 'is-port-reachable' {
timeout?: number | undefined;
host?: string;
}
export default function(
export default function (
port: number | undefined,
options?: IsPortReachableOptions
): Promise<boolean>;

View File

@@ -0,0 +1,7 @@
declare module 'promisepipe' {
export default function (
...streams: Array<
NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream
>
): Promise<void>;
}

View File

@@ -1,7 +1,7 @@
declare module 'serve-handler' {
import http from 'http';
export default function(
export default function (
request: http.IncomingMessage,
response: http.ServerResponse,
options: serveHandler.Options
@@ -59,5 +59,5 @@ declare module 'serve-handler/src/directory' {
directory: string;
}
export default function(spec: Spec): string;
export default function (spec: Spec): string;
}

View File

@@ -0,0 +1,3 @@
declare module '@zeit/source-map-support' {
function install(): void;
}

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