Compare commits

...

21 Commits

Author SHA1 Message Date
Vercel Release Bot
4cd77608e8 Version Packages (#10020)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-01 10:09:24 -05:00
Steven
e6b2980eba [tests] Fix summary workflow to always run (#10048)
### Description
This ensures the Summary job always runs to check if any test failed and
cause itself to fail as well.

This allows us to dynamically add as many concurrent jobs as we want but
only have one (Summary) marked as required.

### Testing
I verified this worked by making a change to `@vercel/static-config`
which is not required and the Summary check failed:

https://github.com/vercel/vercel/actions/runs/5136560652/jobs/9243924596

Then I reverted it so that the test was passing and the Summary check
passed:

https://github.com/vercel/vercel/actions/runs/5137401343/jobs/9245554297
2023-05-31 18:24:54 -04:00
Nathan Rajlich
67e20a6ede [cli] Add repo linking support for deploy command (#10013)
Adds support for `vercel deploy` command when the repository has been linked via `vercel link --repo`.
2023-05-31 18:07:30 +00:00
JJ Kasper
c63679ea0a Revert "[next] Update rsc content-type test fixtures" (#10040)
Relies on https://github.com/vercel/next.js/pull/50472 this should only be merged after the Next.js PR is released to canary

Reverts vercel/vercel#10023
2023-05-31 15:38:22 +00:00
Damien Simonin Feugas
4280166df4 docs(sveltekit): re-introduce speed insights (#9988) 2023-05-31 09:41:46 +02:00
Nathan Rajlich
18ae78137c Fetch git tags during Release workflow (#10045)
Follow-up to #10044. The git tags need to be present for `changeset tag` to work properly.
2023-05-30 21:10:03 +00:00
Nathan Rajlich
ebe4058073 Use pnpm publish -r and changeset tag to publish packages (#10044)
This is a re-application of #10022 (which was reverted in #10032), but with the addition of `changeset tag` after the `pnpm publish -r` command. This ensures the proper git tags are also created, allowing for the GitHub releases to be published.
2023-05-30 19:25:24 +00:00
Vercel Release Bot
942e76840e [tests] Upgrade Turbo to version 1.9.9 (#10036)
This auto-generated PR updates Turbo to version 1.9.9
2023-05-29 20:31:12 +00:00
Nathan Rajlich
57515d2d07 [cli] Fix link subcommand unit tests on Windows (#10033)
Follow-up to #10031 which broke the `link` unit tests on Windows.

For some reason Kodiak was a bad bot and merged the PR with failing tests <picture data-single-emoji=":rarityannoyed:" title=":rarityannoyed:"><img class="emoji" src="https://emoji.slack-edge.com/T0CAQ00TU/rarityannoyed/b62f8c87a5fb7239.png" alt=":rarityannoyed:" width="20" height="auto" align="absmiddle"></picture> 

_Note:_ Probably easier to review with [whitespace hidden](https://github.com/vercel/vercel/pull/10033/files?w=1).
2023-05-26 21:27:19 +00:00
Nathan Rajlich
ef30a46c03 [cli] Add client.cwd to unify all working directory related logic (#10031)
A few commands were still checking on `--cwd` explicitly, which is incorrect since the entrypoint file already handles the directory change.

The new `client.cwd` property is a helper to make writing tests easier. Tests no longer need to `chdir()` explicitly and then revert afterwards.
2023-05-26 20:42:03 +00:00
Nathan Rajlich
113b8ac87b Revert "Use pnpm publish -r to publish packages (#10022)" (#10032)
This reverts commit 1e47bbf32f.

`changeset publish` also creates git tags, whereas `pnpm publish -r` does not. This causes the GitHub Releases to not be created.
2023-05-26 20:18:49 +00:00
Shohei Maeda
b56ac2717d [next] Pass pageExtensions data to apiLambdaGroups (#10015)
Right now, we can't detect API routes correctly if `pageExtensions` is set:

> WARNING: Unable to find source file for page xxxx with extensions: js, jsx, ts, tsx, this can cause functions config from `vercel.json` to not be applied
2023-05-26 18:01:05 +00:00
Vercel Release Bot
aa8957ab10 [examples][tests] Upgrade Next.js to version 13.4.4 (#10025)
This auto-generated PR updates 3 packages to Next.js version 13.4.4
2023-05-26 10:54:42 -04:00
Shohei Maeda
c6c19354e8 [next] Fix functions config with App Router (#9889)
We added appDir support in https://github.com/vercel/vercel/pull/9811 so that users can customize `memory`/`maxDuration` for routes in appDir.

But since RSC is enabled by default in Next.js 13, `vercel build` still reports some warning messages, such as:

```json
"functions": {
  "app/**/*": {
    "maxDuration": 5,
    "memory": 512
  }
}
```
```
WARNING: Unable to find source file for page hello.js with extensions: ts, tsx, js, jsx, md, mdx, this can cause functions config from `vercel.json` to not be applied
WARNING: Unable to find source file for page index.js with extensions: ts, tsx, js, jsx, md, mdx, this can cause functions config from `vercel.json` to not be applied
```

To suppress these errors and properly apply `functions` setting to those routes, updating the current detection logic to also search `page.${ext}` files.
2023-05-25 21:01:29 +00:00
JJ Kasper
96b2502133 [next] Update rsc content-type test fixtures (#10023)
This updates the tests to the latest expected content-type header for RSC responses. 

x-ref: https://github.com/vercel/next.js/pull/50314
2023-05-25 08:32:53 +00:00
Swarnava Sengupta
2df0262675 [examples] Update preact template to use Node 18 (#10012) 2023-05-24 15:06:01 -07:00
Nathan Rajlich
1e47bbf32f Use pnpm publish -r to publish packages (#10022)
Following the instructions here: https://pnpm.io/using-changesets

This should ensure that packages are published in the correct order (see related issue: https://github.com/changesets/changesets/issues/238).
2023-05-24 21:56:45 +00:00
Nathan Rajlich
00813a3945 [cli] Add expect dev dependency to fix type error in toOutput (#10021)
Also make the timeout print what was buffered to help with debugging failed tests.
2023-05-24 21:35:33 +00:00
Chris Barber
a73ec6343f [cli] Clean up 'vc rollback' (#10019)
This PR removes dependency on the deprecated `lastRollbackTarget` project property and adopts many of the code conventions used in `vc promote`.

Important! Please merge #9984 first!
2023-05-24 19:10:28 +00:00
Chris Barber
4bd70d4b6e [cli] New vc promote command (#9984)
~This PR is blocked by https://github.com/vercel/api/pull/19508.~

Linear: https://linear.app/vercel/issue/VCCLI-262/cli-new-command-to-promote-deployment
2023-05-24 17:22:11 +00:00
Javi Velasco
c7bcea4081 Remove usage of env from Edge Functions and Middleware (#10018)
Co-authored-by: Steven <steven@ceriously.com>
2023-05-24 18:22:43 +02:00
114 changed files with 7825 additions and 6296 deletions

View File

@@ -23,6 +23,9 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Fetch git tags
run: git fetch origin 'refs/tags/*:refs/tags/*'
- name: Setup Node
uses: actions/setup-node@v3
with:
@@ -47,8 +50,8 @@ jobs:
id: changesets
uses: changesets/action@v1
with:
version: pnpm version:prepare
publish: pnpm release
version: pnpm ci:version
publish: pnpm ci:publish
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
NPM_CONFIG_PROVENANCE: 'true'

View File

@@ -97,11 +97,21 @@ jobs:
if: matrix.runner != 'windows-latest'
run: echo | openssl s_client -showcerts -servername 'api.vercel.com' -connect 76.76.21.21:443
conclusion:
summary:
name: Summary
runs-on: ubuntu-latest
timeout-minutes: 5
if: always()
needs:
- test
runs-on: ubuntu-latest
name: E2E
steps:
- name: Done
run: echo "Done."
- name: Check All
run: |-
for status in ${{ join(needs.*.result, ' ') }}
do
if [ "$status" != "success" ] && [ "$status" != "skipped" ]
then
echo "Some checks failed"
exit 1
fi
done

View File

@@ -8,9 +8,9 @@
"name": "nextjs",
"version": "0.1.0",
"dependencies": {
"eslint": "8.40.0",
"eslint-config-next": "13.4.3",
"next": "13.4.3",
"eslint": "8.41.0",
"eslint-config-next": "13.4.4",
"next": "13.4.4",
"react": "18.2.0",
"react-dom": "18.2.0"
}
@@ -71,9 +71,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz",
"integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==",
"version": "8.41.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -109,22 +109,22 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"node_modules/@next/env": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.3.tgz",
"integrity": "sha512-pa1ErjyFensznttAk3EIv77vFbfSYT6cLzVRK5jx4uiRuCQo+m2wCFAREaHKIy63dlgvOyMlzh6R8Inu8H3KrQ=="
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.4.tgz",
"integrity": "sha512-q/y7VZj/9YpgzDe64Zi6rY1xPizx80JjlU2BTevlajtaE3w1LqweH1gGgxou2N7hdFosXHjGrI4OUvtFXXhGLg=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.3.tgz",
"integrity": "sha512-5B0uOnh7wyUY9vNNdIA6NUvWozhrZaTMZOzdirYAefqD0ZBK5C/h3+KMYdCKrR7JrXGvVpWnHtv54b3dCzwICA==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.4.tgz",
"integrity": "sha512-5jnh7q6I15efnjR/rR+/TGTc9hn53g3JTbEjAMjmeQiExKqEUgIXqrHI5zlTNlNyzCPkBB860/ctxXheZaF2Vw==",
"dependencies": {
"glob": "7.1.7"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.3.tgz",
"integrity": "sha512-yx18udH/ZmR4Bw4M6lIIPE3JxsAZwo04iaucEfA2GMt1unXr2iodHUX/LAKNyi6xoLP2ghi0E+Xi1f4Qb8f1LQ==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.4.tgz",
"integrity": "sha512-xfjgXvp4KalNUKZMHmsFxr1Ug+aGmmO6NWP0uoh4G3WFqP/mJ1xxfww0gMOeMeSq/Jyr5k7DvoZ2Pv+XOITTtw==",
"cpu": [
"arm64"
],
@@ -137,9 +137,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.3.tgz",
"integrity": "sha512-Mi8xJWh2IOjryAM1mx18vwmal9eokJ2njY4nDh04scy37F0LEGJ/diL6JL6kTXi0UfUCGbMsOItf7vpReNiD2A==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.4.tgz",
"integrity": "sha512-ZY9Ti1hkIwJsxGus3nlubIkvYyB0gNOYxKrfsOrLEqD0I2iCX8D7w8v6QQZ2H+dDl6UT29oeEUdDUNGk4UEpfg==",
"cpu": [
"x64"
],
@@ -152,9 +152,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.3.tgz",
"integrity": "sha512-aBvtry4bxJ1xwKZ/LVPeBGBwWVwxa4bTnNkRRw6YffJnn/f4Tv4EGDPaVeYHZGQVA56wsGbtA6nZMuWs/EIk4Q==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.4.tgz",
"integrity": "sha512-+KZnDeMShYkpkqAvGCEDeqYTRADJXc6SY1jWXz+Uo6qWQO/Jd9CoyhTJwRSxvQA16MoYzvILkGaDqirkRNctyA==",
"cpu": [
"arm64"
],
@@ -167,9 +167,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.3.tgz",
"integrity": "sha512-krT+2G3kEsEUvZoYte3/2IscscDraYPc2B+fDJFipPktJmrv088Pei/RjrhWm5TMIy5URYjZUoDZdh5k940Dyw==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.4.tgz",
"integrity": "sha512-evC1twrny2XDT4uOftoubZvW3EG0zs0ZxMwEtu/dDGVRO5n5pT48S8qqEIBGBUZYu/Xx4zzpOkIxx1vpWdE+9A==",
"cpu": [
"arm64"
],
@@ -182,9 +182,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.3.tgz",
"integrity": "sha512-AMdFX6EKJjC0G/CM6hJvkY8wUjCcbdj3Qg7uAQJ7PVejRWaVt0sDTMavbRfgMchx8h8KsAudUCtdFkG9hlEClw==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.4.tgz",
"integrity": "sha512-PX706XcCHr2FfkyhP2lpf+pX/tUvq6/ke7JYnnr0ykNdEMo+sb7cC/o91gnURh4sPYSiZJhsF2gbIqg9rciOHQ==",
"cpu": [
"x64"
],
@@ -197,9 +197,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.3.tgz",
"integrity": "sha512-jySgSXE48shaLtcQbiFO9ajE9mqz7pcAVLnVLvRIlUHyQYR/WyZdK8ehLs65Mz6j9cLrJM+YdmdJPyV4WDaz2g==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.4.tgz",
"integrity": "sha512-TKUUx3Ftd95JlHV6XagEnqpT204Y+IsEa3awaYIjayn0MOGjgKZMZibqarK3B1FsMSPaieJf2FEAcu9z0yT5aA==",
"cpu": [
"x64"
],
@@ -212,9 +212,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.3.tgz",
"integrity": "sha512-5DxHo8uYcaADiE9pHrg8o28VMt/1kR8voDehmfs9AqS0qSClxAAl+CchjdboUvbCjdNWL1MISCvEfKY2InJ3JA==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.4.tgz",
"integrity": "sha512-FP8AadgSq4+HPtim7WBkCMGbhr5vh9FePXiWx9+YOdjwdQocwoCK5ZVC3OW8oh3TWth6iJ0AXJ/yQ1q1cwSZ3A==",
"cpu": [
"arm64"
],
@@ -227,9 +227,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.3.tgz",
"integrity": "sha512-LaqkF3d+GXRA5X6zrUjQUrXm2MN/3E2arXBtn5C7avBCNYfm9G3Xc646AmmmpN3DJZVaMYliMyCIQCMDEzk80w==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.4.tgz",
"integrity": "sha512-3WekVmtuA2MCdcAOrgrI+PuFiFURtSyyrN1I3UPtS0ckR2HtLqyqmS334Eulf15g1/bdwMteePdK363X/Y9JMg==",
"cpu": [
"ia32"
],
@@ -242,9 +242,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.3.tgz",
"integrity": "sha512-jglUk/x7ZWeOJWlVoKyIAkHLTI+qEkOriOOV+3hr1GyiywzcqfI7TpFSiwC7kk1scOiH7NTFKp8mA3XPNO9bDw==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.4.tgz",
"integrity": "sha512-AHRITu/CrlQ+qzoqQtEMfaTu7GHaQ6bziQln/pVWpOYC1wU+Mq6VQQFlsDtMCnDztPZtppAXdvvbNS7pcfRzlw==",
"cpu": [
"x64"
],
@@ -289,9 +289,9 @@
}
},
"node_modules/@pkgr/utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.0.tgz",
"integrity": "sha512-2OCURAmRtdlL8iUDTypMrrxfwe8frXTeXaxGsVOaYtc/wrUyk8Z/0OBetM7cdlsy7ZFWlMX72VogKeh+A4Xcjw==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz",
"integrity": "sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w==",
"dependencies": {
"cross-spawn": "^7.0.3",
"fast-glob": "^3.2.12",
@@ -308,9 +308,9 @@
}
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
"integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg=="
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz",
"integrity": "sha512-IthPJsJR85GhOkp3Hvp8zFOPK5ynKn6STyHa/WZpioK7E1aYDiBzpqQPrngc14DszIUkIrdd3k9Iu0XSzlP/1w=="
},
"node_modules/@swc/helpers": {
"version": "0.5.1",
@@ -326,13 +326,13 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@typescript-eslint/parser": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz",
"integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==",
"version": "5.59.7",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.7.tgz",
"integrity": "sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ==",
"dependencies": {
"@typescript-eslint/scope-manager": "5.59.6",
"@typescript-eslint/types": "5.59.6",
"@typescript-eslint/typescript-estree": "5.59.6",
"@typescript-eslint/scope-manager": "5.59.7",
"@typescript-eslint/types": "5.59.7",
"@typescript-eslint/typescript-estree": "5.59.7",
"debug": "^4.3.4"
},
"engines": {
@@ -352,12 +352,12 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz",
"integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==",
"version": "5.59.7",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz",
"integrity": "sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==",
"dependencies": {
"@typescript-eslint/types": "5.59.6",
"@typescript-eslint/visitor-keys": "5.59.6"
"@typescript-eslint/types": "5.59.7",
"@typescript-eslint/visitor-keys": "5.59.7"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -368,9 +368,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz",
"integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==",
"version": "5.59.7",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz",
"integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -380,12 +380,12 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz",
"integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==",
"version": "5.59.7",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz",
"integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==",
"dependencies": {
"@typescript-eslint/types": "5.59.6",
"@typescript-eslint/visitor-keys": "5.59.6",
"@typescript-eslint/types": "5.59.7",
"@typescript-eslint/visitor-keys": "5.59.7",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -406,11 +406,11 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz",
"integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==",
"version": "5.59.7",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz",
"integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==",
"dependencies": {
"@typescript-eslint/types": "5.59.6",
"@typescript-eslint/types": "5.59.7",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@@ -591,9 +591,9 @@
}
},
"node_modules/axe-core": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.1.tgz",
"integrity": "sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg==",
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz",
"integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==",
"engines": {
"node": ">=4"
}
@@ -696,9 +696,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001488",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
"integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
"version": "1.0.30001489",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001489.tgz",
"integrity": "sha512-x1mgZEXK8jHIfAxm+xgdpHpk50IN3z3q3zP261/WS+uvePxW8izXuCu6AHz0lkuYTlATDehiZ/tNyYBdSQsOUQ==",
"funding": [
{
"type": "opencollective",
@@ -908,9 +908,9 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/enhanced-resolve": {
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz",
"integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==",
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz",
"integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -1034,14 +1034,14 @@
}
},
"node_modules/eslint": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz",
"integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==",
"version": "8.41.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz",
"integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "8.40.0",
"@eslint/js": "8.41.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -1061,13 +1061,12 @@
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"grapheme-splitter": "^1.0.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@@ -1090,11 +1089,11 @@
}
},
"node_modules/eslint-config-next": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.3.tgz",
"integrity": "sha512-1lXwdFi29fKxzeugof/TUE7lpHyJQt5+U4LaUHyvQfHjvsWO77vFNicJv5sX6k0VDVSbnfz0lw+avxI+CinbMg==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.4.tgz",
"integrity": "sha512-z/PMbm6L0iC/fwISULxe8IVy4DtNqZk2wQY711o35klenq70O6ns82A8yuMVCFjHC0DIyB2lyugesRtuk9u8dQ==",
"dependencies": {
"@next/eslint-plugin-next": "13.4.3",
"@next/eslint-plugin-next": "13.4.4",
"@rushstack/eslint-patch": "^1.1.3",
"@typescript-eslint/parser": "^5.42.0",
"eslint-import-resolver-node": "^0.3.6",
@@ -1765,10 +1764,10 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
},
"node_modules/has": {
"version": "1.0.3",
@@ -2256,15 +2255,6 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/js-sdsl": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
"integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2463,11 +2453,11 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
"node_modules/next": {
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.3.tgz",
"integrity": "sha512-FV3pBrAAnAIfOclTvncw9dDohyeuEEXPe5KNcva91anT/rdycWbgtu3IjUj4n5yHnWK8YEPo0vrUecHmnmUNbA==",
"version": "13.4.4",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.4.tgz",
"integrity": "sha512-C5S0ysM0Ily9McL4Jb48nOQHT1BukOWI59uC3X/xCMlYIh9rJZCv7nzG92J6e1cOBqQbKovlpgvHWFmz4eKKEA==",
"dependencies": {
"@next/env": "13.4.3",
"@next/env": "13.4.4",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
@@ -2482,20 +2472,19 @@
"node": ">=16.8.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "13.4.3",
"@next/swc-darwin-x64": "13.4.3",
"@next/swc-linux-arm64-gnu": "13.4.3",
"@next/swc-linux-arm64-musl": "13.4.3",
"@next/swc-linux-x64-gnu": "13.4.3",
"@next/swc-linux-x64-musl": "13.4.3",
"@next/swc-win32-arm64-msvc": "13.4.3",
"@next/swc-win32-ia32-msvc": "13.4.3",
"@next/swc-win32-x64-msvc": "13.4.3"
"@next/swc-darwin-arm64": "13.4.4",
"@next/swc-darwin-x64": "13.4.4",
"@next/swc-linux-arm64-gnu": "13.4.4",
"@next/swc-linux-arm64-musl": "13.4.4",
"@next/swc-linux-x64-gnu": "13.4.4",
"@next/swc-linux-x64-musl": "13.4.4",
"@next/swc-win32-arm64-msvc": "13.4.4",
"@next/swc-win32-ia32-msvc": "13.4.4",
"@next/swc-win32-x64-msvc": "13.4.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"fibers": ">= 3.1.0",
"node-sass": "^6.0.0 || ^7.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
@@ -2507,9 +2496,6 @@
"fibers": {
"optional": true
},
"node-sass": {
"optional": true
},
"sass": {
"optional": true
}

View File

@@ -9,9 +9,9 @@
"lint": "next lint"
},
"dependencies": {
"eslint": "8.40.0",
"eslint-config-next": "13.4.3",
"next": "13.4.3",
"eslint": "8.41.0",
"eslint-config-next": "13.4.4",
"next": "13.4.4",
"react": "18.2.0",
"react-dom": "18.2.0"
}

View File

@@ -1,14 +1,14 @@
{
"private": true,
"scripts": {
"build": "preact build",
"build": "NODE_OPTIONS=--openssl-legacy-provider preact build",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch",
"lint": "eslint src",
"test": "jest"
},
"engines": {
"node": "16.x"
"node": "18.x"
},
"eslintConfig": {
"extends": "preact",
@@ -17,19 +17,19 @@
]
},
"devDependencies": {
"enzyme": "^3.10.0",
"enzyme-adapter-preact-pure": "^2.0.0",
"eslint": "^6.0.1",
"eslint-config-preact": "^1.1.0",
"jest": "^24.9.0",
"jest-preset-preact": "^1.0.0",
"preact-cli": "^3.0.0",
"sirv-cli": "1.0.3"
"enzyme": "^3.11.0",
"enzyme-adapter-preact-pure": "^4.1.0",
"eslint": "^8.41.0",
"eslint-config-preact": "^1.3.0",
"jest": "^29.5.0",
"jest-preset-preact": "^4.0.4",
"preact-cli": "^3.4.5",
"sirv-cli": "2.0.2"
},
"dependencies": {
"preact": "^10.3.2",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1"
"preact": "^10.15.0",
"preact-render-to-string": "6.0.3",
"preact-router": "^4.1.1"
},
"jest": {
"preset": "jest-preset-preact",

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'prettier'],
plugins: ['svelte3'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,

View File

@@ -1 +1,2 @@
engine-strict=true
resolution-mode=highest

View File

@@ -1,18 +1,12 @@
# create-svelte
# SvelteKit Demo app
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
The official demo app for SvelteKit, hosted on Vercel.
## Creating a project
## Deploy Your Own
If you're seeing this, you've probably already done this step. Congrats!
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fvercel%2Ftree%2Fmain%2Fexamples%2Fsveltekit-1&project-name=sveltekit-vercel&repository-name=sveltekit-vercel&demo-title=SvelteKit%20%2B%20Vercel&demo-url=https%3A%2F%2Fsveltekit-template.vercel.app%2F)
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
_Live Example: https://sveltekit-template.vercel.app_
## Developing
@@ -35,4 +29,8 @@ npm run build
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
## Speed Insights
Once deployed on Vercel, you can benefit from [Speed Insights](https://vercel.com/docs/concepts/speed-insights) simply by navigating to Vercel's dashboard, clicking on the 'Speed Insights' tab, and enabling the product.
You will get data once your application will be re-deployed and will receive visitors.

View File

@@ -16,19 +16,20 @@
"@fontsource/fira-mono": "^4.5.10",
"@neoconfetti/svelte": "^1.0.0",
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-vercel": "^1.0.0",
"@sveltejs/kit": "^1.0.0",
"@sveltejs/adapter-vercel": "^3.0.0",
"@sveltejs/kit": "^1.5.0",
"@types/cookie": "^0.5.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"eslint-plugin-svelte": "^2.26.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0",
"svelte-check": "^2.9.2",
"typescript": "^4.9.3",
"vite": "^4.0.0",
"vitest": "^0.25.3"
"svelte-check": "^3.0.1",
"typescript": "^5.0.0",
"vite": "^4.3.0",
"vitest": "^0.25.3",
"web-vitals": "^3.3.1"
},
"type": "module"
}

View File

@@ -4,7 +4,8 @@ const config = {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests'
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,63 @@
import { onCLS, onFCP, onFID, onLCP, onTTFB } from 'web-vitals';
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
// @ts-ignore
return navigator?.connection?.effectiveType ?? '';
}
/**
* @param {import("web-vitals").Metric} metric
* @param {{ params: { [s: string]: any; } | ArrayLike<any>; path: string; analyticsId: string; debug: boolean; }} options
*/
function sendToAnalytics(metric, options) {
const page = Object.entries(options.params).reduce(
(acc, [key, value]) => acc.replace(value, `[${key}]`),
options.path
);
const body = {
dsn: options.analyticsId,
id: metric.id,
page,
href: location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed()
};
if (options.debug) {
console.log('[Web Vitals]', metric.name, JSON.stringify(body, null, 2));
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded'
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true
});
}
/**
* @param {any} options
*/
export function webVitals(options) {
try {
console.log(`[Web Vitals] for page ${options.path}`);
onFID((metric) => sendToAnalytics(metric, options));
onTTFB((metric) => sendToAnalytics(metric, options));
onLCP((metric) => sendToAnalytics(metric, options));
onCLS((metric) => sendToAnalytics(metric, options));
onFCP((metric) => sendToAnalytics(metric, options));
} catch (err) {
console.error(`[Web Vitals] for page ${options.path}`, err);
}
}

View File

@@ -0,0 +1,6 @@
import { env } from '$env/dynamic/private';
/** @type {import('./$types').LayoutServerLoad} */
export function load() {
return { analyticsId: env.VERCEL_ANALYTICS_ID };
}

View File

@@ -1,6 +1,20 @@
<script>
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { webVitals } from '$lib/vitals';
import Header from './Header.svelte';
import './styles.css';
/** @type {import('./$types').LayoutServerData} */
export let data;
$: if (browser && data?.analyticsId) {
webVitals({
path: $page.url.pathname,
params: $page.params,
analyticsId: data.analyticsId
});
}
</script>
<div class="app">

View File

@@ -107,11 +107,11 @@
<a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
{#each Array(6) as _, row}
{#each Array.from(Array(6).keys()) as row (row)}
{@const current = row === i}
<h2 class="visually-hidden">Row {row + 1}</h2>
<div class="row" class:current>
{#each Array(5) as _, column}
{#each Array.from(Array(5).keys()) as column (column)}
{@const answer = data.answers[row]?.[column]}
{@const value = data.guesses[row]?.[column] ?? ''}
{@const selected = current && column === data.guesses[row].length}

View File

@@ -2,5 +2,5 @@ import { expect, test } from '@playwright/test';
test('about page has expected h1', async ({ page }) => {
await page.goto('/about');
expect(await page.textContent('h1')).toBe('About this app');
await expect(page.getByRole('heading', { name: 'About this app' })).toBeVisible();
});

View File

@@ -1,11 +1,9 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
/** @type {import('vite').UserConfig} */
const config = {
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
};
export default config;
});

View File

@@ -0,0 +1,8 @@
# @vercel-internals/constants
## 1.0.1
### Patch Changes
- Updated dependencies [[`c7bcea408`](https://github.com/vercel/vercel/commit/c7bcea408131df2d65338e50ce319a6d8e4a8a82)]:
- @vercel/build-utils@6.7.4

View File

@@ -1,14 +1,14 @@
{
"private": true,
"name": "@vercel-internals/constants",
"version": "1.0.0",
"version": "1.0.1",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"@vercel/routing-utils": "2.2.1"
},
"devDependencies": {

View File

@@ -0,0 +1,9 @@
# @vercel-internals/types
## 1.0.1
### Patch Changes
- Updated dependencies [[`c7bcea408`](https://github.com/vercel/vercel/commit/c7bcea408131df2d65338e50ce319a6d8e4a8a82)]:
- @vercel/build-utils@6.7.4
- @vercel-internals/constants@1.0.1

View File

@@ -355,7 +355,7 @@ export interface Project extends ProjectSettings {
link?: ProjectLinkData;
alias?: ProjectAliasTarget[];
latestDeployments?: Partial<Deployment>[];
lastRollbackTarget: RollbackTarget | null;
lastAliasRequest?: LastAliasRequest | null;
}
export interface Org {
@@ -365,8 +365,19 @@ export interface Org {
}
export interface ProjectLink {
/**
* ID of the Vercel Project.
*/
projectId: string;
/**
* User or Team ID of the owner of the Vercel Project.
*/
orgId: string;
/**
* When linked as a repository, contains the absolute path
* to the root directory of the repository.
*/
repoRoot?: string;
}
export interface PaginationOptions {
@@ -376,7 +387,7 @@ export interface PaginationOptions {
}
export type ProjectLinkResult =
| { status: 'linked'; org: Org; project: Project }
| { status: 'linked'; org: Org; project: Project; repoRoot?: string }
| { status: 'not_linked'; org: null; project: null }
| {
status: 'error';
@@ -390,6 +401,9 @@ export type ProjectLinkResult =
| 'MISSING_PROJECT_SETTINGS';
};
/**
* @deprecated - `RollbackJobStatus` has been replace by `LastAliasRequest['jobStatus']`.
*/
export type RollbackJobStatus =
| 'pending'
| 'in-progress'
@@ -397,6 +411,10 @@ export type RollbackJobStatus =
| 'failed'
| 'skipped';
/**
* @deprecated - `RollbackTarget` has been renamed to `LastAliasRequest` so it can
* be shared with "promote".
*/
export interface RollbackTarget {
fromDeploymentId: string;
jobStatus: RollbackJobStatus;
@@ -404,6 +422,14 @@ export interface RollbackTarget {
toDeploymentId: string;
}
export interface LastAliasRequest {
fromDeploymentId: string;
jobStatus: 'pending' | 'in-progress' | 'succeeded' | 'failed' | 'skipped';
requestedAt: number;
toDeploymentId: string;
type: 'rollback' | 'promote';
}
export interface Token {
id: string;
name: string;

View File

@@ -1,13 +1,13 @@
{
"private": true,
"name": "@vercel-internals/types",
"version": "1.0.0",
"version": "1.0.1",
"types": "index.d.ts",
"main": "index.d.ts",
"dependencies": {
"@types/node": "14.14.31",
"@vercel-internals/constants": "1.0.0",
"@vercel/build-utils": "6.7.3",
"@vercel-internals/constants": "1.0.1",
"@vercel/build-utils": "6.7.4",
"@vercel/routing-utils": "2.2.1"
},
"devDependencies": {

View File

@@ -32,7 +32,7 @@
"source-map-support": "0.5.12",
"ts-eager": "2.0.2",
"ts-jest": "29.1.0",
"turbo": "1.9.8",
"turbo": "1.9.9",
"typescript": "4.9.5"
},
"scripts": {
@@ -48,8 +48,8 @@
"prettier-check": "prettier --check .",
"prepare": "husky install",
"pack": "cd utils && node -r ts-eager/register ./pack.ts",
"version:prepare": "changeset version && pnpm install --no-frozen-lockfile",
"release": "changeset publish"
"ci:version": "changeset version && pnpm install --no-frozen-lockfile",
"ci:publish": "pnpm publish -r && changeset tag"
},
"lint-staged": {
"./{*,{api,packages,test,utils}/**/*}.{js,ts}": [

View File

@@ -1,5 +1,11 @@
# @vercel/build-utils
## 6.7.4
### Patch Changes
- Remove usage of `env` from Edge Functions and Middleware ([#10018](https://github.com/vercel/vercel/pull/10018))
## 6.7.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "6.7.3",
"version": "6.7.4",
"license": "Apache-2.0",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -27,12 +27,6 @@ export class EdgeFunction {
*/
files: Files;
/**
* Extra environment variables in use for the user code, to be
* assigned to the edge function.
*/
envVarsInUse?: string[];
/**
* Extra binary files to be included in the edge function
*/
@@ -50,7 +44,6 @@ export class EdgeFunction {
this.deploymentTarget = params.deploymentTarget;
this.entrypoint = params.entrypoint;
this.files = params.files;
this.envVarsInUse = params.envVarsInUse;
this.assets = params.assets;
this.regions = params.regions;
this.framework = params.framework;

View File

@@ -1,5 +1,26 @@
# vercel
## 30.1.0
### Minor Changes
- New `vc promote` command ([#9984](https://github.com/vercel/vercel/pull/9984))
### Patch Changes
- Support `deploy` subcommand in "repo linked" mode ([#10013](https://github.com/vercel/vercel/pull/10013))
- [cli] Update `vc rollback` to use `lastRequestAlias` instead of `lastRollbackTarget` ([#10019](https://github.com/vercel/vercel/pull/10019))
- Fix `--cwd` flag with a relative path for `env`, `link`, `promote`, and `rollback` subcommands ([#10031](https://github.com/vercel/vercel/pull/10031))
- Updated dependencies [[`c6c19354e`](https://github.com/vercel/vercel/commit/c6c19354e852cfc1338b223058c4b07fdc71c723), [`b56ac2717`](https://github.com/vercel/vercel/commit/b56ac2717d6769eb400f9746f0a05431929b4501), [`c63679ea0`](https://github.com/vercel/vercel/commit/c63679ea0a6bc48c0759ccf3c0c0a8106bd324f0), [`c7bcea408`](https://github.com/vercel/vercel/commit/c7bcea408131df2d65338e50ce319a6d8e4a8a82)]:
- @vercel/next@3.8.6
- @vercel/build-utils@6.7.4
- @vercel/node@2.14.4
- @vercel/remix-builder@1.8.11
- @vercel/static-build@1.3.33
## 30.0.0
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "30.0.0",
"version": "30.1.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -32,16 +32,16 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"@vercel/go": "2.5.1",
"@vercel/hydrogen": "0.0.64",
"@vercel/next": "3.8.5",
"@vercel/node": "2.14.3",
"@vercel/next": "3.8.6",
"@vercel/node": "2.14.4",
"@vercel/python": "3.1.60",
"@vercel/redwood": "1.1.15",
"@vercel/remix-builder": "1.8.10",
"@vercel/remix-builder": "1.8.11",
"@vercel/ruby": "1.3.76",
"@vercel/static-build": "1.3.32"
"@vercel/static-build": "1.3.33"
},
"devDependencies": {
"@alex_neo/jest-expect-message": "1.0.5",
@@ -85,10 +85,10 @@
"@types/which": "3.0.0",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel-internals/constants": "1.0.0",
"@vercel-internals/constants": "1.0.1",
"@vercel-internals/get-package-json": "1.0.0",
"@vercel-internals/types": "1.0.0",
"@vercel/client": "12.6.0",
"@vercel-internals/types": "1.0.1",
"@vercel/client": "12.6.1",
"@vercel/error-utils": "1.0.10",
"@vercel/frameworks": "1.4.2",
"@vercel/fs-detectors": "3.9.3",
@@ -119,6 +119,7 @@
"escape-html": "1.0.3",
"esm": "3.1.4",
"execa": "3.2.0",
"expect": "29.5.0",
"express": "4.17.1",
"fast-deep-equal": "3.1.3",
"find-up": "4.1.0",

View File

@@ -24,6 +24,7 @@ export const help = () => `
ls | list [app] Lists deployments
login [email] Logs into your account or creates a new one
logout Logs out of your account
promote [url|id] Promote an existing deployment to current
pull [path] Pull your Project Settings from the cloud
redeploy [url|id] Rebuild and deploy a previous deployment.
rollback [url|id] Quickly revert back to a previous deployment

View File

@@ -133,7 +133,7 @@ const help = () => {
};
export default async function main(client: Client): Promise<number> {
const { output } = client;
const { cwd, output } = client;
// Ensure that `vc build` is not being invoked recursively
if (process.env.__VERCEL_BUILD_RUNNING) {
@@ -165,8 +165,6 @@ export default async function main(client: Client): Promise<number> {
return 2;
}
const cwd = process.cwd();
// Build `target` influences which environment variables will be used
const target = argv['--prod'] ? 'production' : 'preview';
const yes = Boolean(argv['--yes']);

View File

@@ -2,7 +2,7 @@ import ms from 'ms';
import fs from 'fs-extra';
import bytes from 'bytes';
import chalk from 'chalk';
import { join, resolve, basename } from 'path';
import { join, resolve } from 'path';
import {
fileNameSymbol,
VALID_ARCHIVE_FORMATS,
@@ -130,24 +130,19 @@ export default async (client: Client): Promise<number> => {
if (argv._.length > 0) {
// If path is relative: resolve
// if path is absolute: clear up strange `/` etc
paths = argv._.map(item => resolve(process.cwd(), item));
paths = argv._.map(item => resolve(client.cwd, item));
} else {
paths = [process.cwd()];
paths = [client.cwd];
}
// check paths
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
let localConfig = client.localConfig || readLocalConfig(paths[0]);
for (const path of paths) {
try {
await fs.stat(path);
} catch (err) {
output.error(
`The specified file or directory "${basename(path)}" does not exist.`
);
return 1;
}
}
if (localConfig) {
const { version } = localConfig;
const file = highlight(localConfig[fileNameSymbol]!);
@@ -176,14 +171,7 @@ export default async (client: Client): Promise<number> => {
const quiet = !client.stdout.isTTY;
// check paths
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
const { path } = pathValidation;
let { path: cwd } = pathValidation;
const autoConfirm = argv['--yes'];
// deprecate --name
@@ -217,7 +205,7 @@ export default async (client: Client): Promise<number> => {
// build `--prebuilt`
if (argv['--prebuilt']) {
const prebuiltExists = await fs.pathExists(join(path, '.vercel/output'));
const prebuiltExists = await fs.pathExists(join(cwd, '.vercel/output'));
if (!prebuiltExists) {
error(
`The ${param(
@@ -229,7 +217,7 @@ export default async (client: Client): Promise<number> => {
return 1;
}
const prebuiltBuild = await getPrebuiltJson(path);
const prebuiltBuild = await getPrebuiltJson(cwd);
// Ensure that there was not a build error
const prebuiltError =
@@ -272,7 +260,7 @@ export default async (client: Client): Promise<number> => {
}
// retrieve `project` and `org` from .vercel
const link = await getLinkedProject(client, path);
const link = await getLinkedProject(client, cwd);
if (link.status === 'error') {
return link.exitCode;
@@ -289,7 +277,7 @@ export default async (client: Client): Promise<number> => {
autoConfirm ||
(await confirm(
client,
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
`Set up and deploy ${chalk.cyan(`${toHumanPath(cwd)}`)}?`,
true
));
@@ -336,7 +324,7 @@ export default async (client: Client): Promise<number> => {
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
rootDirectory = await inputRootDirectory(client, cwd, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
@@ -344,8 +332,8 @@ export default async (client: Client): Promise<number> => {
// we can already link the project
await linkFolderToProject(
output,
path,
client,
cwd,
{
projectId: project.id,
orgId: org.id,
@@ -357,6 +345,11 @@ export default async (client: Client): Promise<number> => {
}
}
// For repo-style linking, reset the path to the root of the repository
if (link.status === 'linked' && link.repoRoot) {
cwd = link.repoRoot;
}
// At this point `org` should be populated
if (!org) {
throw new Error(`"org" is not defined`);
@@ -371,14 +364,14 @@ export default async (client: Client): Promise<number> => {
// and upload the entire directory.
const sourcePath =
rootDirectory && !sourceFilesOutsideRootDirectory
? join(path, rootDirectory)
: path;
? join(cwd, rootDirectory)
: cwd;
if (
rootDirectory &&
(await validateRootDirectory(
output,
path,
cwd,
sourcePath,
project
? `To change your Project Settings, go to https://vercel.com/${org?.slug}/${project.name}/settings`
@@ -391,7 +384,7 @@ export default async (client: Client): Promise<number> => {
// If Root Directory is used we'll try to read the config
// from there instead and use it if it exists.
if (rootDirectory) {
const rootDirectoryConfig = readLocalConfig(join(path, rootDirectory));
const rootDirectoryConfig = readLocalConfig(join(cwd, rootDirectory));
if (rootDirectoryConfig) {
debug(`Read local config from root directory (${rootDirectory})`);
@@ -467,7 +460,7 @@ export default async (client: Client): Promise<number> => {
parseMeta(argv['--meta'])
);
const gitMetadata = await createGitMeta(path, output, project);
const gitMetadata = await createGitMeta(cwd, output, project);
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
const deploymentEnv = Object.assign(
@@ -560,11 +553,11 @@ export default async (client: Client): Promise<number> => {
client,
now,
contextName,
[sourcePath],
sourcePath,
createArgs,
org,
!project,
path,
cwd,
archive
);
@@ -596,11 +589,11 @@ export default async (client: Client): Promise<number> => {
client,
now,
contextName,
[sourcePath],
sourcePath,
createArgs,
org,
false,
path
cwd
);
}

View File

@@ -130,10 +130,9 @@ export default async function main(client: Client) {
return 2;
}
const cwd = argv['--cwd'] || process.cwd();
const subArgs = argv._.slice(1);
const { subcommand, args } = getSubcommand(subArgs, COMMAND_CONFIG);
const { output, config } = client;
const { cwd, output, config } = client;
const target = argv['--environment']?.toLowerCase() || 'development';
if (!isValidEnvTarget(target)) {

View File

@@ -16,7 +16,6 @@ import {
parseRepoUrl,
printRemoteUrls,
} from '../../util/git/connect-git-provider';
import validatePaths from '../../util/validate-paths';
interface GitRepoCheckParams {
client: Client;
@@ -56,7 +55,7 @@ export default async function connect(
project: Project | undefined,
org: Org | undefined
) {
const { output } = client;
const { cwd, output } = client;
const confirm = Boolean(argv['--yes']);
const repoArg = argv._[1];
@@ -77,19 +76,11 @@ export default async function connect(
return 1;
}
let paths = [process.cwd()];
const validate = await validatePaths(client, paths);
if (!validate.valid) {
return validate.exitCode;
}
const { path } = validate;
const gitProviderLink = project.link;
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
// get project from .git
const gitConfigPath = join(path, '.git/config');
const gitConfigPath = join(cwd, '.git/config');
const gitConfig = await parseGitConfig(gitConfigPath, output);
if (repoArg) {

View File

@@ -6,7 +6,6 @@ import getInvalidSubcommand from '../../util/get-invalid-subcommand';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import validatePaths from '../../util/validate-paths';
import connect from './connect';
import disconnect from './disconnect';
@@ -81,16 +80,9 @@ export default async function main(client: Client) {
subcommand = argv._[0];
const args = argv._.slice(1);
const autoConfirm = Boolean(argv['--yes']);
const { output } = client;
const { cwd, output } = client;
let paths = [process.cwd()];
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
const { path } = pathValidation;
const linkedProject = await ensureLink('git', client, path, { autoConfirm });
const linkedProject = await ensureLink('git', client, cwd, { autoConfirm });
if (typeof linkedProject === 'number') {
return linkedProject;
}

View File

@@ -26,6 +26,7 @@ export default new Map([
['ls', 'list'],
['project', 'project'],
['projects', 'project'],
['promote', 'promote'],
['pull', 'pull'],
['redeploy', 'redeploy'],
['remove', 'remove'],

View File

@@ -90,7 +90,7 @@ export default async function main(client: Client) {
)} instead`
);
} else {
cwd = process.cwd();
cwd = client.cwd;
}
if (argv['--repo']) {

View File

@@ -15,7 +15,6 @@ import getCommandFlags from '../util/get-command-flags';
import { getPkgName, getCommandName } from '../util/pkg-name';
import Client from '../util/client';
import { Deployment } from '@vercel/client';
import validatePaths from '../util/validate-paths';
import { getLinkedProject } from '../util/projects/link';
import { ensureLink } from '../util/link/ensure-link';
import getScope from '../util/get-scope';
@@ -94,7 +93,7 @@ export default async function main(client: Client) {
return 1;
}
const { output, config } = client;
const { cwd, output, config } = client;
if ('--confirm' in argv) {
output.warn('`--confirm` is deprecated, please use `--yes` instead');
@@ -115,19 +114,10 @@ export default async function main(client: Client) {
const autoConfirm = !!argv['--yes'];
const prod = argv['--prod'] || false;
const meta = parseMeta(argv['--meta']);
let paths = [process.cwd()];
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
const { path } = pathValidation;
// retrieve `project` and `org` from .vercel
let link = await getLinkedProject(client, path);
let link = await getLinkedProject(client, cwd);
if (link.status === 'error') {
return link.exitCode;
@@ -146,7 +136,7 @@ export default async function main(client: Client) {
// If there's no linked project and user doesn't pass `app` arg,
// prompt to link their current directory.
if (status === 'not_linked' && !app) {
const linkedProject = await ensureLink('list', client, path, {
const linkedProject = await ensureLink('list', client, cwd, {
autoConfirm,
link,
});

View File

@@ -0,0 +1,121 @@
import chalk from 'chalk';
import type Client from '../../util/client';
import getArgs from '../../util/get-args';
import getProjectByCwdOrLink from '../../util/projects/get-project-by-cwd-or-link';
import { getPkgName } from '../../util/pkg-name';
import handleError from '../../util/handle-error';
import { isErrnoException } from '@vercel/error-utils';
import logo from '../../util/output/logo';
import ms from 'ms';
import requestPromote from './request-promote';
import promoteStatus from './status';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} promote`)} [deployment id/url]
Promote an existing deployment to current.
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
--timeout=${chalk.bold.underline(
'TIME'
)} Time to wait for promotion completion [3m]
-y, --yes Skip questions when setting up new project using default scope and settings
${chalk.dim('Examples:')}
${chalk.gray('')} Show the status of any current pending promotions
${chalk.cyan(`$ ${getPkgName()} promote`)}
${chalk.cyan(`$ ${getPkgName()} promote status`)}
${chalk.cyan(`$ ${getPkgName()} promote status <project>`)}
${chalk.cyan(`$ ${getPkgName()} promote status --timeout 30s`)}
${chalk.gray('')} Promote a deployment using id or url
${chalk.cyan(`$ ${getPkgName()} promote <deployment id/url>`)}
`);
};
/**
* `vc promote` command
* @param {Client} client
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async (client: Client): Promise<number> => {
let argv;
try {
argv = getArgs(client.argv.slice(2), {
'--timeout': String,
'--yes': Boolean,
'-y': '--yes',
});
} catch (err) {
handleError(err);
return 1;
}
if (argv['--help'] || argv._[0] === 'help') {
help();
return 2;
}
// validate the timeout
let timeout = argv['--timeout'];
if (timeout && ms(timeout) === undefined) {
client.output.error(`Invalid timeout "${timeout}"`);
return 1;
}
const actionOrDeployId = argv._[1] || 'status';
try {
if (actionOrDeployId === 'status') {
const project = await getProjectByCwdOrLink({
autoConfirm: Boolean(argv['--yes']),
client,
commandName: 'promote',
cwd: client.cwd,
projectNameOrId: argv._[2],
});
return await promoteStatus({
client,
project,
timeout,
});
}
return await requestPromote({
client,
deployId: actionOrDeployId,
timeout,
});
} catch (err) {
if (isErrnoException(err)) {
if (err.code === 'ERR_CANCELED') {
return 0;
}
if (err.code === 'ERR_INVALID_CWD' || err.code === 'ERR_LINK_PROJECT') {
// do not show the message
return 1;
}
}
client.output.prettyError(err);
return 1;
}
};

View File

@@ -0,0 +1,57 @@
import chalk from 'chalk';
import type Client from '../../util/client';
import { getCommandName } from '../../util/pkg-name';
import getProjectByDeployment from '../../util/projects/get-project-by-deployment';
import ms from 'ms';
import promoteStatus from './status';
/**
* Requests a promotion and waits for it complete.
* @param {Client} client - The Vercel client instance
* @param {string} deployId - The deployment name or id to promote
* @param {string} [timeout] - Time to poll for succeeded/failed state
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async function requestPromote({
client,
deployId,
timeout,
}: {
client: Client;
deployId: string;
timeout?: string;
}): Promise<number> {
const { output } = client;
const { contextName, deployment, project } = await getProjectByDeployment({
client,
deployId,
output: client.output,
});
// request the promotion
await client.fetch(`/v9/projects/${project.id}/promote/${deployment.id}`, {
body: {}, // required
json: false,
method: 'POST',
});
if (timeout !== undefined && ms(timeout) === 0) {
output.log(
`Successfully requested promote of ${chalk.bold(project.name)} to ${
deployment.url
} (${deployment.id})`
);
output.log(`To check promote status, run ${getCommandName('promote')}.`);
return 0;
}
// check the status
return await promoteStatus({
client,
contextName,
deployment,
project,
timeout,
});
}

View File

@@ -0,0 +1,265 @@
import chalk from 'chalk';
import type Client from '../../util/client';
import type {
Deployment,
LastAliasRequest,
PaginationOptions,
Project,
} from '@vercel-internals/types';
import elapsed from '../../util/output/elapsed';
import formatDate from '../../util/format-date';
import getDeployment from '../../util/get-deployment';
import { getPkgName } from '../../util/pkg-name';
import getProjectByNameOrId from '../../util/projects/get-project-by-id-or-name';
import getScope from '../../util/get-scope';
import ms from 'ms';
import { ProjectNotFound } from '../../util/errors-ts';
import renderAliasStatus from '../../util/alias/render-alias-status';
import sleep from '../../util/sleep';
interface DeploymentAlias {
alias: {
alias: string;
deploymentId: string;
};
id: string;
status: 'completed' | 'in-progress' | 'pending' | 'failed';
}
interface AliasesResponse {
aliases: DeploymentAlias[];
pagination: PaginationOptions;
}
/**
* Continuously checks a deployment status until it has succeeded, failed, or
* taken longer than the timeout (default 3 minutes).
*
* @param {Client} client - The Vercel client instance
* @param {string} [contextName] - The scope name; if not specified, it will be
* extracted from the `client`
* @param {Deployment} [deployment] - Info about the deployment which is used
* to display different output following a promotion request
* @param {Project} project - Project info instance
* @param {string} [timeout] - Milliseconds to poll for succeeded/failed state
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async function promoteStatus({
client,
contextName,
deployment,
project,
timeout = '3m',
}: {
client: Client;
contextName?: string;
deployment?: Deployment;
project: Project;
timeout?: string;
}): Promise<number> {
const { output } = client;
const recentThreshold = Date.now() - ms('3m');
const promoteTimeout = Date.now() + ms(timeout);
let counter = 0;
let spinnerMessage = deployment
? 'Promote in progress'
: `Checking promotion status of ${project.name}`;
if (!contextName) {
({ contextName } = await getScope(client));
}
try {
output.spinner(`${spinnerMessage}`);
// continuously loop until the promotion has explicitly succeeded, failed,
// or timed out
for (;;) {
const projectCheck = await getProjectByNameOrId(
client,
project.id,
project.accountId,
true
);
if (projectCheck instanceof ProjectNotFound) {
throw projectCheck;
}
const {
jobStatus,
requestedAt,
toDeploymentId,
type,
}: Partial<LastAliasRequest> = projectCheck.lastAliasRequest ?? {};
if (
!jobStatus ||
(jobStatus !== 'in-progress' && jobStatus !== 'pending')
) {
output.stopSpinner();
output.log(`${spinnerMessage}`);
}
if (
!jobStatus ||
!requestedAt ||
!toDeploymentId ||
requestedAt < recentThreshold
) {
output.log('No deployment promotion in progress');
return 0;
}
if (jobStatus === 'skipped' && type === 'promote') {
output.log('Promote deployment was skipped');
return 0;
}
if (jobStatus === 'succeeded') {
return await renderJobSucceeded({
client,
contextName,
performingPromote: !!deployment,
requestedAt,
project,
toDeploymentId,
});
}
if (jobStatus === 'failed') {
return await renderJobFailed({
client,
contextName,
deployment,
project,
toDeploymentId,
});
}
// lastly, if we're not pending/in-progress, then we don't know what
// the status is, so bail
if (jobStatus !== 'pending' && jobStatus !== 'in-progress') {
output.log(`Unknown promote deployment status "${jobStatus}"`);
return 1;
}
// check if we have been running for too long
if (requestedAt < recentThreshold || Date.now() >= promoteTimeout) {
output.log(
`The promotion exceeded its deadline - rerun ${chalk.bold(
`${getPkgName()} promote ${toDeploymentId}`
)} to try again`
);
return 1;
}
// if we've done our first poll and not rolling back, then print the
// requested at date/time
if (counter++ === 0 && !deployment) {
spinnerMessage += ` requested at ${formatDate(requestedAt)}`;
}
output.spinner(`${spinnerMessage}`);
await sleep(250);
}
} finally {
output.stopSpinner();
}
}
async function renderJobFailed({
client,
contextName,
deployment,
project,
toDeploymentId,
}: {
client: Client;
contextName: string;
deployment?: Deployment;
project: Project;
toDeploymentId: string;
}) {
const { output } = client;
try {
const name = (
deployment || (await getDeployment(client, contextName, toDeploymentId))
)?.url;
output.error(
`Failed to remap all aliases to the requested deployment ${name} (${toDeploymentId})`
);
} catch (e) {
output.error(
`Failed to remap all aliases to the requested deployment ${toDeploymentId}`
);
}
// aliases are paginated, so continuously loop until all of them have been
// fetched
let nextTimestamp;
for (;;) {
let url = `/v9/projects/${project.id}/promote/aliases?failedOnly=true&limit=20`;
if (nextTimestamp) {
url += `&until=${nextTimestamp}`;
}
const { aliases, pagination } = await client.fetch<AliasesResponse>(url);
for (const { alias, status } of aliases) {
output.log(
` ${renderAliasStatus(status).padEnd(11)} ${alias.alias} (${
alias.deploymentId
})`
);
}
if (pagination?.next) {
nextTimestamp = pagination.next;
} else {
break;
}
}
return 1;
}
async function renderJobSucceeded({
client,
contextName,
performingPromote,
project,
requestedAt,
toDeploymentId,
}: {
client: Client;
contextName: string;
performingPromote: boolean;
project: Project;
requestedAt: number;
toDeploymentId: string;
}) {
const { output } = client;
// attempt to get the new deployment url
let deploymentInfo = '';
try {
const deployment = await getDeployment(client, contextName, toDeploymentId);
deploymentInfo = `${chalk.bold(deployment.url)} (${toDeploymentId})`;
} catch (err: any) {
output.debug(
`Failed to get deployment url for ${toDeploymentId}: ${
err?.toString() || err
}`
);
deploymentInfo = chalk.bold(toDeploymentId);
}
const duration = performingPromote ? elapsed(Date.now() - requestedAt) : '';
output.log(
`Success! ${chalk.bold(
project.name
)} was promoted to ${deploymentInfo} ${duration}`
);
return 0;
}

View File

@@ -1,20 +1,18 @@
import chalk from 'chalk';
import type Client from '../util/client';
import { ensureLink } from '../util/link/ensure-link';
import getArgs from '../util/get-args';
import { getPkgName } from '../util/pkg-name';
import handleError from '../util/handle-error';
import logo from '../util/output/logo';
import type Client from '../../util/client';
import getArgs from '../../util/get-args';
import getProjectByCwdOrLink from '../../util/projects/get-project-by-cwd-or-link';
import { getPkgName } from '../../util/pkg-name';
import handleError from '../../util/handle-error';
import { isErrnoException } from '@vercel/error-utils';
import logo from '../../util/output/logo';
import ms from 'ms';
import requestRollback from '../util/rollback/request-rollback';
import rollbackStatus from '../util/rollback/status';
import validatePaths from '../util/validate-paths';
import requestRollback from './request-rollback';
import rollbackStatus from './status';
const help = () => {
console.log(`
${chalk.bold(
`${logo} ${getPkgName()} rollback`
)} [deploymentId|deploymentName]
${chalk.bold(`${logo} ${getPkgName()} rollback`)} [deployment id/url]
Quickly revert back to a previous deployment.
@@ -43,6 +41,7 @@ const help = () => {
${chalk.cyan(`$ ${getPkgName()} rollback`)}
${chalk.cyan(`$ ${getPkgName()} rollback status`)}
${chalk.cyan(`$ ${getPkgName()} rollback status <project>`)}
${chalk.cyan(`$ ${getPkgName()} rollback status --timeout 30s`)}
${chalk.gray('')} Rollback a deployment using id or url
@@ -60,8 +59,6 @@ export default async (client: Client): Promise<number> => {
let argv;
try {
argv = getArgs(client.argv.slice(2), {
'--debug': Boolean,
'-d': '--debug',
'--timeout': String,
'--yes': Boolean,
'-y': '--yes',
@@ -76,26 +73,6 @@ export default async (client: Client): Promise<number> => {
return 2;
}
// ensure the current directory is good
const cwd = argv['--cwd'] || process.cwd();
const pathValidation = await validatePaths(client, [cwd]);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
// ensure the current directory is a linked project
const linkedProject = await ensureLink(
'rollback',
client,
pathValidation.path,
{
autoConfirm: Boolean(argv['--yes']),
}
);
if (typeof linkedProject === 'number') {
return linkedProject;
}
// validate the timeout
let timeout = argv['--timeout'];
if (timeout && ms(timeout) === undefined) {
@@ -103,21 +80,42 @@ export default async (client: Client): Promise<number> => {
return 1;
}
const { project } = linkedProject;
const actionOrDeployId = argv._[1] || 'status';
if (actionOrDeployId === 'status') {
return await rollbackStatus({
try {
if (actionOrDeployId === 'status') {
const project = await getProjectByCwdOrLink({
autoConfirm: Boolean(argv['--yes']),
client,
commandName: 'promote',
cwd: client.cwd,
projectNameOrId: argv._[2],
});
return await rollbackStatus({
client,
project,
timeout,
});
}
return await requestRollback({
client,
project,
deployId: actionOrDeployId,
timeout,
});
}
} catch (err) {
if (isErrnoException(err)) {
if (err.code === 'ERR_CANCELED') {
return 0;
}
if (err.code === 'ERR_INVALID_CWD' || err.code === 'ERR_LINK_PROJECT') {
// do not show the message
return 1;
}
}
return await requestRollback({
client,
deployIdOrUrl: actionOrDeployId,
project,
timeout,
});
client.output.prettyError(err);
return 1;
}
};

View File

@@ -0,0 +1,56 @@
import chalk from 'chalk';
import type Client from '../../util/client';
import { getCommandName } from '../../util/pkg-name';
import getProjectByDeployment from '../../util/projects/get-project-by-deployment';
import ms from 'ms';
import rollbackStatus from './status';
/**
* Requests a rollback and waits for it complete.
* @param {Client} client - The Vercel client instance
* @param {string} deployIdOrUrl - The deployment name or id to rollback
* @param {string} [timeout] - Time to poll for succeeded/failed state
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async function requestRollback({
client,
deployId,
timeout,
}: {
client: Client;
deployId: string;
timeout?: string;
}): Promise<number> {
const { output } = client;
const { contextName, deployment, project } = await getProjectByDeployment({
client,
deployId,
output: client.output,
});
// create the rollback
await client.fetch(`/v9/projects/${project.id}/rollback/${deployment.id}`, {
body: {}, // required
method: 'POST',
});
if (timeout !== undefined && ms(timeout) === 0) {
output.log(
`Successfully requested rollback of ${chalk.bold(project.name)} to ${
deployment.url
} (${deployment.id})`
);
output.log(`To check rollback status, run ${getCommandName('rollback')}.`);
return 0;
}
// check the status
return await rollbackStatus({
client,
contextName,
deployment,
project,
timeout,
});
}

View File

@@ -1,18 +1,21 @@
import chalk from 'chalk';
import type Client from '../client';
import type Client from '../../util/client';
import type {
Deployment,
LastAliasRequest,
PaginationOptions,
Project,
RollbackTarget,
} from '@vercel-internals/types';
import elapsed from '../output/elapsed';
import formatDate from '../format-date';
import getDeployment from '../get-deployment';
import getScope from '../get-scope';
import elapsed from '../../util/output/elapsed';
import formatDate from '../../util/format-date';
import getDeployment from '../../util/get-deployment';
import { getPkgName } from '../../util/pkg-name';
import getProjectByNameOrId from '../../util/projects/get-project-by-id-or-name';
import getScope from '../../util/get-scope';
import ms from 'ms';
import renderAliasStatus from './render-alias-status';
import sleep from '../sleep';
import { ProjectNotFound } from '../../util/errors-ts';
import renderAliasStatus from '../../util/alias/render-alias-status';
import sleep from '../../util/sleep';
interface RollbackAlias {
alias: {
@@ -61,13 +64,6 @@ export default async function rollbackStatus({
? 'Rollback in progress'
: `Checking rollback status of ${project.name}`;
const check = async () => {
const { lastRollbackTarget } = await client.fetch<any>(
`/v9/projects/${project.id}?rollbackInfo=true`
);
return lastRollbackTarget;
};
if (!contextName) {
({ contextName } = await getScope(client));
}
@@ -78,8 +74,22 @@ export default async function rollbackStatus({
// continuously loop until the rollback has explicitly succeeded, failed,
// or timed out
for (;;) {
const { jobStatus, requestedAt, toDeploymentId }: RollbackTarget =
(await check()) ?? {};
const projectCheck = await getProjectByNameOrId(
client,
project.id,
project.accountId,
true
);
if (projectCheck instanceof ProjectNotFound) {
throw projectCheck;
}
const {
jobStatus,
requestedAt,
toDeploymentId,
type,
}: Partial<LastAliasRequest> = projectCheck.lastAliasRequest ?? {};
if (
!jobStatus ||
@@ -89,12 +99,17 @@ export default async function rollbackStatus({
output.log(`${spinnerMessage}`);
}
if (!jobStatus || requestedAt < recentThreshold) {
if (
!jobStatus ||
!requestedAt ||
!toDeploymentId ||
requestedAt < recentThreshold
) {
output.log('No deployment rollback in progress');
return 0;
}
if (jobStatus === 'skipped') {
if (jobStatus === 'skipped' && type === 'rollback') {
output.log('Rollback was skipped');
return 0;
}
@@ -131,7 +146,7 @@ export default async function rollbackStatus({
if (requestedAt < recentThreshold || Date.now() >= rollbackTimeout) {
output.log(
`The rollback exceeded its deadline - rerun ${chalk.bold(
`vercel rollback ${toDeploymentId}`
`${getPkgName()} rollback ${toDeploymentId}`
)} to try again`
);
return 1;

View File

@@ -144,12 +144,6 @@ const main = async () => {
return 1;
}
let cwd = argv['--cwd'];
if (cwd) {
process.chdir(cwd);
}
cwd = process.cwd();
// The second argument to the command can be:
//
// * a path to deploy (as in: `vercel path/`)
@@ -277,6 +271,12 @@ const main = async () => {
argv: process.argv,
});
// The `--cwd` flag is respected for all sub-commands
if (argv['--cwd']) {
client.cwd = argv['--cwd'];
}
const { cwd } = client;
// Gets populated to the subcommand name when a built-in is
// provided, otherwise it remains undefined for an extension
let subcommand: string | undefined = undefined;
@@ -571,6 +571,9 @@ const main = async () => {
case 'project':
func = require('./commands/project').default;
break;
case 'promote':
func = require('./commands/promote').default;
break;
case 'pull':
func = require('./commands/pull').default;
break;

View File

@@ -207,4 +207,12 @@ export default class Client extends EventEmitter implements Stdio {
output: this.stderr as NodeJS.WriteStream,
});
}
get cwd(): string {
return process.cwd();
}
set cwd(v: string) {
process.chdir(v);
}
}

View File

@@ -12,21 +12,21 @@ export default async function createDeploy(
client: Client,
now: Now,
contextName: string,
paths: string[],
path: string,
createArgs: CreateOptions,
org: Org,
isSettingUpProject: boolean,
cwd?: string,
cwd: string,
archive?: ArchiveFormat
): Promise<any | DeploymentError> {
try {
return await now.create(
paths,
path,
createArgs,
org,
isSettingUpProject,
archive,
cwd
cwd,
archive
);
} catch (err: unknown) {
if (ERRORS_TS.isAPIError(err)) {
@@ -109,10 +109,11 @@ export default async function createDeploy(
client,
now,
contextName,
paths,
path,
createArgs,
org,
isSettingUpProject
isSettingUpProject,
cwd
);
}

View File

@@ -40,8 +40,7 @@ export default async function processDeployment({
...args
}: {
now: Now;
output: Output;
paths: string[];
path: string;
requestBody: DeploymentOptions;
uploadStamp: () => string;
deployStamp: () => string;
@@ -54,15 +53,14 @@ export default async function processDeployment({
isSettingUpProject: boolean;
archive?: ArchiveFormat;
skipAutoDetectionConfirmation?: boolean;
cwd?: string;
cwd: string;
rootDirectory?: string | null;
noWait?: boolean;
agent?: Agent;
}) {
let {
now,
output,
paths,
path,
requestBody,
deployStamp,
force,
@@ -72,10 +70,9 @@ export default async function processDeployment({
rootDirectory,
} = args;
const { debug } = output;
const client = now._client;
const { output } = client;
const { env = {} } = requestBody;
const token = now._token;
if (!token) {
throw new Error('Missing authentication token');
@@ -87,7 +84,7 @@ export default async function processDeployment({
token,
debug: now._debug,
userAgent: ua,
path: paths[0],
path,
force,
withCache,
prebuilt,
@@ -114,7 +111,7 @@ export default async function processDeployment({
if (event.type === 'file-count') {
const { total, missing, uploads } = event.payload;
debug(`Total files ${total.size}, ${missing.length} changed`);
output.debug(`Total files ${total.size}, ${missing.length} changed`);
const missingSize = missing
.map((sha: string) => total.get(sha).data.length)
@@ -157,7 +154,7 @@ export default async function processDeployment({
}
if (event.type === 'file-uploaded') {
debug(
output.debug(
`Uploaded: ${event.payload.file.names.join(' ')} (${bytes(
event.payload.file.data.length
)})`
@@ -166,8 +163,8 @@ export default async function processDeployment({
if (event.type === 'created') {
await linkFolderToProject(
output,
cwd || paths[0],
client,
cwd,
{
orgId: org.id,
projectId: event.payload.projectId,

View File

@@ -107,7 +107,7 @@ export default class Now extends EventEmitter {
}
async create(
paths: string[],
path: string,
{
// Legacy
nowConfig: nowConfig = {},
@@ -135,8 +135,8 @@ export default class Now extends EventEmitter {
}: CreateOptions,
org: Org,
isSettingUpProject: boolean,
archive?: ArchiveFormat,
cwd?: string
cwd: string,
archive?: ArchiveFormat
) {
let hashes: any = {};
const uploadStamp = stamp();
@@ -163,9 +163,8 @@ export default class Now extends EventEmitter {
const deployment = await processDeployment({
now: this,
output: this._output,
agent: this._client.agent,
paths,
path,
requestBody,
uploadStamp,
deployStamp,

View File

@@ -220,8 +220,20 @@ export async function findRepoRoot(start: string): Promise<string | undefined> {
break;
}
// if `.vercel/repo.json` exists (already linked),
// then consider this the repo root
const repoConfigPath = join(current, VERCEL_DIR, VERCEL_DIR_REPO);
let stat = await lstat(repoConfigPath).catch(err => {
if (err.code !== 'ENOENT') throw err;
});
if (stat) {
return current;
}
// if `.git/config` exists (unlinked),
// then consider this the repo root
const gitConfigPath = join(current, '.git/config');
const stat = await lstat(gitConfigPath).catch(err => {
stat = await lstat(gitConfigPath).catch(err => {
if (err.code !== 'ENOENT') throw err;
});
if (stat) {
@@ -247,19 +259,19 @@ function sortByDirectory(a: RepoProjectConfig, b: RepoProjectConfig): number {
}
/**
* Finds the matching Project from an array of Project links
* Finds the matching Projects from an array of Project links
* where the provided relative path is within the Project's
* root directory.
*/
export function findProjectFromPath(
export function findProjectsFromPath(
projects: RepoProjectConfig[],
path: string
): RepoProjectConfig | undefined {
): RepoProjectConfig[] {
const normalizedPath = normalizePath(path);
return projects
.slice()
.sort(sortByDirectory)
.find(project => {
.filter(project => {
if (project.directory === '.') {
// Project has no "Root Directory" setting, so any path is valid
return true;
@@ -270,3 +282,13 @@ export function findProjectFromPath(
);
});
}
/**
* TODO: remove
*/
export function findProjectFromPath(
projects: RepoProjectConfig[],
path: string
): RepoProjectConfig | undefined {
return findProjectsFromPath(projects, path)[0];
}

View File

@@ -133,7 +133,7 @@ export default async function setupAndLink(
const project = projectOrNewProjectName;
await linkFolderToProject(
output,
client,
path,
{
projectId: project.id,
@@ -207,7 +207,7 @@ export default async function setupAndLink(
client,
now,
config.currentTeam || 'current user',
[sourcePath],
sourcePath,
createArgs,
org,
true,
@@ -251,7 +251,7 @@ export default async function setupAndLink(
Object.assign(project, settings);
await linkFolderToProject(
output,
client,
path,
{
projectId: project.id,

View File

@@ -0,0 +1,40 @@
import type Client from '../client';
import { ProjectNotFound } from '../errors-ts';
import { ensureLink } from '../link/ensure-link';
import getProjectByNameOrId from './get-project-by-id-or-name';
import type { Project } from '@vercel-internals/types';
export default async function getProjectByCwdOrLink({
autoConfirm,
client,
commandName,
cwd,
projectNameOrId,
}: {
autoConfirm?: boolean;
client: Client;
commandName: string;
cwd: string;
projectNameOrId?: string;
}): Promise<Project> {
if (projectNameOrId) {
const project = await getProjectByNameOrId(client, projectNameOrId);
if (project instanceof ProjectNotFound) {
throw project;
}
return project;
}
// ensure the current directory is a linked project
const linkedProject = await ensureLink(commandName, client, cwd, {
autoConfirm,
});
if (typeof linkedProject === 'number') {
const err: NodeJS.ErrnoException = new Error('Link project error');
err.code = 'ERR_LINK_PROJECT';
throw err;
}
return linkedProject.project;
}

View File

@@ -0,0 +1,104 @@
import chalk from 'chalk';
import type Client from '../client';
import type { Deployment, Project, Team } from '@vercel-internals/types';
import getDeployment from '../get-deployment';
import getProjectByNameOrId from './get-project-by-id-or-name';
import getScope from '../get-scope';
import getTeamById from '../teams/get-team-by-id';
import { isValidName } from '../is-valid-name';
import { Output } from '../output';
import { ProjectNotFound } from '../errors-ts';
export default async function getProjectByDeployment({
client,
deployId,
output,
}: {
client: Client;
deployId: string;
output?: Output;
}): Promise<{
contextName: string;
deployment: Deployment;
project: Project;
}> {
const { config } = client;
const { contextName } = await getScope(client);
if (!isValidName(deployId)) {
throw new Error(
`The provided argument "${deployId}" is not a valid deployment ID or URL`
);
}
let deployment: Deployment;
let team: Team | undefined;
try {
output?.spinner(
`Fetching deployment "${deployId}" in ${chalk.bold(contextName)}`
);
const [teamResult, deploymentResult] = await Promise.allSettled([
config.currentTeam ? getTeamById(client, config.currentTeam) : undefined,
getDeployment(client, contextName, deployId),
]);
if (teamResult.status === 'rejected') {
throw new Error(
`Failed to retrieve team information: ${teamResult.reason}`
);
}
if (deploymentResult.status === 'rejected') {
throw new Error(deploymentResult.reason);
}
team = teamResult.value;
deployment = deploymentResult.value;
// re-render the spinner text
output?.log(
`Fetching deployment "${deployId}" in ${chalk.bold(contextName)}`
);
if (deployment.team?.id) {
if (!team || deployment.team.id !== team.id) {
const err: NodeJS.ErrnoException = new Error(
team
? `Deployment doesn't belong to current team ${chalk.bold(
contextName
)}`
: `Deployment belongs to a different team`
);
err.code = 'ERR_INVALID_TEAM';
throw err;
}
}
if (team) {
const err: NodeJS.ErrnoException = new Error(
`Deployment doesn't belong to current team ${chalk.bold(contextName)}`
);
err.code = 'ERR_INVALID_TEAM';
throw err;
}
if (!deployment.projectId) {
throw new Error('Deployment is not associated to a project');
}
const project = await getProjectByNameOrId(client, deployment.projectId);
if (project instanceof ProjectNotFound) {
throw project;
}
return {
contextName,
deployment,
project,
};
} finally {
output?.stopSpinner();
}
}

View File

@@ -5,11 +5,13 @@ import { isAPIError, ProjectNotFound } from '../errors-ts';
export default async function getProjectByNameOrId(
client: Client,
projectNameOrId: string,
accountId?: string
accountId?: string,
includeRollbackInfo?: boolean
) {
try {
const qs = includeRollbackInfo ? '?rollbackInfo=true' : '';
const project = await client.fetch<Project>(
`/v8/projects/${encodeURIComponent(projectNameOrId)}`,
`/v9/projects/${encodeURIComponent(projectNameOrId)}${qs}`,
{ accountId }
);
return project;

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import AJV from 'ajv';
import chalk from 'chalk';
import { join, relative } from 'path';
import { ensureDir, readJSON } from 'fs-extra';
import { ensureDir } from 'fs-extra';
import { promisify } from 'util';
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
@@ -10,7 +10,6 @@ import Client from '../client';
import { InvalidToken, isAPIError, ProjectNotFound } from '../errors-ts';
import getUser from '../get-user';
import getTeamById from '../teams/get-team-by-id';
import { Output } from '../output';
import type {
Project,
ProjectLinkResult,
@@ -22,8 +21,9 @@ import { isDirectory } from '../config/global-path';
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
import outputCode from '../output/code';
import { isErrnoException, isError } from '@vercel/error-utils';
import { findProjectFromPath, findRepoRoot } from '../link/repo';
import { findProjectsFromPath, getRepoLink } from '../link/repo';
import { addToGitIgnore } from '../link/add-to-gitignore';
import type { RepoProjectConfig } from '../link/repo';
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
@@ -69,38 +69,49 @@ export function getVercelDirectory(cwd: string): string {
return existingDirs[0] || possibleDirs[0];
}
async function getLink(path: string): Promise<ProjectLink | null> {
// Try the individual project linking style (`${cwd}/.vercel/project.json`)
const dir = getVercelDirectory(path);
const linkFromProject = await getLinkFromDir(dir);
if (linkFromProject) {
return linkFromProject;
}
// Try the repo linking style (`${repoRoot}/.vercel/repo.json`)
return getLinkFromRepo(path);
async function getProjectLink(
client: Client,
path: string
): Promise<ProjectLink | null> {
return (
(await getProjectLinkFromRepoLink(client, path)) ||
(await getLinkFromDir(getVercelDirectory(path)))
);
}
async function getLinkFromRepo(path: string): Promise<ProjectLink | null> {
const repoRoot = await findRepoRoot(path);
if (!repoRoot) {
async function getProjectLinkFromRepoLink(
client: Client,
path: string
): Promise<ProjectLink | null> {
const repoLink = await getRepoLink(path);
if (!repoLink?.repoConfig) {
return null;
}
try {
const vercelDir = join(repoRoot, VERCEL_DIR);
const repoJsonPath = join(vercelDir, VERCEL_DIR_REPO);
const repoJson = await readJSON(repoJsonPath);
const project = findProjectFromPath(
repoJson.projects,
relative(repoRoot, path)
);
if (project) {
return { orgId: repoJson.orgId, projectId: project.id };
}
} catch (err: unknown) {
if (!isErrnoException(err) || err.code !== 'ENOENT') {
throw err;
}
const projects = findProjectsFromPath(
repoLink.repoConfig.projects,
relative(repoLink.rootPath, path)
);
let project: RepoProjectConfig | undefined;
if (projects.length === 1) {
project = projects[0];
} else {
const { p } = await client.prompt({
name: 'p',
type: 'list',
message: `Please select a Project:`,
choices: repoLink.repoConfig.projects.map(p => ({
value: p,
name: p.name,
})),
});
project = p;
}
if (project) {
return {
orgId: repoLink.repoConfig.orgId,
projectId: project.id,
repoRoot: repoLink.rootPath,
};
}
return null;
}
@@ -154,9 +165,45 @@ async function getOrgById(client: Client, orgId: string): Promise<Org | null> {
return { type: 'user', id: orgId, slug: user.username };
}
async function hasProjectLink(
projectLink: ProjectLink,
path: string
): Promise<boolean> {
// "linked" via env vars?
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
if (
VERCEL_ORG_ID === projectLink.orgId &&
VERCEL_PROJECT_ID === projectLink.projectId
) {
return true;
}
// linked via `repo.json`?
const repoLink = await getRepoLink(path);
if (
repoLink?.repoConfig?.orgId === projectLink.orgId &&
repoLink.repoConfig.projects.find(p => p.id === projectLink.projectId)
) {
return true;
}
// if the project is already linked, we skip linking
const link = await getLinkFromDir(getVercelDirectory(path));
if (
link &&
link.orgId === projectLink.orgId &&
link.projectId === projectLink.projectId
) {
return true;
}
return false;
}
export async function getLinkedProject(
client: Client,
path = process.cwd()
path = client.cwd
): Promise<ProjectLinkResult> {
const { output } = client;
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
@@ -177,7 +224,7 @@ export async function getLinkedProject(
const link =
VERCEL_ORG_ID && VERCEL_PROJECT_ID
? { orgId: VERCEL_ORG_ID, projectId: VERCEL_PROJECT_ID }
: await getLink(path);
: await getProjectLink(client, path);
if (!link) {
return { status: 'not_linked', org: null, project: null };
@@ -234,32 +281,19 @@ export async function getLinkedProject(
return { status: 'not_linked', org: null, project: null };
}
return { status: 'linked', org, project };
return { status: 'linked', org, project, repoRoot: link.repoRoot };
}
export async function linkFolderToProject(
output: Output,
client: Client,
path: string,
projectLink: ProjectLink,
projectName: string,
orgSlug: string,
successEmoji: EmojiLabel = 'link'
) {
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
// if defined, skip linking
if (VERCEL_ORG_ID || VERCEL_PROJECT_ID) {
return;
}
// if the project is already linked, we skip linking
const link = await getLink(path);
if (
link &&
link.orgId === projectLink.orgId &&
link.projectId === projectLink.projectId
) {
if (await hasProjectLink(projectLink, path)) {
return;
}
@@ -287,7 +321,7 @@ export async function linkFolderToProject(
// update .gitignore
const isGitIgnoreUpdated = await addToGitIgnore(path);
output.print(
client.output.print(
prependEmoji(
`Linked to ${chalk.bold(
`${orgSlug}/${projectName}`

View File

@@ -1,75 +0,0 @@
import chalk from 'chalk';
import type Client from '../client';
import type { Project } from '@vercel-internals/types';
import { getCommandName } from '../pkg-name';
import { getDeploymentByIdOrURL } from '../deploy/get-deployment-by-id-or-url';
import getScope from '../get-scope';
import { isErrnoException } from '@vercel/error-utils';
import ms from 'ms';
import rollbackStatus from './status';
/**
* Requests a rollback and waits for it complete.
* @param {Client} client - The Vercel client instance
* @param {string} deployIdOrUrl - The deployment name or id to rollback
* @param {Project} project - Project info instance
* @param {string} [timeout] - Time to poll for succeeded/failed state
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async function requestRollback({
client,
deployIdOrUrl,
project,
timeout,
}: {
client: Client;
deployIdOrUrl: string;
project: Project;
timeout?: string;
}): Promise<number> {
const { output } = client;
const { contextName } = await getScope(client);
try {
const deployment = await getDeploymentByIdOrURL({
client,
contextName,
deployIdOrUrl,
});
// create the rollback
await client.fetch(`/v9/projects/${project.id}/rollback/${deployment.id}`, {
body: {}, // required
method: 'POST',
});
if (timeout !== undefined && ms(timeout) === 0) {
output.log(
`Successfully requested rollback of ${chalk.bold(project.name)} to ${
deployment.url
} (${deployment.id})`
);
output.log(
`To check rollback status, run ${getCommandName('rollback')}.`
);
return 0;
}
// check the status
return await rollbackStatus({
client,
contextName,
deployment,
project,
timeout,
});
} catch (err: unknown) {
output.prettyError(err);
if (isErrnoException(err) && err.code === 'ERR_INVALID_TEAM') {
output.error(
`Use ${chalk.bold('vc switch')} to change your current team`
);
}
return 1;
}
}

View File

@@ -1,5 +1,4 @@
import { lstat as lstatRaw } from 'fs';
import { promisify } from 'util';
import { lstat } from 'fs-extra';
import { Output } from './output';
import chalk from 'chalk';
import { homedir } from 'os';
@@ -7,8 +6,6 @@ import confirm from './input/confirm';
import toHumanPath from './humanize-path';
import Client from './client';
const stat = promisify(lstatRaw);
/**
* A helper function to validate the `rootDirectory` input.
*/
@@ -18,7 +15,7 @@ export async function validateRootDirectory(
path: string,
errorSuffix: string
) {
const pathStat = await stat(path).catch(() => null);
const pathStat = await lstat(path).catch(() => null);
const suffix = errorSuffix ? ` ${errorSuffix}` : '';
if (!pathStat) {
@@ -66,7 +63,7 @@ export default async function validatePaths(
const path = paths[0];
// can only deploy a directory
const pathStat = await stat(path).catch(() => null);
const pathStat = await lstat(path).catch(() => null);
if (!pathStat) {
output.error(`Could not find ${chalk.cyan(`${toHumanPath(path)}`)}`);

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,11 @@
{
"orgId": "team_dummy",
"remoteName": "origin",
"projects": [
{
"id": "QmbKpqpiUqbcke",
"name": "app",
"directory": "app"
}
]
}

View File

@@ -0,0 +1 @@
<h1>hi</h1>

View File

@@ -0,0 +1,3 @@
body {
background-color: red;
}

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "vercel-promote"
}

View File

@@ -0,0 +1,12 @@
{
"scripts": {
"build": "next build",
"dev": "next",
"now-build": "next build"
},
"dependencies": {
"next": "13.4.3",
"react": "latest",
"react-dom": "latest"
}
}

View File

@@ -0,0 +1,10 @@
{
"version": 2,
"name": "vercel-promote",
"routes": [
{
"src": "/(.*)",
"dest": "/index?route-param=b"
}
]
}

View File

@@ -0,0 +1,3 @@
.next
yarn.lock
!.vercel

View File

@@ -0,0 +1,11 @@
import { withRouter } from 'next/router';
function Index({ router }) {
const data = {
pathname: router.pathname,
query: router.query,
};
return <div>{JSON.stringify(data)}</div>;
}
export default withRouter(Index);

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,21 @@
{
"orgId": "team_dummy",
"remoteName": "origin",
"projects": [
{
"id": "QmbKpqpiUqbcke",
"name": "monorepo-dashboard",
"directory": "dashboard"
},
{
"id": "QmX6P93ChNDoZP",
"name": "monorepo-marketing",
"directory": "marketing"
},
{
"id": "QmScb7GPQt6gsS",
"name": "monorepo-blog",
"directory": "blog"
}
]
}

View File

@@ -34,16 +34,19 @@ export function setupUnitFixture(fixtureName: string) {
);
}
const cwd = setupTmpDir(fixtureName);
fs.copySync(fixturePath, cwd);
return cwd;
}
export function setupTmpDir(fixtureName?: string) {
if (!tempRoot) {
// Create a shared root temp directory for fixture files
tempRoot = tmp.dirSync({ unsafeCleanup: true }); // clean up even if files are left
}
const cwd = path.join(tempRoot.name, String(tempNumber++), fixtureName);
const cwd = path.join(tempRoot.name, String(tempNumber++), fixtureName ?? '');
fs.mkdirpSync(cwd);
fs.copySync(fixturePath, cwd);
return cwd;
}

View File

@@ -12,6 +12,7 @@ import retry from 'async-retry';
import fs, { ensureDir } from 'fs-extra';
import logo from '../src/util/output/logo';
import sleep from '../src/util/sleep';
import humanizePath from '../src/util/humanize-path';
import pkg from '../package.json';
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
import waitForPrompt from './helpers/wait-for-prompt';
@@ -819,7 +820,9 @@ test('create a production deployment', async () => {
});
test('try to deploy non-existing path', async () => {
const goal = `Error: The specified file or directory "${session}" does not exist.`;
const goal = `Error: Could not find “${humanizePath(
path.join(process.cwd(), session)
)}`;
const { stderr, stdout, exitCode } = await execCli(binaryPath, [
session,
@@ -1201,9 +1204,13 @@ test('vercel hasOwnProperty not a valid subcommand', async () => {
const output = await execCli(binaryPath, ['hasOwnProperty']);
expect(output.exitCode, formatOutput(output)).toBe(1);
expect(output.stderr).toMatch(
/The specified file or directory "hasOwnProperty" does not exist/gm
);
expect(
output.stderr.endsWith(
`Error: Could not find “${humanizePath(
path.join(process.cwd(), 'hasOwnProperty')
)}`
)
).toEqual(true);
});
test('create zero-config deployment', async () => {

View File

@@ -1,3 +1,5 @@
const originalCwd = process.cwd();
// Register Jest matcher extensions for CLI unit tests
import './matchers';
@@ -73,6 +75,8 @@ export class MockClient extends Client {
});
this.scenario = Router();
this.reset();
}
reset() {
@@ -99,11 +103,14 @@ export class MockClient extends Client {
};
this.config = {};
this.localConfig = {};
this.localConfigPath = undefined;
this.scenario = Router();
this.agent?.destroy();
this.agent = undefined;
this.cwd = originalCwd;
}
async startMockServer() {
@@ -156,7 +163,7 @@ beforeAll(async () => {
await client.startMockServer();
});
beforeEach(() => {
afterEach(() => {
client.reset();
});

View File

@@ -3,6 +3,7 @@ import chance from 'chance';
import { client } from './client';
import { Build, Deployment, User } from '@vercel-internals/types';
import type { Request, Response } from 'express';
import { defaultProject } from './project';
let deployments = new Map<string, Deployment>();
let deploymentBuilds = new Map<Deployment, Build[]>();
@@ -16,6 +17,7 @@ export function useDeployment({
creator,
state = 'READY',
createdAt,
project = defaultProject,
}: {
creator: Pick<User, 'id' | 'email' | 'name' | 'username'>;
state?:
@@ -26,6 +28,7 @@ export function useDeployment({
| 'READY'
| 'CANCELED';
createdAt?: number;
project: any; // FIX ME: Use `Project` once PR #9956 is merged
}) {
setupDeploymentEndpoints();
@@ -53,6 +56,7 @@ export function useDeployment({
name,
ownerId: creator.id,
plan: 'hobby',
projectId: project.id,
public: false,
ready: createdAt + 30000,
readyState: state,

View File

@@ -56,7 +56,7 @@ export async function toOutput(
resolve({
pass: false,
message() {
return `${hint}Timed out waiting ${timeout} ms for output`;
return `${hint}Timed out waiting ${timeout} ms for output.\n\nExpected: "${test}"\nReceived: "${output}"`;
},
});
}

View File

@@ -128,7 +128,7 @@ export const defaultProject: Project = {
url: 'a-project-name-rjtr4pz3f.vercel.app',
},
],
lastRollbackTarget: null,
lastAliasRequest: null,
alias: [
{
domain: 'foobar.com',
@@ -205,10 +205,10 @@ export function useProject(
project: Partial<Project> = defaultProject,
projectEnvs: ProjectEnvVariable[] = envs
) {
client.scenario.get(`/v8/projects/${project.name}`, (_req, res) => {
client.scenario.get(`/:version/projects/${project.name}`, (_req, res) => {
res.json(project);
});
client.scenario.get(`/v8/projects/${project.id}`, (_req, res) => {
client.scenario.get(`/:version/projects/${project.id}`, (_req, res) => {
res.json(project);
});
client.scenario.patch(`/:version/projects/${project.id}`, (req, res) => {

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import { setupUnitFixture } from '../../helpers/setup-unit-fixture';
import { defaultProject, useProject } from '../../mocks/project';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
import humanizePath from '../../../src/util/humanize-path';
describe('deploy', () => {
it('should reject deploying a single file', async () => {
@@ -30,10 +31,13 @@ describe('deploy', () => {
});
it('should reject deploying a directory that does not exist', async () => {
client.setArgv('deploy', 'does-not-exists');
const badName = 'does-not-exist';
client.setArgv('deploy', badName);
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
`Error: The specified file or directory "does-not-exists" does not exist.\n`
`Error: Could not find “${humanizePath(
join(client.cwd, 'does-not-exist')
)}\n`
);
await expect(exitCodePromise).resolves.toEqual(1);
});
@@ -141,232 +145,250 @@ describe('deploy', () => {
});
it('should send a tgz file when `--archive=tgz`', async () => {
const cwd = setupUnitFixture('commands/deploy/static');
const originalCwd = process.cwd();
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'static',
id: 'static',
});
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'static',
id: 'static',
let body: any;
client.scenario.post(`/v13/deployments`, (req, res) => {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
});
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
client.scenario.get(`/v10/now/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
let body: any;
client.scenario.post(`/v13/deployments`, (req, res) => {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
});
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
client.scenario.get(
`/v10/now/deployments/dpl_archive_test`,
(req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
}
);
client.setArgv('deploy', '--archive=tgz');
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body?.files?.length).toEqual(1);
expect(body?.files?.[0].file).toEqual('.vercel/source.tgz');
} finally {
process.chdir(originalCwd);
}
client.cwd = setupUnitFixture('commands/deploy/static');
client.setArgv('deploy', '--archive=tgz');
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body?.files?.length).toEqual(1);
expect(body?.files?.[0].file).toEqual('.vercel/source.tgz');
});
it('should pass flag to skip custom domain assignment', async () => {
const cwd = setupUnitFixture('commands/deploy/static');
const originalCwd = process.cwd();
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'static',
id: 'static',
});
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'static',
id: 'static',
let body: any;
client.scenario.post(`/v13/deployments`, (req, res) => {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
});
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
let body: any;
client.scenario.post(`/v13/deployments`, (req, res) => {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
});
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
client.setArgv('deploy', '--prod', '--skip-domain');
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body).toMatchObject({
target: 'production',
source: 'cli',
autoAssignCustomDomains: false,
version: 2,
});
} finally {
process.chdir(originalCwd);
}
client.cwd = setupUnitFixture('commands/deploy/static');
client.setArgv('deploy', '--prod', '--skip-domain');
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body).toMatchObject({
target: 'production',
source: 'cli',
autoAssignCustomDomains: false,
version: 2,
});
});
it('should upload missing files', async () => {
const cwd = setupUnitFixture('commands/deploy/static');
const originalCwd = process.cwd();
client.cwd = cwd;
// Add random 1mb file
await fs.writeFile(join(cwd, 'data'), randomBytes(bytes('1mb')));
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'static',
id: 'static',
});
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'static',
id: 'static',
});
let body: any;
let fileUploaded = false;
client.scenario.post(`/v13/deployments`, (req, res) => {
if (fileUploaded) {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
});
} else {
const sha = req.body.files[0].sha;
res.status(400).json({
error: {
code: 'missing_files',
message: 'Missing files',
missing: [sha],
},
});
}
});
client.scenario.post('/v2/files', (req, res) => {
// Wait for file to be finished uploading
req.on('data', () => {
// Noop
});
req.on('end', () => {
fileUploaded = true;
res.end();
});
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
let body: any;
let fileUploaded = false;
client.scenario.post(`/v13/deployments`, (req, res) => {
if (fileUploaded) {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
} else {
const sha = req.body.files[0].sha;
res.status(400).json({
error: {
code: 'missing_files',
message: 'Missing files',
missing: [sha],
},
});
}
});
client.scenario.post('/v2/files', (req, res) => {
// Wait for file to be finished uploading
req.on('data', () => {
// Noop
});
client.scenario.get(
`/v10/now/deployments/dpl_archive_test`,
(req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
}
);
// When stderr is not a TTY we expect 5 progress lines to be printed
client.stderr.isTTY = false;
client.setArgv('deploy', '--archive=tgz');
const uploadingLines: string[] = [];
client.stderr.on('data', data => {
if (data.startsWith('Uploading [')) {
uploadingLines.push(data);
}
req.on('end', () => {
fileUploaded = true;
res.end();
});
client.stderr.resume();
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body?.files?.length).toEqual(1);
expect(body?.files?.[0].file).toEqual('.vercel/source.tgz');
expect(uploadingLines.length).toEqual(5);
expect(
uploadingLines[0].startsWith('Uploading [--------------------]')
).toEqual(true);
expect(
uploadingLines[1].startsWith('Uploading [=====---------------]')
).toEqual(true);
expect(
uploadingLines[2].startsWith('Uploading [==========----------]')
).toEqual(true);
expect(
uploadingLines[3].startsWith('Uploading [===============-----]')
).toEqual(true);
expect(
uploadingLines[4].startsWith('Uploading [====================]')
).toEqual(true);
} finally {
process.chdir(originalCwd);
}
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
client.scenario.get(`/v10/now/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
// When stderr is not a TTY we expect 5 progress lines to be printed
client.stderr.isTTY = false;
client.setArgv('deploy', '--archive=tgz');
const uploadingLines: string[] = [];
client.stderr.on('data', data => {
if (data.startsWith('Uploading [')) {
uploadingLines.push(data);
}
});
client.stderr.resume();
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body?.files?.length).toEqual(1);
expect(body?.files?.[0].file).toEqual('.vercel/source.tgz');
expect(uploadingLines.length).toEqual(5);
expect(
uploadingLines[0].startsWith('Uploading [--------------------]')
).toEqual(true);
expect(
uploadingLines[1].startsWith('Uploading [=====---------------]')
).toEqual(true);
expect(
uploadingLines[2].startsWith('Uploading [==========----------]')
).toEqual(true);
expect(
uploadingLines[3].startsWith('Uploading [===============-----]')
).toEqual(true);
expect(
uploadingLines[4].startsWith('Uploading [====================]')
).toEqual(true);
});
it('should deploy project linked with `repo.json`', async () => {
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
name: 'app',
id: 'QmbKpqpiUqbcke',
});
let body: any;
client.scenario.post(`/v13/deployments`, (req, res) => {
body = req.body;
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
});
});
client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => {
res.json({
creator: {
uid: user.id,
username: user.username,
},
id: 'dpl_archive_test',
readyState: 'READY',
aliasAssigned: true,
alias: [],
});
});
const repoRoot = setupUnitFixture('commands/deploy/monorepo-static');
client.cwd = join(repoRoot, 'app');
client.setArgv('deploy');
const exitCode = await deploy(client);
expect(exitCode).toEqual(0);
expect(body).toMatchObject({
source: 'cli',
version: 2,
});
});
});

View File

@@ -11,7 +11,6 @@ import { useUser } from '../../mocks/user';
describe('env', () => {
describe('pull', () => {
it('should handle pulling', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -19,7 +18,9 @@ describe('env', () => {
id: 'vercel-env-pull',
name: 'vercel-env-pull',
});
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv('env', 'pull', '--yes');
const exitCodePromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `development` Environment Variables for Project vercel-env-pull'
@@ -35,7 +36,6 @@ describe('env', () => {
});
it('should handle pulling from Preview env vars', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -43,15 +43,9 @@ describe('env', () => {
id: 'vercel-env-pull',
name: 'vercel-env-pull',
});
client.setArgv(
'env',
'pull',
'--yes',
'--cwd',
cwd,
'--environment',
'preview'
);
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv('env', 'pull', '--yes', '--environment', 'preview');
const exitCodePromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `preview` Environment Variables for Project vercel-env-pull'
@@ -70,7 +64,6 @@ describe('env', () => {
});
it('should handle pulling from specific Git branch', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -78,12 +71,12 @@ describe('env', () => {
id: 'vercel-env-pull',
name: 'vercel-env-pull',
});
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv(
'env',
'pull',
'--yes',
'--cwd',
cwd,
'--environment',
'preview',
'--git-branch',
@@ -114,7 +107,6 @@ describe('env', () => {
});
it('should handle alternate filename', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -122,7 +114,9 @@ describe('env', () => {
id: 'vercel-env-pull',
name: 'vercel-env-pull',
});
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv('env', 'pull', 'other.env', '--yes');
const exitCodePromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `development` Environment Variables for Project vercel-env-pull'
@@ -138,7 +132,6 @@ describe('env', () => {
});
it('should use given environment', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -146,15 +139,9 @@ describe('env', () => {
id: 'vercel-env-pull',
name: 'vercel-env-pull',
});
client.setArgv(
'env',
'pull',
'--environment',
'production',
'--cwd',
cwd
);
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv('env', 'pull', '--environment', 'production');
const exitCodePromise = env(client);
await expect(client.stderr).toOutput(
`Downloading \`production\` Environment Variables for Project vercel-env-pull`
@@ -172,7 +159,6 @@ describe('env', () => {
});
it('should throw an error when it does not recognize given environment', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -180,15 +166,14 @@ describe('env', () => {
id: 'vercel-env-pull',
name: 'vercel-env-pull',
});
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv(
'env',
'pull',
'.env.production',
'--environment',
'something-invalid',
'--cwd',
cwd
'something-invalid'
);
const exitCodePromise = env(client);
@@ -200,7 +185,6 @@ describe('env', () => {
});
it('should expose production system env variables', async () => {
const cwd = setupUnitFixture('vercel-env-pull');
useUser();
useTeams('team_dummy');
useProject({
@@ -209,8 +193,9 @@ describe('env', () => {
name: 'vercel-env-pull',
autoExposeSystemEnvs: true,
});
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
const cwd = setupUnitFixture('vercel-env-pull');
client.cwd = cwd;
client.setArgv('env', 'pull', 'other.env', '--yes');
const exitCodePromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `development` Environment Variables for Project vercel-env-pull'
@@ -228,6 +213,7 @@ describe('env', () => {
it('should show a delta string', async () => {
const cwd = setupUnitFixture('vercel-env-pull-delta');
client.cwd = cwd;
try {
useUser();
useTeams('team_dummy');
@@ -237,7 +223,7 @@ describe('env', () => {
name: 'env-pull-delta',
});
client.setArgv('env', 'add', 'NEW_VAR', '--cwd', cwd);
client.setArgv('env', 'add', 'NEW_VAR');
const addPromise = env(client);
await expect(client.stderr).toOutput('Whats the value of NEW_VAR?');
@@ -253,7 +239,7 @@ describe('env', () => {
await expect(addPromise).resolves.toEqual(0);
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
client.setArgv('env', 'pull', '--yes');
const pullPromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `development` Environment Variables for Project env-pull-delta'
@@ -265,13 +251,12 @@ describe('env', () => {
await expect(pullPromise).resolves.toEqual(0);
} finally {
client.setArgv('env', 'rm', 'NEW_VAR', '--yes', '--cwd', cwd);
client.setArgv('env', 'rm', 'NEW_VAR', '--yes');
await env(client);
}
});
it('should not show a delta string when it fails to read a file', async () => {
const cwd = setupUnitFixture('vercel-env-pull-delta-corrupt');
useUser();
useTeams('team_dummy');
useProject({
@@ -279,15 +264,15 @@ describe('env', () => {
id: 'env-pull-delta-corrupt',
name: 'env-pull-delta-corrupt',
});
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
const cwd = setupUnitFixture('vercel-env-pull-delta-corrupt');
client.cwd = cwd;
client.setArgv('env', 'pull', '--yes');
const pullPromise = env(client);
await expect(client.stderr).toOutput('Updated .env.local file');
await expect(pullPromise).resolves.toEqual(0);
});
it('should show that no changes were found', async () => {
const cwd = setupUnitFixture('vercel-env-pull-delta-no-changes');
useUser();
useTeams('team_dummy');
useProject({
@@ -295,8 +280,8 @@ describe('env', () => {
id: 'env-pull-delta-no-changes',
name: 'env-pull-delta-no-changes',
});
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
client.cwd = setupUnitFixture('vercel-env-pull-delta-no-changes');
client.setArgv('env', 'pull', '--yes');
const pullPromise = env(client);
await expect(client.stderr).toOutput('> No changes found.');
await expect(client.stderr).toOutput('Updated .env.local file');
@@ -305,6 +290,7 @@ describe('env', () => {
it('should correctly render delta string when env variable has quotes', async () => {
const cwd = setupUnitFixture('vercel-env-pull-delta-quotes');
client.cwd = cwd;
try {
useUser();
useTeams('team_dummy');
@@ -329,7 +315,7 @@ describe('env', () => {
]
);
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
client.setArgv('env', 'pull', '--yes');
const pullPromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `development` Environment Variables for Project env-pull-delta'
@@ -339,13 +325,14 @@ describe('env', () => {
await expect(pullPromise).resolves.toEqual(0);
} finally {
client.setArgv('env', 'rm', 'NEW_VAR', '--yes', '--cwd', cwd);
client.setArgv('env', 'rm', 'NEW_VAR', '--yes');
await env(client);
}
});
it('should correctly render delta string when local env variable has quotes', async () => {
const cwd = setupUnitFixture('vercel-env-pull-delta-quotes');
client.cwd = cwd;
try {
useUser();
useTeams('team_dummy');
@@ -370,7 +357,7 @@ describe('env', () => {
]
);
client.setArgv('env', 'pull', '.env.testquotes', '--yes', '--cwd', cwd);
client.setArgv('env', 'pull', '.env.testquotes', '--yes');
const pullPromise = env(client);
await expect(client.stderr).toOutput(
'Downloading `development` Environment Variables for Project env-pull-delta'
@@ -380,7 +367,7 @@ describe('env', () => {
await expect(pullPromise).resolves.toEqual(0);
} finally {
client.setArgv('env', 'rm', 'NEW_VAR', '--yes', '--cwd', cwd);
client.setArgv('env', 'rm', 'NEW_VAR', '--yes');
await env(client);
}
});

View File

@@ -9,14 +9,13 @@ import type { Project } from '@vercel-internals/types';
describe('git', () => {
describe('connect', () => {
const originalCwd = process.cwd();
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/git/connect', name);
it('connects an unlinked project', async () => {
const cwd = fixture('unlinked');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -62,13 +61,12 @@ describe('git', () => {
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('connects an unlinked project with a remote url', async () => {
const cwd = fixture('unlinked');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -119,34 +117,28 @@ describe('git', () => {
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail when there is no git config', async () => {
const cwd = fixture('no-git-config');
try {
process.chdir(cwd);
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'no-git-config',
name: 'no-git-config',
});
client.setArgv('git', 'connect', '--yes');
const exitCode = await git(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
`Error: No local Git repository found. Run \`git clone <url>\` to clone a remote Git repository first.\n`
);
} finally {
process.chdir(originalCwd);
}
client.cwd = fixture('no-git-config');
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'no-git-config',
name: 'no-git-config',
});
client.setArgv('git', 'connect', '--yes');
const exitCode = await git(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
`Error: No local Git repository found. Run \`git clone <url>\` to clone a remote Git repository first.\n`
);
});
it('should fail when there is no remote url', async () => {
const cwd = fixture('no-remote-url');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -163,13 +155,12 @@ describe('git', () => {
);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail when the remote url is bad', async () => {
const cwd = fixture('bad-remote-url');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -190,13 +181,12 @@ describe('git', () => {
);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should connect a repo to a project that is not already connected', async () => {
const cwd = fixture('new-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -232,13 +222,12 @@ describe('git', () => {
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should replace an old connection with a new one', async () => {
const cwd = fixture('existing-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -285,13 +274,12 @@ describe('git', () => {
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should exit when an already-connected repo is connected', async () => {
const cwd = fixture('new-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -324,13 +312,12 @@ describe('git', () => {
expect(exitCode).toEqual(1);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail when it cannot find the repository', async () => {
const cwd = fixture('invalid-repo');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -354,13 +341,12 @@ describe('git', () => {
expect(exitCode).toEqual(1);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should connect the default option of multiple remotes', async () => {
const cwd = fixture('multiple-remotes');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -404,19 +390,17 @@ describe('git', () => {
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
});
describe('disconnect', () => {
const originalCwd = process.cwd();
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/git/connect', name);
it('should disconnect a repository', async () => {
const cwd = fixture('new-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -453,13 +437,12 @@ describe('git', () => {
expect(exitCode).toEqual(0);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail if there is no repository to disconnect', async () => {
const cwd = fixture('new-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -480,13 +463,12 @@ describe('git', () => {
expect(exitCode).toEqual(1);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should connect a given repository', async () => {
const cwd = fixture('no-remote-url');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -522,13 +504,12 @@ describe('git', () => {
await expect(gitPromise).resolves.toEqual(0);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should prompt when it finds a repository', async () => {
const cwd = fixture('new-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -571,13 +552,12 @@ describe('git', () => {
await expect(gitPromise).resolves.toEqual(0);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should prompt when it finds multiple remotes', async () => {
const cwd = fixture('multiple-remotes');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -621,13 +601,12 @@ describe('git', () => {
await expect(gitPromise).resolves.toEqual(0);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should continue as normal when input matches single git remote', async () => {
const cwd = fixture('new-connection');
client.cwd = cwd;
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
@@ -663,7 +642,6 @@ describe('git', () => {
await expect(gitPromise).resolves.toEqual(0);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
});

View File

@@ -1,5 +1,5 @@
import { basename, join } from 'path';
import { mkdtemp, readJSON, remove } from 'fs-extra';
import { readJSON } from 'fs-extra';
import link from '../../../src/commands/link';
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
@@ -9,117 +9,100 @@ import {
useProject,
useUnknownProject,
} from '../../mocks/project';
import { tmpdir } from 'os';
import { setupTmpDir } from '../../helpers/setup-unit-fixture';
describe('link', () => {
const origCwd = process.cwd();
it('should prompt for link', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'cli-'));
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
...defaultProject,
id: basename(cwd),
name: basename(cwd),
});
useUnknownProject();
const cwd = setupTmpDir();
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
...defaultProject,
id: basename(cwd),
name: basename(cwd),
});
useUnknownProject();
const exitCodePromise = link(client);
client.cwd = cwd;
const exitCodePromise = link(client);
await expect(client.stderr).toOutput('Set up');
client.stdin.write('y\n');
await expect(client.stderr).toOutput('Set up');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
'Which scope should contain your project?'
);
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
'Which scope should contain your project?'
);
client.stdin.write('y\n');
await expect(client.stderr).toOutput('Link to it?');
client.stdin.write('y\n');
await expect(client.stderr).toOutput('Link to it?');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Linked to ${user.username}/${project.name} (created .vercel and added it to .gitignore)`
);
await expect(client.stderr).toOutput(
`Linked to ${user.username}/${project.name} (created .vercel and added it to .gitignore)`
);
await expect(exitCodePromise).resolves.toEqual(0);
await expect(exitCodePromise).resolves.toEqual(0);
const projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(project.id);
} finally {
process.chdir(origCwd);
await remove(cwd);
}
const projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(project.id);
});
it('should allow specifying `--project` flag', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'cli-'));
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
...defaultProject,
id: basename(cwd),
name: basename(cwd),
});
useUnknownProject();
const cwd = setupTmpDir();
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
...defaultProject,
id: basename(cwd),
name: basename(cwd),
});
useUnknownProject();
client.setArgv('--project', project.name!, '--yes');
const exitCodePromise = link(client);
client.cwd = cwd;
client.setArgv('--project', project.name!, '--yes');
const exitCodePromise = link(client);
await expect(client.stderr).toOutput(
`Linked to ${user.username}/${project.name} (created .vercel and added it to .gitignore)`
);
await expect(client.stderr).toOutput(
`Linked to ${user.username}/${project.name} (created .vercel and added it to .gitignore)`
);
await expect(exitCodePromise).resolves.toEqual(0);
await expect(exitCodePromise).resolves.toEqual(0);
const projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(project.id);
} finally {
process.chdir(origCwd);
await remove(cwd);
}
const projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(project.id);
});
it('should allow overwriting existing link', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'cli-'));
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
const { project: proj1 } = useProject({
...defaultProject,
id: 'one',
name: 'one',
});
const { project: proj2 } = useProject({
...defaultProject,
id: 'two',
name: 'two',
});
useUnknownProject();
const cwd = setupTmpDir();
const user = useUser();
useTeams('team_dummy');
const { project: proj1 } = useProject({
...defaultProject,
id: 'one',
name: 'one',
});
const { project: proj2 } = useProject({
...defaultProject,
id: 'two',
name: 'two',
});
useUnknownProject();
client.setArgv('--project', proj1.name!, '--yes');
await expect(link(client)).resolves.toEqual(0);
client.cwd = cwd;
client.setArgv('--project', proj1.name!, '--yes');
await expect(link(client)).resolves.toEqual(0);
let projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(proj1.id);
let projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(proj1.id);
client.setArgv('--project', proj2.name!, '--yes');
await expect(link(client)).resolves.toEqual(0);
client.setArgv('--project', proj2.name!, '--yes');
await expect(link(client)).resolves.toEqual(0);
projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(proj2.id);
} finally {
process.chdir(origCwd);
await remove(cwd);
}
projectJson = await readJSON(join(cwd, '.vercel/project.json'));
expect(projectJson.orgId).toEqual(user.id);
expect(projectJson.projectId).toEqual(proj2.id);
});
});

View File

@@ -18,179 +18,160 @@ const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/list', name);
describe('list', () => {
const originalCwd = process.cwd();
let teamSlug: string;
it('should get deployments from a project linked by a directory', async () => {
const cwd = fixture('with-team');
try {
process.chdir(cwd);
const user = useUser();
const team = useTeams('team_dummy');
teamSlug = team[0].slug;
useProject({
...defaultProject,
id: 'with-team',
name: 'with-team',
});
const deployment = useDeployment({ creator: user });
const user = useUser();
const team = useTeams('team_dummy');
teamSlug = team[0].slug;
useProject({
...defaultProject,
id: 'with-team',
name: 'with-team',
});
const deployment = useDeployment({ creator: user });
client.cwd = fixture('with-team');
await list(client);
await list(client);
const lines = createLineIterator(client.stderr);
const lines = createLineIterator(client.stderr);
let line = await lines.next();
expect(line.value).toEqual('Retrieving project…');
let line = await lines.next();
expect(line.value).toEqual('Retrieving project…');
line = await lines.next();
expect(line.value).toEqual(`Fetching deployments in ${team[0].slug}`);
line = await lines.next();
expect(line.value).toEqual(`Fetching deployments in ${team[0].slug}`);
line = await lines.next();
const { org } = pluckIdentifiersFromDeploymentList(line.value!);
expect(org).toEqual(team[0].slug);
line = await lines.next();
const { org } = pluckIdentifiersFromDeploymentList(line.value!);
expect(org).toEqual(team[0].slug);
// skip next line
await lines.next();
// skip next line
await lines.next();
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual([
'Age',
'Deployment',
'Status',
'Duration',
'Username',
]);
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual([
'Age',
'Deployment',
'Status',
'Duration',
'Username',
]);
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.shift();
expect(data).toEqual([
`https://${deployment.url}`,
stateString(deployment.state || ''),
getDeploymentDuration(deployment),
user.username,
]);
} finally {
process.chdir(originalCwd);
}
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.shift();
expect(data).toEqual([
`https://${deployment.url}`,
stateString(deployment.state || ''),
getDeploymentDuration(deployment),
user.username,
]);
});
it('should get deployments for linked project where the scope is a user', async () => {
const cwd = fixture('with-team');
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'with-team',
name: 'with-team',
});
const deployment = useDeployment({ creator: user });
const user = useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'with-team',
name: 'with-team',
});
const deployment = useDeployment({ creator: user });
client.cwd = fixture('with-team');
client.setArgv('-S', user.username);
await list(client);
client.setArgv('-S', user.username);
await list(client);
const lines = createLineIterator(client.stderr);
const lines = createLineIterator(client.stderr);
let line = await lines.next();
expect(line.value).toEqual('Retrieving project…');
let line = await lines.next();
expect(line.value).toEqual('Retrieving project…');
line = await lines.next();
expect(line.value).toEqual(`Fetching deployments in ${user.username}`);
line = await lines.next();
expect(line.value).toEqual(`Fetching deployments in ${user.username}`);
line = await lines.next();
const { org } = pluckIdentifiersFromDeploymentList(line.value!);
expect(org).toEqual(user.username);
line = await lines.next();
const { org } = pluckIdentifiersFromDeploymentList(line.value!);
expect(org).toEqual(user.username);
// skip next line
await lines.next();
// skip next line
await lines.next();
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual(['Age', 'Deployment', 'Status', 'Duration']);
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual(['Age', 'Deployment', 'Status', 'Duration']);
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.shift();
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.shift();
expect(data).toEqual([
'https://' + deployment.url,
stateString(deployment.state || ''),
getDeploymentDuration(deployment),
]);
} finally {
process.chdir(originalCwd);
}
expect(data).toEqual([
'https://' + deployment.url,
stateString(deployment.state || ''),
getDeploymentDuration(deployment),
]);
});
it('should get the deployments for a specified project', async () => {
const cwd = fixture('with-team');
try {
process.chdir(cwd);
const user = useUser();
const team = useTeams('team_dummy');
useProject({
...defaultProject,
id: 'with-team',
name: 'with-team',
});
const deployment = useDeployment({ creator: user });
const user = useUser();
const team = useTeams('team_dummy');
useProject({
...defaultProject,
id: 'with-team',
name: 'with-team',
});
const deployment = useDeployment({ creator: user });
client.cwd = fixture('with-team');
client.setArgv(deployment.name);
await list(client);
client.setArgv(deployment.name);
await list(client);
const lines = createLineIterator(client.stderr);
const lines = createLineIterator(client.stderr);
let line = await lines.next();
expect(line.value).toEqual('Retrieving project…');
let line = await lines.next();
expect(line.value).toEqual('Retrieving project…');
line = await lines.next();
expect(line.value).toEqual(
`Fetching deployments in ${teamSlug || team[0].slug}`
);
line = await lines.next();
expect(line.value).toEqual(
`Fetching deployments in ${teamSlug || team[0].slug}`
);
line = await lines.next();
const { org } = pluckIdentifiersFromDeploymentList(line.value!);
expect(org).toEqual(teamSlug || team[0].slug);
line = await lines.next();
const { org } = pluckIdentifiersFromDeploymentList(line.value!);
expect(org).toEqual(teamSlug || team[0].slug);
// skip next line
await lines.next();
// skip next line
await lines.next();
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual([
'Age',
'Deployment',
'Status',
'Duration',
'Username',
]);
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual([
'Age',
'Deployment',
'Status',
'Duration',
'Username',
]);
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.shift();
expect(data).toEqual([
`https://${deployment.url}`,
stateString(deployment.state || ''),
getDeploymentDuration(deployment),
user.username,
]);
} finally {
process.chdir(originalCwd);
}
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.shift();
expect(data).toEqual([
`https://${deployment.url}`,
stateString(deployment.state || ''),
getDeploymentDuration(deployment),
user.username,
]);
});
});

View File

@@ -0,0 +1,365 @@
import chalk from 'chalk';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
import { Request, Response } from 'express';
import promote from '../../../src/commands/promote';
import { LastAliasRequest } from '@vercel-internals/types';
import { setupUnitFixture } from '../../helpers/setup-unit-fixture';
import { useDeployment } from '../../mocks/deployment';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
import sleep from '../../../src/util/sleep';
jest.setTimeout(60000);
describe('promote', () => {
it('should error if timeout is invalid', async () => {
const { cwd } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', '--yes', '--timeout', 'foo');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput('Error: Invalid timeout "foo"');
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if invalid deployment ID', async () => {
const { cwd } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', '????', '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
'Error: The provided argument "????" is not a valid deployment ID or URL'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if deployment not found', async () => {
const { cwd } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', 'foo', '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput('Fetching deployment "foo" in ');
await expect(client.stderr).toOutput(
'Error: Error: Can\'t find the deployment "foo" under the context'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should show status when not promoting', async () => {
const { cwd } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
'Checking promotion status of vercel-promote'
);
await expect(client.stderr).toOutput('No deployment promotion in progress');
await expect(exitCodePromise).resolves.toEqual(0);
});
it('should promote by deployment id', async () => {
const { cwd, previousDeployment } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', previousDeployment.id, '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Promote in progress');
await expect(client.stderr).toOutput(
`Success! ${chalk.bold('vercel-promote')} was promoted to ${
previousDeployment.url
} (${previousDeployment.id})`
);
await expect(exitCodePromise).resolves.toEqual(0);
});
it('should promote by deployment url', async () => {
const { cwd, previousDeployment } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', previousDeployment.url, '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Promote in progress');
await expect(client.stderr).toOutput(
`Success! ${chalk.bold('vercel-promote')} was promoted to ${
previousDeployment.url
} (${previousDeployment.id})`
);
await expect(exitCodePromise).resolves.toEqual(0);
});
it('should get status while promoting', async () => {
const { cwd, previousDeployment, project } = initPromoteTest({
promotePollCount: 10,
});
client.cwd = cwd;
// start the promote
client.setArgv('promote', previousDeployment.id, '--yes');
promote(client);
// need to wait for the promote request to be accepted
await sleep(300);
// get the status
client.setArgv('promote', '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Checking promotion status of ${project.name}`
);
await expect(client.stderr).toOutput(
`Success! ${chalk.bold('vercel-promote')} was promoted to ${
previousDeployment.url
} (${previousDeployment.id})`
);
await expect(exitCodePromise).resolves.toEqual(0);
});
it('should error if promote request fails', async () => {
const { cwd, previousDeployment } = initPromoteTest({
promotePollCount: 10,
promoteStatusCode: 500,
});
client.cwd = cwd;
client.setArgv('promote', previousDeployment.id, '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
// we need to wait a super long time because fetch will return on 500
await expect(client.stderr).toOutput('Response Error (500)', 20000);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if promote fails (no aliases)', async () => {
const { cwd, previousDeployment } = initPromoteTest({
promoteJobStatus: 'failed',
});
client.cwd = cwd;
client.setArgv('promote', previousDeployment.id, '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Promote in progress');
await expect(client.stderr).toOutput(
`Error: Failed to remap all aliases to the requested deployment ${previousDeployment.url} (${previousDeployment.id})`
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if promote fails (with aliases)', async () => {
const { cwd, previousDeployment } = initPromoteTest({
promoteAliases: [
{
alias: { alias: 'foo', deploymentId: 'foo_123' },
status: 'completed',
},
{
alias: { alias: 'bar', deploymentId: 'bar_123' },
status: 'failed',
},
],
promoteJobStatus: 'failed',
});
client.cwd = cwd;
client.setArgv('promote', previousDeployment.id, '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Promote in progress');
await expect(client.stderr).toOutput(
`Error: Failed to remap all aliases to the requested deployment ${previousDeployment.url} (${previousDeployment.id})`
);
await expect(client.stderr).toOutput(
` ${chalk.green('completed')} foo (foo_123)`
);
await expect(client.stderr).toOutput(
` ${chalk.red('failed')} bar (bar_123)`
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if deployment times out', async () => {
const { cwd, previousDeployment } = initPromoteTest({
promotePollCount: 10,
});
client.cwd = cwd;
client.setArgv('promote', previousDeployment.id, '--yes', '--timeout', '1');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Promote in progress');
await expect(client.stderr).toOutput(
`The promotion exceeded its deadline - rerun ${chalk.bold(
`vercel promote ${previousDeployment.id}`
)} to try again`,
10000
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should immediately exit after requesting promote', async () => {
const { cwd, previousDeployment } = initPromoteTest();
client.cwd = cwd;
client.setArgv('promote', previousDeployment.id, '--yes', '--timeout', '0');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput(
`Successfully requested promote of ${chalk.bold('vercel-promote')} to ${
previousDeployment.url
} (${previousDeployment.id})`
);
await expect(exitCodePromise).resolves.toEqual(0);
});
it('should error if deployment belongs to different team', async () => {
const { cwd, previousDeployment } = initPromoteTest();
client.cwd = cwd;
previousDeployment.team = {
id: 'abc',
name: 'abc',
slug: 'abc',
};
client.setArgv('promote', previousDeployment.id, '--yes');
const exitCodePromise = promote(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput(
'Error: Deployment belongs to a different team'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
});
type DeploymentAlias = {
alias: {
alias: string;
deploymentId: string;
};
status: string;
};
function initPromoteTest({
promoteAliases = [],
promoteJobStatus = 'succeeded',
promotePollCount = 2,
promoteStatusCode,
}: {
promoteAliases?: DeploymentAlias[];
promoteJobStatus?: LastAliasRequest['jobStatus'];
promotePollCount?: number;
promoteStatusCode?: number;
} = {}) {
const cwd = setupUnitFixture('commands/promote/simple-next-site');
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
...defaultProject,
id: 'vercel-promote',
name: 'vercel-promote',
});
const currentDeployment = useDeployment({ creator: user, project });
const previousDeployment = useDeployment({ creator: user, project });
let pollCounter = 0;
let lastAliasRequest: LastAliasRequest | null = null;
client.scenario.post(
'/:version/projects/:project/promote/:id',
(req: Request, res: Response) => {
if (promoteStatusCode === 500) {
res.statusCode = 500;
res.end('Server error');
return;
}
const { id } = req.params;
if (previousDeployment.id !== id) {
res.statusCode = 404;
res.json({
error: { code: 'not_found', message: 'Deployment not found', id },
});
return;
}
lastAliasRequest = {
fromDeploymentId: currentDeployment.id,
jobStatus: 'in-progress',
requestedAt: Date.now(),
toDeploymentId: id,
type: 'promote',
};
Object.defineProperty(project, 'lastAliasRequest', {
get(): LastAliasRequest | null {
if (
lastAliasRequest &&
promotePollCount !== undefined &&
pollCounter++ > promotePollCount
) {
lastAliasRequest.jobStatus = promoteJobStatus;
}
return lastAliasRequest;
},
set(value: LastAliasRequest | null) {
lastAliasRequest = value;
},
});
res.statusCode = 201;
res.end();
}
);
client.scenario.get(
'/:version/projects/:project/promote/aliases',
(req, res) => {
res.json({
aliases: promoteAliases,
pagination: null,
});
}
);
return {
cwd,
project,
currentDeployment,
previousDeployment,
};
}

View File

@@ -3,7 +3,7 @@ import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
import { Request, Response } from 'express';
import rollback from '../../../src/commands/rollback';
import { RollbackJobStatus, RollbackTarget } from '@vercel-internals/types';
import type { LastAliasRequest } from '@vercel-internals/types';
import { setupUnitFixture } from '../../helpers/setup-unit-fixture';
import { useDeployment } from '../../mocks/deployment';
import { useTeams } from '../../mocks/team';
@@ -13,32 +13,22 @@ import sleep from '../../../src/util/sleep';
jest.setTimeout(60000);
describe('rollback', () => {
it('should error if cwd is invalid', async () => {
client.setArgv('rollback', '--cwd', __filename);
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput(
'Error: Support for single file deployments has been removed.'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if timeout is invalid', async () => {
const { cwd } = initRollbackTest();
client.setArgv('rollback', '--yes', '--cwd', cwd, '--timeout', 'foo');
client.cwd = cwd;
client.setArgv('rollback', '--yes', '--timeout', 'foo');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Error: Invalid timeout "foo"');
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if invalid deployment name', async () => {
it('should error if invalid deployment ID', async () => {
const { cwd } = initRollbackTest();
client.setArgv('rollback', '????', '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', '????', '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
'Error: The provided argument "????" is not a valid deployment ID or URL'
);
@@ -47,10 +37,10 @@ describe('rollback', () => {
it('should error if deployment not found', async () => {
const { cwd } = initRollbackTest();
client.setArgv('rollback', 'foo', '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', 'foo', '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
'Error: Can\'t find the deployment "foo" under the context'
);
@@ -60,10 +50,10 @@ describe('rollback', () => {
it('should show status when not rolling back', async () => {
const { cwd } = initRollbackTest();
client.setArgv('rollback', '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
'Checking rollback status of vercel-rollback'
);
@@ -74,10 +64,10 @@ describe('rollback', () => {
it('should rollback by deployment id', async () => {
const { cwd, previousDeployment } = initRollbackTest();
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', previousDeployment.id, '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
@@ -93,10 +83,10 @@ describe('rollback', () => {
it('should rollback by deployment url', async () => {
const { cwd, previousDeployment } = initRollbackTest();
client.setArgv('rollback', previousDeployment.url, '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', previousDeployment.url, '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
);
@@ -114,19 +104,19 @@ describe('rollback', () => {
const { cwd, previousDeployment, project } = initRollbackTest({
rollbackPollCount: 10,
});
client.cwd = cwd;
// start the rollback
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
client.setArgv('rollback', previousDeployment.id, '--yes');
rollback(client);
// need to wait for the rollback request to be accepted
await sleep(500);
await sleep(300);
// get the status
client.setArgv('rollback', '--yes', '--cwd', cwd);
client.setArgv('rollback', '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Checking rollback status of ${project.name}`
);
@@ -144,26 +134,28 @@ describe('rollback', () => {
rollbackPollCount: 10,
rollbackStatusCode: 500,
});
client.cwd = cwd;
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
const exitCode = await rollback(client);
client.setArgv('rollback', previousDeployment.id, '--yes');
const exitCodePromise = rollback(client);
expect(exitCode).toBe(1);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Response Error (500)');
// we need to wait a super long time because fetch will return on 500
await expect(client.stderr).toOutput('Response Error (500)', 20000);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if rollback fails (no aliases)', async () => {
const { cwd, previousDeployment } = initRollbackTest({
rollbackJobStatus: 'failed',
});
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', previousDeployment.id, '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
@@ -189,10 +181,10 @@ describe('rollback', () => {
],
rollbackJobStatus: 'failed',
});
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', previousDeployment.id, '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
@@ -214,18 +206,16 @@ describe('rollback', () => {
const { cwd, previousDeployment } = initRollbackTest({
rollbackPollCount: 10,
});
client.cwd = cwd;
client.setArgv(
'rollback',
previousDeployment.id,
'--yes',
'--cwd',
cwd,
'--timeout',
'1s'
'1'
);
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
@@ -241,18 +231,16 @@ describe('rollback', () => {
it('should immediately exit after requesting rollback', async () => {
const { cwd, previousDeployment } = initRollbackTest();
client.cwd = cwd;
client.setArgv(
'rollback',
previousDeployment.id,
'--yes',
'--cwd',
cwd,
'--timeout',
'0'
);
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
@@ -272,10 +260,10 @@ describe('rollback', () => {
name: 'abc',
slug: 'abc',
};
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
client.cwd = cwd;
client.setArgv('rollback', previousDeployment.id, '--yes');
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
);
@@ -302,11 +290,11 @@ function initRollbackTest({
rollbackStatusCode,
}: {
rollbackAliases?: RollbackAlias[];
rollbackJobStatus?: RollbackJobStatus;
rollbackJobStatus?: LastAliasRequest['jobStatus'];
rollbackPollCount?: number;
rollbackStatusCode?: number;
} = {}) {
const cwd = setupUnitFixture('vercel-rollback');
const cwd = setupUnitFixture('commands/rollback/simple-next-site');
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
@@ -315,9 +303,11 @@ function initRollbackTest({
name: 'vercel-rollback',
});
const currentDeployment = useDeployment({ creator: user });
const previousDeployment = useDeployment({ creator: user });
let lastRollbackTarget: RollbackTarget | null = null;
const currentDeployment = useDeployment({ creator: user, project });
const previousDeployment = useDeployment({ creator: user, project });
let pollCounter = 0;
let lastAliasRequest: LastAliasRequest | null = null;
client.scenario.post(
'/:version/projects/:project/rollback/:id',
@@ -337,30 +327,35 @@ function initRollbackTest({
return;
}
lastRollbackTarget = {
lastAliasRequest = {
fromDeploymentId: currentDeployment.id,
jobStatus: 'in-progress',
requestedAt: Date.now(),
toDeploymentId: id,
type: 'rollback',
};
Object.defineProperty(project, 'lastAliasRequest', {
get(): LastAliasRequest | null {
if (
lastAliasRequest &&
rollbackPollCount !== undefined &&
pollCounter++ > rollbackPollCount
) {
lastAliasRequest.jobStatus = rollbackJobStatus;
}
return lastAliasRequest;
},
set(value: LastAliasRequest | null) {
lastAliasRequest = value;
},
});
res.statusCode = 201;
res.end();
}
);
let counter = 0;
client.scenario.get(`/:version/projects/${project.id}`, (req, res) => {
const data = { ...project };
if (req.query?.rollbackInfo === 'true') {
if (lastRollbackTarget && counter++ > rollbackPollCount) {
lastRollbackTarget.jobStatus = rollbackJobStatus;
}
data.lastRollbackTarget = lastRollbackTarget;
}
res.json(data);
});
client.scenario.get(
'/:version/projects/:project/rollback/aliases',
(req, res) => {

View File

@@ -306,10 +306,9 @@ describe('createGitMeta', () => {
}
});
it('uses the repo url for a connected project', async () => {
const originalCwd = process.cwd();
const directory = fixture('connected-repo');
client.cwd = directory;
try {
process.chdir(directory);
await fs.rename(join(directory, 'git'), join(directory, '.git'));
useUser();
@@ -343,7 +342,6 @@ describe('createGitMeta', () => {
});
} finally {
await fs.rename(join(directory, '.git'), join(directory, 'git'));
process.chdir(originalCwd);
}
});
});

View File

@@ -28,7 +28,7 @@ describe('getLinkedProject', () => {
try {
link = await getLinkedProject(client, cwd);
} catch (err) {
error = err;
error = err as Error;
}
expect(link).toBeUndefined();
@@ -57,7 +57,7 @@ describe('getLinkedProject', () => {
try {
link = await getLinkedProject(client, cwd);
} catch (err) {
error = err;
error = err as Error;
}
expect(link).toBeUndefined();
@@ -69,4 +69,106 @@ describe('getLinkedProject', () => {
'Could not retrieve Project Settings. To link your Project, remove the `.vercel` directory and deploy again.'
);
});
it('should return link with `project.json`', async () => {
const cwd = fixture('vercel-pull-next');
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'vercel-pull-next',
name: 'vercel-pull-next',
});
const link = await getLinkedProject(client, cwd);
if (link.status !== 'linked') {
throw new Error('Expected to be linked');
}
expect(link.org.id).toEqual('team_dummy');
expect(link.org.type).toEqual('team');
expect(link.project.id).toEqual('vercel-pull-next');
expect(link.repoRoot).toBeUndefined();
});
it('should return link with `repo.json`', async () => {
const cwd = fixture('monorepo-link');
useUser();
useTeams('team_dummy');
// dashboard
useProject({
...defaultProject,
id: 'QmbKpqpiUqbcke',
name: 'monorepo-dashboard',
});
let link = await getLinkedProject(client, join(cwd, 'dashboard'));
if (link.status !== 'linked') {
throw new Error('Expected to be linked');
}
expect(link.org.id).toEqual('team_dummy');
expect(link.org.type).toEqual('team');
expect(link.project.id).toEqual('QmbKpqpiUqbcke');
expect(link.repoRoot).toEqual(cwd);
// marketing
useProject({
...defaultProject,
id: 'QmX6P93ChNDoZP',
name: 'monorepo-marketing',
});
link = await getLinkedProject(client, join(cwd, 'marketing/subdir'));
if (link.status !== 'linked') {
throw new Error('Expected to be linked');
}
expect(link.org.id).toEqual('team_dummy');
expect(link.org.type).toEqual('team');
expect(link.project.id).toEqual('QmX6P93ChNDoZP');
expect(link.repoRoot).toEqual(cwd);
// blog
useProject({
...defaultProject,
id: 'QmScb7GPQt6gsS',
name: 'monorepo-blog',
});
link = await getLinkedProject(client, join(cwd, 'blog'));
if (link.status !== 'linked') {
throw new Error('Expected to be linked');
}
expect(link.org.id).toEqual('team_dummy');
expect(link.org.type).toEqual('team');
expect(link.project.id).toEqual('QmScb7GPQt6gsS');
expect(link.repoRoot).toEqual(cwd);
});
it('should show project selector prompt link with `repo.json`', async () => {
const cwd = fixture('monorepo-link');
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'QmbKpqpiUqbcke',
name: 'monorepo-dashboard',
});
const linkPromise = getLinkedProject(client, cwd);
// wait for prompt
await expect(client.stderr).toOutput('Please select a Project:');
// make selection
client.stdin.write('\r');
const link = await linkPromise;
if (link.status !== 'linked') {
throw new Error('Expected to be linked');
}
expect(link.org.id).toEqual('team_dummy');
expect(link.org.type).toEqual('team');
expect(link.project.id).toEqual('QmbKpqpiUqbcke');
expect(link.repoRoot).toEqual(cwd);
});
});

View File

@@ -1,5 +1,12 @@
# @vercel/client
## 12.6.1
### Patch Changes
- Updated dependencies [[`c7bcea408`](https://github.com/vercel/vercel/commit/c7bcea408131df2d65338e50ce319a6d8e4a8a82)]:
- @vercel/build-utils@6.7.4
## 12.6.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.6.0",
"version": "12.6.1",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -35,7 +35,7 @@
"typescript": "4.9.5"
},
"dependencies": {
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"@vercel/routing-utils": "2.2.1",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",

View File

@@ -35,7 +35,7 @@
"@types/minimatch": "3.0.5",
"@types/node": "14.18.33",
"@types/semver": "7.3.10",
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"typescript": "4.9.5"
}
}

View File

@@ -1,5 +1,13 @@
# @vercel/gatsby-plugin-vercel-builder
## 1.3.6
### Patch Changes
- Updated dependencies [[`c7bcea408`](https://github.com/vercel/vercel/commit/c7bcea408131df2d65338e50ce319a6d8e4a8a82)]:
- @vercel/build-utils@6.7.4
- @vercel/node@2.14.4
## 1.3.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/gatsby-plugin-vercel-builder",
"version": "1.3.5",
"version": "1.3.6",
"main": "dist/index.js",
"files": [
"dist",
@@ -20,8 +20,8 @@
},
"dependencies": {
"@sinclair/typebox": "0.25.24",
"@vercel/build-utils": "6.7.3",
"@vercel/node": "2.14.3",
"@vercel/build-utils": "6.7.4",
"@vercel/node": "2.14.4",
"@vercel/routing-utils": "2.2.1",
"esbuild": "0.14.47",
"etag": "1.8.1",

View File

@@ -27,7 +27,7 @@
"@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0",
"@types/yauzl-promise": "2.1.0",
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"@vercel/ncc": "0.24.0",
"async-retry": "1.3.1",
"execa": "^1.0.0",

View File

@@ -21,7 +21,7 @@
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "14.18.33",
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"@vercel/static-config": "2.0.17",
"execa": "3.2.0",
"fs-extra": "11.1.0",

View File

@@ -1,5 +1,17 @@
# @vercel/next
## 3.8.6
### Patch Changes
- [next] Fix `functions` config with App Router ([#9889](https://github.com/vercel/vercel/pull/9889))
- [next] Pass `pageExtensions` data to `apiLambdaGroups` ([#10015](https://github.com/vercel/vercel/pull/10015))
- Revert "[next] Update rsc content-type test fixtures" ([#10040](https://github.com/vercel/vercel/pull/10040))
- Remove usage of `env` from Edge Functions and Middleware ([#10018](https://github.com/vercel/vercel/pull/10018))
## 3.8.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.8.5",
"version": "3.8.6",
"license": "Apache-2.0",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -35,7 +35,7 @@
"@types/semver": "6.0.0",
"@types/text-table": "0.2.1",
"@types/webpack-sources": "3.2.0",
"@vercel/build-utils": "6.7.3",
"@vercel/build-utils": "6.7.4",
"@vercel/nft": "0.22.5",
"@vercel/routing-utils": "2.2.1",
"async-sema": "3.0.1",

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