mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-12 12:57:47 +00:00
Compare commits
37 Commits
@vercel/py
...
@vercel/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e52f8532b | ||
|
|
702cb9e29c | ||
|
|
d3d5555d79 | ||
|
|
2fd3fc73e5 | ||
|
|
de0b13a46e | ||
|
|
d0fe85db92 | ||
|
|
bfbd927320 | ||
|
|
90bacf88b8 | ||
|
|
07c369c542 | ||
|
|
a2e4186ccb | ||
|
|
6e1d708e3f | ||
|
|
38503103c3 | ||
|
|
e8fec4b69c | ||
|
|
b3ffcdf80d | ||
|
|
43c1a93c1d | ||
|
|
5b118fd4e6 | ||
|
|
8916b674af | ||
|
|
1807f83c69 | ||
|
|
74e8ec7c64 | ||
|
|
2644e3127b | ||
|
|
d77ac04b0c | ||
|
|
0ef9c8df4d | ||
|
|
dfc4c98820 | ||
|
|
0e51884725 | ||
|
|
1b264fe60e | ||
|
|
f18bca9718 | ||
|
|
c23dc73f41 | ||
|
|
273718e0b7 | ||
|
|
230b88bf9b | ||
|
|
676a3d2568 | ||
|
|
f221f041d0 | ||
|
|
aca42b2aac | ||
|
|
cf11a8efb5 | ||
|
|
be09349daf | ||
|
|
a01372bcbb | ||
|
|
b941715d7b | ||
|
|
ee9a8a0415 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Bug Report
|
||||
url: https://vercel.com/support/request
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
13218
examples/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,4 +100,4 @@ class FileFsRef implements File {
|
||||
}
|
||||
}
|
||||
|
||||
export = FileFsRef;
|
||||
export default FileFsRef;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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([
|
||||
|
||||
11
packages/cli/@types/intercept-stdout/index.d.ts
vendored
11
packages/cli/@types/intercept-stdout/index.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
declare module 'intercept-stdout' {
|
||||
export default function (fn?: InterceptFn): UnhookIntercept
|
||||
}
|
||||
|
||||
interface InterceptFn {
|
||||
(text: string): string | void
|
||||
}
|
||||
|
||||
interface UnhookIntercept {
|
||||
(): void
|
||||
}
|
||||
5
packages/cli/@types/promisepipe/index.d.ts
vendored
5
packages/cli/@types/promisepipe/index.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
declare module 'promisepipe' {
|
||||
export default function (
|
||||
...streams: Array<NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream>
|
||||
): Promise<void>
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -10,7 +10,6 @@ export default new Map([
|
||||
['dns', 'dns'],
|
||||
['domain', 'domains'],
|
||||
['domains', 'domains'],
|
||||
['downgrade', 'upgrade'],
|
||||
['env', 'env'],
|
||||
['help', 'help'],
|
||||
['init', 'init'],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -23,7 +23,6 @@ export { VercelConfig };
|
||||
|
||||
export interface DevServerOptions {
|
||||
output: Output;
|
||||
debug: boolean;
|
||||
devCommand?: string;
|
||||
frameworkSlug?: string;
|
||||
projectSettings?: ProjectSettings;
|
||||
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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') {
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -22,6 +22,5 @@ export default async function responseError(
|
||||
}
|
||||
|
||||
const msg = bodyError?.message || fallbackMessage || 'Response Error';
|
||||
|
||||
return new APIError(msg, res, bodyError);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
13
packages/cli/src/util/teams/create-team.ts
Normal file
13
packages/cli/src/util/teams/create-team.ts
Normal 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;
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
69
packages/cli/src/util/teams/get-teams.ts
Normal file
69
packages/cli/src/util/teams/get-teams.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
packages/cli/src/util/teams/invite-user-to-team.ts
Normal file
23
packages/cli/src/util/teams/invite-user-to-team.ts
Normal 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;
|
||||
}
|
||||
17
packages/cli/src/util/teams/patch-team.ts
Normal file
17
packages/cli/src/util/teams/patch-team.ts
Normal 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;
|
||||
}
|
||||
30
packages/cli/test/commands/inspect.test.ts
Normal file
30
packages/cli/test/commands/inspect.test.ts
Normal 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`
|
||||
);
|
||||
});
|
||||
});
|
||||
16
packages/cli/test/commands/login.test.ts
Normal file
16
packages/cli/test/commands/login.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
20
packages/cli/test/commands/whoami.test.ts
Normal file
20
packages/cli/test/commands/whoami.test.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
264
packages/cli/test/dev-builder.unit.js
vendored
264
packages/cli/test/dev-builder.unit.js
vendored
@@ -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);
|
||||
}
|
||||
});
|
||||
325
packages/cli/test/dev-router.unit.js
vendored
325
packages/cli/test/dev-router.unit.js
vendored
@@ -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,
|
||||
});
|
||||
});
|
||||
500
packages/cli/test/dev-server.unit.js
vendored
500
packages/cli/test/dev-server.unit.js
vendored
@@ -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(/"/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:');
|
||||
});
|
||||
283
packages/cli/test/dev-validate.unit.js
vendored
283
packages/cli/test/dev-validate.unit.js
vendored
@@ -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');
|
||||
});
|
||||
2
packages/cli/test/integration.js
vendored
2
packages/cli/test/integration.js
vendored
@@ -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)
|
||||
);
|
||||
|
||||
109
packages/cli/test/mocks/client.ts
Normal file
109
packages/cli/test/mocks/client.ts
Normal 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();
|
||||
});
|
||||
80
packages/cli/test/mocks/deployment.ts
Normal file
80
packages/cli/test/mocks/deployment.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
21
packages/cli/test/mocks/user.ts
Normal file
21
packages/cli/test/mocks/user.ts
Normal 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
4
packages/cli/test/tsconfig.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*.test.ts"]
|
||||
}
|
||||
485
packages/cli/test/unit.js
vendored
485
packages/cli/test/unit.js
vendored
@@ -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.'
|
||||
);
|
||||
});
|
||||
301
packages/cli/test/util/dev/builder-cache.test.ts
Normal file
301
packages/cli/test/util/dev/builder-cache.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
61
packages/cli/test/util/dev/parse-listen.test.ts
Normal file
61
packages/cli/test/util/dev/parse-listen.test.ts
Normal 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:');
|
||||
});
|
||||
});
|
||||
326
packages/cli/test/util/dev/router.test.ts
Normal file
326
packages/cli/test/util/dev/router.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
338
packages/cli/test/util/dev/server.test.ts
Normal file
338
packages/cli/test/util/dev/server.test.ts
Normal 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(/"/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');
|
||||
})
|
||||
);
|
||||
});
|
||||
256
packages/cli/test/util/dev/validate.test.ts
Normal file
256
packages/cli/test/util/dev/validate.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
189
packages/cli/test/util/error.test.ts
Normal file
189
packages/cli/test/util/error.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
53
packages/cli/test/util/get-files.test.ts
Normal file
53
packages/cli/test/util/get-files.test.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
47
packages/cli/test/util/get-project-name.test.ts
Normal file
47
packages/cli/test/util/get-project-name.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
11
packages/cli/test/util/get-update-command.test.ts
Normal file
11
packages/cli/test/util/get-update-command.test.ts
Normal 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'}`
|
||||
);
|
||||
});
|
||||
});
|
||||
54
packages/cli/test/util/init/did-you-mean.test.ts
Normal file
54
packages/cli/test/util/init/did-you-mean.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
21
packages/cli/test/util/is-valid-name.test.ts
Normal file
21
packages/cli/test/util/is-valid-name.test.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
32
packages/cli/test/util/projects/get-vercel-directory.test.ts
Normal file
32
packages/cli/test/util/projects/get-vercel-directory.test.ts
Normal 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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
35
packages/cli/test/util/to-host.test.ts
Normal file
35
packages/cli/test/util/to-host.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
|
||||
3
packages/cli/types/epipebomb/index.d.ts
vendored
Normal file
3
packages/cli/types/epipebomb/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'epipebomb' {
|
||||
export default function (): void;
|
||||
}
|
||||
11
packages/cli/types/intercept-stdout/index.d.ts
vendored
Normal file
11
packages/cli/types/intercept-stdout/index.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
declare module 'intercept-stdout' {
|
||||
export default function (fn?: InterceptFn): UnhookIntercept;
|
||||
}
|
||||
|
||||
interface InterceptFn {
|
||||
(text: string): string | void;
|
||||
}
|
||||
|
||||
interface UnhookIntercept {
|
||||
(): void;
|
||||
}
|
||||
@@ -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>;
|
||||
7
packages/cli/types/promisepipe/index.d.ts
vendored
Normal file
7
packages/cli/types/promisepipe/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module 'promisepipe' {
|
||||
export default function (
|
||||
...streams: Array<
|
||||
NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream
|
||||
>
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
3
packages/cli/types/zeit__source-map-support/index.d.ts
vendored
Normal file
3
packages/cli/types/zeit__source-map-support/index.d.ts
vendored
Normal 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
Reference in New Issue
Block a user