mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-24 19:00:03 +00:00
Compare commits
46 Commits
@vercel/fr
...
@vercel/cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f39513118a | ||
|
|
fd188a396b | ||
|
|
cdfc879f69 | ||
|
|
b58dcc5733 | ||
|
|
cddf3fa00c | ||
|
|
627b79fb14 | ||
|
|
7259a324c2 | ||
|
|
1aab599652 | ||
|
|
2461b571af | ||
|
|
1088bf2dea | ||
|
|
c5bd242a79 | ||
|
|
764ba97967 | ||
|
|
4e416d7d7b | ||
|
|
8775ff3fcf | ||
|
|
bd100baa16 | ||
|
|
2e6aab01cb | ||
|
|
3e57c4a2de | ||
|
|
0c3d136414 | ||
|
|
238d6db17a | ||
|
|
bb7bfd027f | ||
|
|
a56ab4ded9 | ||
|
|
5660dab2ae | ||
|
|
1202cf8792 | ||
|
|
80b8c3d022 | ||
|
|
68e5edb9a2 | ||
|
|
1545e31a31 | ||
|
|
76ea6ea2a7 | ||
|
|
5b8b87739e | ||
|
|
6ff7b34a44 | ||
|
|
97d9e83232 | ||
|
|
a3fb7e6abe | ||
|
|
b9f6d765c0 | ||
|
|
c24d85f574 | ||
|
|
3c42649916 | ||
|
|
c1891f64f4 | ||
|
|
2fc46c0d47 | ||
|
|
73b112b1f7 | ||
|
|
548afd371a | ||
|
|
d0877c1023 | ||
|
|
326fe0f0e6 | ||
|
|
346e665bb0 | ||
|
|
9a27ccbe6d | ||
|
|
3b6d96891b | ||
|
|
07d8d11989 | ||
|
|
8a16e8eb65 | ||
|
|
63e9de8932 |
22
.github/CODEOWNERS
vendored
22
.github/CODEOWNERS
vendored
@@ -2,17 +2,17 @@
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
# Restricted Paths
|
||||
* @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55
|
||||
/.github/workflows @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @ijjk
|
||||
/packages/fs-detectors @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @agadzik @chloetedder
|
||||
/packages/next @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @timneutkens @ijjk @ztanner @huozhi @Ethan-Arrowood @styfle
|
||||
/packages/routing-utils @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @ijjk
|
||||
/packages/static-build @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55
|
||||
/packages/edge @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @vercel/compute
|
||||
/examples @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @leerob
|
||||
/examples/create-react-app @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @Timer
|
||||
/examples/nextjs @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @timneutkens @ijjk @ztanner @huozhi @Ethan-Arrowood @styfle
|
||||
/packages/node @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @Kikobeats
|
||||
* @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads
|
||||
/.github/workflows @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @ijjk
|
||||
/packages/fs-detectors @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @agadzik @chloetedder
|
||||
/packages/next @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @timneutkens @ijjk @ztanner @huozhi @Ethan-Arrowood @styfle
|
||||
/packages/routing-utils @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @ijjk
|
||||
/packages/static-build @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads
|
||||
/packages/edge @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @vercel/compute
|
||||
/examples @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @leerob
|
||||
/examples/create-react-app @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @Timer
|
||||
/examples/nextjs @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @timneutkens @ijjk @ztanner @huozhi @Ethan-Arrowood @styfle
|
||||
/packages/node @TooTallNate @EndangeredMassa @trek @onsclom @jeffsee55 @erikareads @Kikobeats
|
||||
|
||||
# Unrestricted Paths
|
||||
.changeset/
|
||||
|
||||
109
.github/CONTRIBUTING.md
vendored
109
.github/CONTRIBUTING.md
vendored
@@ -1,108 +1,3 @@
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via [GitHub Discussions](https://github.com/vercel/vercel/discussions/new) with the owners of this repository before submitting a Pull Request.
|
||||
|
||||
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
|
||||
|
||||
## Local development
|
||||
|
||||
This project is configured in a monorepo, where one repository contains multiple npm packages. Dependencies are installed and managed with `pnpm`, not `npm` CLI.
|
||||
|
||||
To get started, execute the following:
|
||||
|
||||
```
|
||||
git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm lint
|
||||
pnpm test-unit
|
||||
```
|
||||
|
||||
Make sure all the tests pass before making changes.
|
||||
|
||||
### Running Vercel CLI Changes
|
||||
|
||||
You can use `pnpm dev` from the `cli` package to invoke Vercel CLI with local changes:
|
||||
|
||||
```
|
||||
cd ./packages/cli
|
||||
pnpm dev <cli-commands...>
|
||||
```
|
||||
|
||||
See [CLI Local Development](../packages/cli#local-development) for more details.
|
||||
|
||||
## Verifying your change
|
||||
|
||||
Once you are done with your changes (we even suggest doing it along the way), make sure all the tests still pass by running:
|
||||
|
||||
```
|
||||
pnpm test-unit
|
||||
```
|
||||
|
||||
from the root of the project.
|
||||
|
||||
If any test fails, make sure to fix it along with your changes. See [Interpreting test errors](#Interpreting-test-errors) for more information about how the tests are executed, especially the integration tests.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
Once you are confident that your changes work properly, open a pull request on the main repository.
|
||||
|
||||
The pull request will be reviewed by the maintainers and the tests will be checked by our continuous integration platform.
|
||||
|
||||
## Interpreting test errors
|
||||
|
||||
There are 2 kinds of tests in this repository – Unit tests and Integration tests.
|
||||
|
||||
Unit tests are run locally with `jest` and execute quickly because they are testing the smallest units of code.
|
||||
|
||||
### Integration tests
|
||||
|
||||
Integration tests create deployments to your Vercel account using the `test` project name. After each test is deployed, the `probes` key is used to check if the response is the expected value. If the value doesn't match, you'll see a message explaining the difference. If the deployment failed to build, you'll see a more generic message like the following:
|
||||
|
||||
```
|
||||
[Error: Fetched page https://test-8ashcdlew.vercel.app/root.js does not contain hello Root!. Instead it contains An error occurred with this application.
|
||||
|
||||
NO_STATUS_CODE_FRO Response headers:
|
||||
cache-control=s-maxage=0
|
||||
connection=close
|
||||
content-type=text/plain; charset=utf-8
|
||||
date=Wed, 19 Jun 2019 18:01:37 GMT
|
||||
server=now
|
||||
strict-transport-security=max-age=63072000
|
||||
transfer-encoding=chunked
|
||||
x-now-id=iad1:hgtzj-1560967297876-44ae12559f95
|
||||
x-now-trace=iad1]
|
||||
```
|
||||
|
||||
In such cases, you can visit the URL of the failed deployment and append `/_logs` to see the build error. In the case above, that would be https://test-8ashcdlew.vercel.app/_logs
|
||||
|
||||
The logs of this deployment will contain the actual error which may help you to understand what went wrong.
|
||||
|
||||
### @vercel/nft
|
||||
|
||||
Some of the Builders use `@vercel/nft` to tree-shake files before deployment. If you suspect an error with this tree-shaking mechanism, you can create the following script in your project:
|
||||
|
||||
```js
|
||||
const { nodeFileTrace } = require('@vercel/nft');
|
||||
nodeFileTrace(['path/to/entrypoint.js'], {
|
||||
ts: true,
|
||||
mixedModules: true,
|
||||
})
|
||||
.then(o => console.log(o.fileList))
|
||||
.then(e => console.error(e));
|
||||
```
|
||||
|
||||
When you run this script, you'll see all the imported files. If anything file is missing, the bug is in [@vercel/nft](https://github.com/vercel/nft) and not the Builder.
|
||||
|
||||
## Deploy a Builder with existing project
|
||||
|
||||
Sometimes you want to test changes to a Builder against an existing project, maybe with `vercel dev` or actual deployment. You can avoid publishing every Builder change to npm by uploading the Builder as a tarball.
|
||||
|
||||
1. Change directory to the desired Builder `cd ./packages/node`
|
||||
2. Run `pnpm build` to compile typescript and other build steps
|
||||
3. Run `npm pack` to create a tarball file
|
||||
4. Run `vercel *.tgz` to upload the tarball file and get a URL
|
||||
5. Edit any existing `vercel.json` project and replace `use` with the URL
|
||||
6. Run `vercel` or `vercel dev` to deploy with the experimental Builder
|
||||
See the [Contributing Guidelines](../README.md#contributing) for more details.
|
||||
|
||||
62
.github/dependabot.yml
vendored
62
.github/dependabot.yml
vendored
@@ -90,8 +90,15 @@ updates:
|
||||
prefix: '[framework-fixtures]'
|
||||
package-ecosystem: 'npm'
|
||||
allow:
|
||||
- dependency-name: '@angular*'
|
||||
- dependency-name: '@ionic*'
|
||||
- dependency-name: '@angular*'
|
||||
ignore:
|
||||
- dependency-name: '@ionic*'
|
||||
update-types:
|
||||
['version-update:semver-major', 'version-update:semver-patch']
|
||||
- dependency-name: '@angular*'
|
||||
update-types:
|
||||
['version-update:semver-major', 'version-update:semver-patch']
|
||||
groups:
|
||||
core:
|
||||
patterns:
|
||||
@@ -100,6 +107,35 @@ updates:
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/ionic-react-v7
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
open-pull-requests-limit: 1
|
||||
reviewers:
|
||||
- 'trek'
|
||||
- 'TooTallNate'
|
||||
- 'EndangeredMassa'
|
||||
commit-message:
|
||||
prefix: '[framework-fixtures]'
|
||||
package-ecosystem: 'npm'
|
||||
allow:
|
||||
- dependency-name: '@ionic*'
|
||||
- dependency-name: 'react*'
|
||||
ignore:
|
||||
- dependency-name: '@ionic*'
|
||||
update-types:
|
||||
['version-update:semver-major', 'version-update:semver-patch']
|
||||
- dependency-name: 'react*'
|
||||
update-types:
|
||||
['version-update:semver-major', 'version-update:semver-patch']
|
||||
groups:
|
||||
core:
|
||||
patterns:
|
||||
- '@ionic*'
|
||||
- 'react*'
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/nuxt-v3
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
@@ -139,6 +175,30 @@ updates:
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/preact-v10
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
open-pull-requests-limit: 1
|
||||
reviewers:
|
||||
- 'trek'
|
||||
- 'TooTallNate'
|
||||
- 'EndangeredMassa'
|
||||
commit-message:
|
||||
prefix: '[framework-fixtures]'
|
||||
package-ecosystem: 'npm'
|
||||
allow:
|
||||
- dependency-name: 'preact*'
|
||||
ignore:
|
||||
- dependency-name: 'preact*'
|
||||
update-types:
|
||||
['version-update:semver-major', 'version-update:semver-patch']
|
||||
groups:
|
||||
core:
|
||||
patterns:
|
||||
- 'preact*'
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/stencil-v4
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -79,16 +79,13 @@ jobs:
|
||||
run: npm i -g pnpm@8.3.1
|
||||
|
||||
- run: pnpm install
|
||||
- name: fetch ssl certificate before build (linux, os x)
|
||||
if: matrix.runner != 'windows-latest'
|
||||
run: echo | openssl s_client -showcerts -servername 'api.vercel.com' -connect 76.76.21.21:443
|
||||
|
||||
- name: Build ${{matrix.packageName}} and all its dependencies
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run build --cache-dir=".turbo" --log-order=stream --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
env:
|
||||
FORCE_COLOR: '1'
|
||||
- name: Test ${{matrix.packageName}}
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run test --summarize --cache-dir=".turbo" --log-order=stream --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run ${{matrix.testScript}} --summarize --cache-dir=".turbo" --log-order=stream --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
shell: bash
|
||||
env:
|
||||
JEST_JUNIT_OUTPUT_FILE: ${{github.workspace}}/.junit-reports/${{matrix.scriptName}}-${{matrix.packageName}}-${{matrix.chunkNumber}}-${{ matrix.runner }}.xml
|
||||
@@ -104,9 +101,6 @@ jobs:
|
||||
TURBO_MISS_COUNT=`node utils/determine-turbo-hit-or-miss.js`
|
||||
echo "MISS COUNT: $TURBO_MISS_COUNT"
|
||||
echo "misses=$TURBO_MISS_COUNT" >> $GITHUB_OUTPUT
|
||||
- name: fetch ssl certificate after tests (linux, os x)
|
||||
if: matrix.runner != 'windows-latest'
|
||||
run: echo | openssl s_client -showcerts -servername 'api.vercel.com' -connect 76.76.21.21:443
|
||||
- name: 'Upload Test Report to Datadog'
|
||||
if: ${{ steps['turbo-summary'].outputs.misses != '0' && !cancelled() }}
|
||||
run: 'npx @datadog/datadog-ci@2.18.1 junit upload --service vercel-cli .junit-reports'
|
||||
|
||||
129
README.md
129
README.md
@@ -35,7 +35,134 @@ This project uses [pnpm](https://pnpm.io/) to install dependencies and run scrip
|
||||
|
||||
You can use the `dev` script to run local changes as if you were invoking Vercel CLI. For example, `vercel deploy --cwd=/path/to/project` could be run with local changes with `pnpm dev deploy --cwd=/path/to/project`.
|
||||
|
||||
See the [Contributing Guidelines](./.github/CONTRIBUTING.md) for more details.
|
||||
When contributing to this repository, please first discuss the change you wish to make via [GitHub Discussions](https://github.com/vercel/vercel/discussions/new) with the owners of this repository before submitting a Pull Request.
|
||||
|
||||
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
|
||||
|
||||
### Local development
|
||||
|
||||
This project is configured in a monorepo, where one repository contains multiple npm packages. Dependencies are installed and managed with `pnpm`, not `npm` CLI.
|
||||
|
||||
To get started, execute the following:
|
||||
|
||||
```
|
||||
git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm lint
|
||||
pnpm test-unit
|
||||
```
|
||||
|
||||
Make sure all the tests pass before making changes.
|
||||
|
||||
#### Running Vercel CLI Changes
|
||||
|
||||
You can use `pnpm dev` from the `cli` package to invoke Vercel CLI with local changes:
|
||||
|
||||
```
|
||||
cd ./packages/cli
|
||||
pnpm dev <cli-commands...>
|
||||
```
|
||||
|
||||
See [CLI Local Development](../packages/cli#local-development) for more details.
|
||||
|
||||
### Verifying your change
|
||||
|
||||
Once you are done with your changes (we even suggest doing it along the way), make sure all the tests still pass by running:
|
||||
|
||||
```
|
||||
pnpm test-unit
|
||||
```
|
||||
|
||||
from the root of the project.
|
||||
|
||||
If any test fails, make sure to fix it along with your changes. See [Interpreting test errors](#Interpreting-test-errors) for more information about how the tests are executed, especially the integration tests.
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
Once you are confident that your changes work properly, open a pull request on the main repository.
|
||||
|
||||
The pull request will be reviewed by the maintainers and the tests will be checked by our continuous integration platform.
|
||||
|
||||
### Interpreting test errors
|
||||
|
||||
There are 2 kinds of tests in this repository – Unit tests and Integration tests.
|
||||
|
||||
Unit tests are run locally with `jest` and execute quickly because they are testing the smallest units of code.
|
||||
|
||||
#### Integration tests
|
||||
|
||||
Integration tests create deployments to your Vercel account using the `test` project name. After each test is deployed, the `probes` key is used to check if the response is the expected value. If the value doesn't match, you'll see a message explaining the difference. If the deployment failed to build, you'll see a more generic message like the following:
|
||||
|
||||
```
|
||||
[Error: Fetched page https://test-8ashcdlew.vercel.app/root.js does not contain hello Root!. Instead it contains An error occurred with this application.
|
||||
|
||||
NO_STATUS_CODE_FRO Response headers:
|
||||
cache-control=s-maxage=0
|
||||
connection=close
|
||||
content-type=text/plain; charset=utf-8
|
||||
date=Wed, 19 Jun 2019 18:01:37 GMT
|
||||
server=now
|
||||
strict-transport-security=max-age=63072000
|
||||
transfer-encoding=chunked
|
||||
x-now-id=iad1:hgtzj-1560967297876-44ae12559f95
|
||||
x-now-trace=iad1]
|
||||
```
|
||||
|
||||
In such cases, you can visit the URL of the failed deployment and append `/_logs` to see the build error. In the case above, that would be https://test-8ashcdlew.vercel.app/_logs
|
||||
|
||||
The logs of this deployment will contain the actual error which may help you to understand what went wrong.
|
||||
|
||||
##### Running integration tests locally
|
||||
|
||||
While running the full integration suite locally is not recommended, it's sometimes useful to isolate a failing test by running it on your machine. To do so, you'll need to ensure you have the appropriate credentials sourced in your shell:
|
||||
|
||||
1. Create an access token. Follow the insructions here https://vercel.com/docs/rest-api#creating-an-access-token. Ensure the token scope is for your personal
|
||||
account.
|
||||
2. Grab the team ID from the Vercel dashboard at `https://vercel.com/<MY-TEAM>/~/settings`.
|
||||
3. Source these into your shell rc file: `echo 'export VERCEL_TOKEN=<MY-TOKEN> VERCEL_TEAM_ID=<MY-TEAM-ID>' >> ~/.zshrc`
|
||||
|
||||
From there, you should be able to trigger an integration test. Choose one
|
||||
that's already isolated to check that things work:
|
||||
|
||||
```
|
||||
cd packages/next
|
||||
```
|
||||
|
||||
Run the test:
|
||||
|
||||
```
|
||||
pnpm test test/fixtures/00-server-build/index.test.js
|
||||
```
|
||||
|
||||
#### @vercel/nft
|
||||
|
||||
Some of the Builders use `@vercel/nft` to tree-shake files before deployment. If you suspect an error with this tree-shaking mechanism, you can create the following script in your project:
|
||||
|
||||
```js
|
||||
const { nodeFileTrace } = require('@vercel/nft');
|
||||
nodeFileTrace(['path/to/entrypoint.js'], {
|
||||
ts: true,
|
||||
mixedModules: true,
|
||||
})
|
||||
.then(o => console.log(o.fileList))
|
||||
.then(e => console.error(e));
|
||||
```
|
||||
|
||||
When you run this script, you'll see all the imported files. If anything file is missing, the bug is in [@vercel/nft](https://github.com/vercel/nft) and not the Builder.
|
||||
|
||||
### Deploy a Builder with existing project
|
||||
|
||||
Sometimes you want to test changes to a Builder against an existing project, maybe with `vercel dev` or actual deployment. You can avoid publishing every Builder change to npm by uploading the Builder as a tarball.
|
||||
|
||||
1. Change directory to the desired Builder `cd ./packages/node`
|
||||
2. Run `pnpm build` to compile typescript and other build steps
|
||||
3. Run `npm pack` to create a tarball file
|
||||
4. Run `vercel *.tgz` to upload the tarball file and get a URL
|
||||
5. Edit any existing `vercel.json` project and replace `use` with the URL
|
||||
6. Run `vercel` or `vercel dev` to deploy with the experimental Builder
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
@@ -50,4 +50,4 @@ Ensure any segments used in the `destination` property are also used in the `sou
|
||||
|
||||
- [path-to-regexp](https://github.com/pillarjs/path-to-regexp/tree/v6.1.0)
|
||||
- [named parameters](https://github.com/pillarjs/path-to-regexp/blob/v6.1.0/Readme.md#named-parameters)
|
||||
- [un-named paramters](https://github.com/pillarjs/path-to-regexp/blob/v6.1.0/Readme.md#unnamed-parameters)
|
||||
- [un-named parameters](https://github.com/pillarjs/path-to-regexp/blob/v6.1.0/Readme.md#unnamed-parameters)
|
||||
|
||||
2
examples/package.json
vendored
2
examples/package.json
vendored
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.4.1",
|
||||
"@vercel/build-utils": "7.10.0",
|
||||
"@vercel/build-utils": "7.11.0",
|
||||
"@vercel/frameworks": "3.0.1"
|
||||
},
|
||||
"version": null
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @vercel-internals/types
|
||||
|
||||
## 1.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`73b112b1f`](https://github.com/vercel/vercel/commit/73b112b1f74480e1bb941e1b754105fc7dace401)]:
|
||||
- @vercel/build-utils@7.11.0
|
||||
|
||||
## 1.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@vercel-internals/types",
|
||||
"version": "1.0.28",
|
||||
"version": "1.0.29",
|
||||
"types": "index.d.ts",
|
||||
"main": "index.d.ts",
|
||||
"files": [
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@types/node": "14.14.31",
|
||||
"@vercel-internals/constants": "1.0.4",
|
||||
"@vercel/build-utils": "7.10.0",
|
||||
"@vercel/build-utils": "7.11.0",
|
||||
"@vercel/routing-utils": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"source-map-support": "0.5.12",
|
||||
"ts-eager": "2.0.2",
|
||||
"ts-jest": "29.1.0",
|
||||
"turbo": "1.13.0",
|
||||
"turbo": "1.13.2",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @vercel/build-utils
|
||||
|
||||
## 7.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add `getOsRelease()` and `getProvidedRuntime()` functions ([#11370](https://github.com/vercel/vercel/pull/11370))
|
||||
|
||||
## 7.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "7.10.0",
|
||||
"version": "7.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -103,6 +103,7 @@ export {
|
||||
export { EdgeFunction } from './edge-function';
|
||||
export { readConfigFile } from './fs/read-config-file';
|
||||
export { normalizePath } from './fs/normalize-path';
|
||||
export { getOsRelease, getProvidedRuntime } from './os';
|
||||
|
||||
export * from './should-serve';
|
||||
export * from './schemas';
|
||||
|
||||
42
packages/build-utils/src/os.ts
Normal file
42
packages/build-utils/src/os.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { readFile } from 'fs-extra';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
export async function getOsRelease() {
|
||||
try {
|
||||
const data = await readFile('/etc/os-release', 'utf8');
|
||||
return await parseOsRelease(data);
|
||||
} catch (err) {
|
||||
if (isErrnoException(err) && err.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseOsRelease(data: string) {
|
||||
const obj: Record<string, string> = {};
|
||||
// Example file contents:
|
||||
// NAME="Amazon Linux"
|
||||
// VERSION="2023"
|
||||
// ID="amzn"
|
||||
// ID_LIKE="fedora"
|
||||
for (const line of data.trim().split('\n')) {
|
||||
const m = /(?<key>.*)="(?<value>.*)"/.exec(line);
|
||||
if (!m?.groups) {
|
||||
continue;
|
||||
}
|
||||
obj[m.groups.key] = m.groups.value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export async function getProvidedRuntime() {
|
||||
const os = await getOsRelease();
|
||||
if (!os) {
|
||||
return 'provided.al2023';
|
||||
}
|
||||
|
||||
return os.PRETTY_NAME === 'Amazon Linux 2'
|
||||
? 'provided.al2'
|
||||
: 'provided.al2023';
|
||||
}
|
||||
89
packages/build-utils/test/unit.get-os-release.test.ts
vendored
Normal file
89
packages/build-utils/test/unit.get-os-release.test.ts
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import { parseOsRelease } from '../src/os';
|
||||
|
||||
describe('getOsRelease()', () => {
|
||||
it('should parse `amazonlinux:2`', async () => {
|
||||
const data = `NAME="Amazon Linux"
|
||||
VERSION="2"
|
||||
ID="amzn"
|
||||
ID_LIKE="centos rhel fedora"
|
||||
VERSION_ID="2"
|
||||
PRETTY_NAME="Amazon Linux 2"
|
||||
ANSI_COLOR="0;33"
|
||||
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
|
||||
HOME_URL="https://amazonlinux.com/"
|
||||
SUPPORT_END="2025-06-30"
|
||||
`;
|
||||
const parsed = await parseOsRelease(data);
|
||||
expect(parsed).toMatchObject({
|
||||
ANSI_COLOR: '0;33',
|
||||
CPE_NAME: 'cpe:2.3:o:amazon:amazon_linux:2',
|
||||
HOME_URL: 'https://amazonlinux.com/',
|
||||
ID: 'amzn',
|
||||
ID_LIKE: 'centos rhel fedora',
|
||||
NAME: 'Amazon Linux',
|
||||
PRETTY_NAME: 'Amazon Linux 2',
|
||||
SUPPORT_END: '2025-06-30',
|
||||
VERSION: '2',
|
||||
VERSION_ID: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse `amazonlinux:2023`', async () => {
|
||||
const data = `NAME="Amazon Linux"
|
||||
VERSION="2023"
|
||||
ID="amzn"
|
||||
ID_LIKE="fedora"
|
||||
VERSION_ID="2023"
|
||||
PLATFORM_ID="platform:al2023"
|
||||
PRETTY_NAME="Amazon Linux 2023"
|
||||
ANSI_COLOR="0;33"
|
||||
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
|
||||
HOME_URL="https://aws.amazon.com/linux/"
|
||||
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
|
||||
SUPPORT_END="2028-03-01"
|
||||
`;
|
||||
const parsed = await parseOsRelease(data);
|
||||
expect(parsed).toMatchObject({
|
||||
NAME: 'Amazon Linux',
|
||||
VERSION: '2023',
|
||||
ID: 'amzn',
|
||||
ID_LIKE: 'fedora',
|
||||
VERSION_ID: '2023',
|
||||
PLATFORM_ID: 'platform:al2023',
|
||||
PRETTY_NAME: 'Amazon Linux 2023',
|
||||
ANSI_COLOR: '0;33',
|
||||
CPE_NAME: 'cpe:2.3:o:amazon:amazon_linux:2023',
|
||||
HOME_URL: 'https://aws.amazon.com/linux/',
|
||||
BUG_REPORT_URL: 'https://github.com/amazonlinux/amazon-linux-2023',
|
||||
SUPPORT_END: '2028-03-01',
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse `ubuntu:jammy`', async () => {
|
||||
const data = `PRETTY_NAME="Ubuntu 22.04.3 LTS"
|
||||
NAME="Ubuntu"
|
||||
VERSION_ID="22.04"
|
||||
VERSION="22.04.3 LTS (Jammy Jellyfish)"
|
||||
VERSION_CODENAME=jammy
|
||||
ID=ubuntu
|
||||
ID_LIKE=debian
|
||||
HOME_URL="https://www.ubuntu.com/"
|
||||
SUPPORT_URL="https://help.ubuntu.com/"
|
||||
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
|
||||
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
|
||||
UBUNTU_CODENAME=jammy
|
||||
`;
|
||||
const parsed = await parseOsRelease(data);
|
||||
expect(parsed).toMatchObject({
|
||||
PRETTY_NAME: 'Ubuntu 22.04.3 LTS',
|
||||
NAME: 'Ubuntu',
|
||||
VERSION_ID: '22.04',
|
||||
VERSION: '22.04.3 LTS (Jammy Jellyfish)',
|
||||
HOME_URL: 'https://www.ubuntu.com/',
|
||||
SUPPORT_URL: 'https://help.ubuntu.com/',
|
||||
BUG_REPORT_URL: 'https://bugs.launchpad.net/ubuntu/',
|
||||
PRIVACY_POLICY_URL:
|
||||
'https://www.ubuntu.com/legal/terms-and-policies/privacy-policy',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,63 @@
|
||||
# vercel
|
||||
|
||||
## 34.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- Disables promotion of preview deployments ([#11411](https://github.com/vercel/vercel/pull/11411))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Always set `projectSettings.nodeVersion` in `vc deploy` ([#11351](https://github.com/vercel/vercel/pull/11351))
|
||||
|
||||
- [cli] optional override of existing environment variables with --force ([#11348](https://github.com/vercel/vercel/pull/11348))
|
||||
|
||||
## 33.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix flickering during interactive UI rerendering ([#11392](https://github.com/vercel/vercel/pull/11392))
|
||||
|
||||
- fix `vc ls` message to be `vc projects ls` ([#11400](https://github.com/vercel/vercel/pull/11400))
|
||||
|
||||
- Updated dependencies [[`2461b571a`](https://github.com/vercel/vercel/commit/2461b571af037fbfdf92299a272010a5a8f4898b)]:
|
||||
- @vercel/next@4.2.0
|
||||
|
||||
## 33.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- improve UX for text input validation ([#11388](https://github.com/vercel/vercel/pull/11388))
|
||||
|
||||
- Replace the implementation of the yes/no prompt in several areas to be consistent with the rest of the CLI. ([#11279](https://github.com/vercel/vercel/pull/11279))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [cli] Fix how we determine the GIT_CONFIG_PATH to support git worktrees and git submodules ([#11283](https://github.com/vercel/vercel/pull/11283))
|
||||
|
||||
- avoid printing errors when user does ctrl+c ([#11377](https://github.com/vercel/vercel/pull/11377))
|
||||
|
||||
- Warn that promoting preview deploys is deprecated ([#11376](https://github.com/vercel/vercel/pull/11376))
|
||||
|
||||
- Updated dependencies [[`a3fb7e6ab`](https://github.com/vercel/vercel/commit/a3fb7e6abe9bb619a653850decd739728b1af225)]:
|
||||
- @vercel/go@3.1.1
|
||||
|
||||
## 33.6.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Handle `--repo` linked in `vc deploy --prebuilt` ([#11309](https://github.com/vercel/vercel/pull/11309))
|
||||
|
||||
- Revert "[cli] extract `isZeroConfigBuild` into utility function (#11316)" ([#11350](https://github.com/vercel/vercel/pull/11350))
|
||||
|
||||
- Replace `inquirer` with `@inquirer/prompts` ([#11321](https://github.com/vercel/vercel/pull/11321))
|
||||
|
||||
- Updated dependencies [[`73b112b1f`](https://github.com/vercel/vercel/commit/73b112b1f74480e1bb941e1b754105fc7dace401), [`346e665bb`](https://github.com/vercel/vercel/commit/346e665bb021e6034bc70c82ef336485622595fe), [`73b112b1f`](https://github.com/vercel/vercel/commit/73b112b1f74480e1bb941e1b754105fc7dace401), [`548afd371`](https://github.com/vercel/vercel/commit/548afd371aa7a9dd3a7f4c60f7f94a7084d8023e)]:
|
||||
- @vercel/go@3.1.0
|
||||
- @vercel/node@3.0.26
|
||||
- @vercel/build-utils@7.11.0
|
||||
- @vercel/static-build@2.4.6
|
||||
|
||||
## 33.6.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "33.6.2",
|
||||
"version": "34.0.0",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -12,7 +12,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest --reporters=default --reporters=jest-junit --env node --verbose --bail",
|
||||
"test-unit": "pnpm vitest test/unit/",
|
||||
"vitest-unit-run": "pnpm vitest",
|
||||
"vitest-unit": "pnpm jest test/unit/ --listTests",
|
||||
"test-e2e": "rimraf test/fixtures/integration && pnpm test test/integration-1.test.ts test/integration-2.test.ts test/integration-3.test.ts",
|
||||
"test-dev": "pnpm test test/dev/",
|
||||
"coverage": "codecov",
|
||||
@@ -31,22 +32,27 @@
|
||||
"node": ">= 16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "7.10.0",
|
||||
"@vercel/build-utils": "7.11.0",
|
||||
"@vercel/fun": "1.1.0",
|
||||
"@vercel/go": "3.0.5",
|
||||
"@vercel/go": "3.1.1",
|
||||
"@vercel/hydrogen": "1.0.2",
|
||||
"@vercel/next": "4.1.6",
|
||||
"@vercel/node": "3.0.25",
|
||||
"@vercel/next": "4.2.0",
|
||||
"@vercel/node": "3.0.26",
|
||||
"@vercel/python": "4.1.1",
|
||||
"@vercel/redwood": "2.0.8",
|
||||
"@vercel/remix-builder": "2.1.5",
|
||||
"@vercel/ruby": "2.0.5",
|
||||
"@vercel/static-build": "2.4.5",
|
||||
"@vercel/static-build": "2.4.6",
|
||||
"chokidar": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alex_neo/jest-expect-message": "1.0.5",
|
||||
"@edge-runtime/node-utils": "2.3.0",
|
||||
"@inquirer/checkbox": "2.2.2",
|
||||
"@inquirer/confirm": "3.1.2",
|
||||
"@inquirer/expand": "2.1.2",
|
||||
"@inquirer/input": "2.1.2",
|
||||
"@inquirer/select": "2.2.2",
|
||||
"@next/env": "11.1.2",
|
||||
"@sentry/node": "5.5.0",
|
||||
"@sindresorhus/slugify": "0.11.0",
|
||||
@@ -63,7 +69,6 @@
|
||||
"@types/glob": "7.1.1",
|
||||
"@types/http-proxy": "1.16.2",
|
||||
"@types/ini": "1.3.31",
|
||||
"@types/inquirer": "7.3.1",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/jest-expect-message": "1.0.3",
|
||||
"@types/json-parse-better-errors": "1.0.0",
|
||||
@@ -79,7 +84,6 @@
|
||||
"@types/qs": "6.9.7",
|
||||
"@types/semver": "6.0.1",
|
||||
"@types/tar-fs": "1.16.1",
|
||||
"@types/text-table": "0.2.0",
|
||||
"@types/title": "3.4.1",
|
||||
"@types/universal-analytics": "0.4.2",
|
||||
"@types/update-notifier": "5.1.0",
|
||||
@@ -88,8 +92,8 @@
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel-internals/constants": "1.0.4",
|
||||
"@vercel-internals/get-package-json": "1.0.0",
|
||||
"@vercel-internals/types": "1.0.28",
|
||||
"@vercel/client": "13.1.8",
|
||||
"@vercel-internals/types": "1.0.29",
|
||||
"@vercel/client": "13.2.0",
|
||||
"@vercel/error-utils": "2.0.2",
|
||||
"@vercel/frameworks": "3.0.1",
|
||||
"@vercel/fs-detectors": "5.2.2",
|
||||
@@ -127,7 +131,6 @@
|
||||
"glob": "7.1.2",
|
||||
"http-proxy": "1.18.1",
|
||||
"ini": "3.0.0",
|
||||
"inquirer": "7.0.4",
|
||||
"is-docker": "2.2.1",
|
||||
"is-port-reachable": "3.1.0",
|
||||
"is-url": "1.2.2",
|
||||
@@ -159,7 +162,6 @@
|
||||
"strip-ansi": "6.0.1",
|
||||
"supports-hyperlinks": "3.0.0",
|
||||
"tar-fs": "1.16.3",
|
||||
"text-table": "0.2.0",
|
||||
"title": "3.4.1",
|
||||
"tmp-promise": "1.0.3",
|
||||
"tree-kill": "1.2.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import Client from '../../util/client';
|
||||
import getAliases from '../../util/alias/get-aliases';
|
||||
import getScope from '../../util/get-scope';
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
getPaginationOpts,
|
||||
} from '../../util/get-pagination-opts';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import strlen from '../../util/strlen';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import type { Alias } from '@vercel-internals/types';
|
||||
@@ -78,10 +77,6 @@ function printAliasTable(aliases: Alias[]) {
|
||||
ms(Date.now() - a.createdAt),
|
||||
]),
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'r'],
|
||||
hsep: ' '.repeat(4),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ align: ['l', 'l', 'r'], hsep: 4 }
|
||||
).replace(/^/gm, ' ')}\n\n`;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import Client from '../../util/client';
|
||||
import getScope from '../../util/get-scope';
|
||||
import removeAliasById from '../../util/alias/remove-alias-by-id';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import strlen from '../../util/strlen';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
|
||||
|
||||
@@ -84,11 +83,7 @@ async function confirmAliasRemove(client: Client, alias: Alias) {
|
||||
chalk.gray(`${ms(Date.now() - alias.createdAt)} ago`),
|
||||
],
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'r'],
|
||||
hsep: ' '.repeat(4),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ hsep: 4 }
|
||||
);
|
||||
|
||||
client.output.log(`The following alias will be removed permanently`);
|
||||
|
||||
@@ -45,10 +45,16 @@ export default async function bisect(client: Client): Promise<number> {
|
||||
|
||||
let bad =
|
||||
argv['--bad'] ||
|
||||
(await prompt(client, `Specify a URL where the bug occurs:`));
|
||||
(await client.input.text({
|
||||
message: `Specify a URL where the bug occurs:`,
|
||||
validate: val => (val ? true : 'A URL must be provided'),
|
||||
}));
|
||||
let good =
|
||||
argv['--good'] ||
|
||||
(await prompt(client, `Specify a URL where the bug does not occur:`));
|
||||
(await client.input.text({
|
||||
message: `Specify a URL where the bug does not occur:`,
|
||||
validate: val => (val ? true : 'A URL must be provided'),
|
||||
}));
|
||||
let subpath = argv['--path'] || '';
|
||||
let run = argv['--run'] || '';
|
||||
const openEnabled = argv['--open'] || false;
|
||||
@@ -97,10 +103,10 @@ export default async function bisect(client: Client): Promise<number> {
|
||||
}
|
||||
|
||||
if (!subpath) {
|
||||
subpath = await prompt(
|
||||
client,
|
||||
`Specify the URL subpath where the bug occurs:`
|
||||
);
|
||||
subpath = await client.input.text({
|
||||
message: `Specify the URL subpath where the bug occurs:`,
|
||||
validate: val => (val ? true : 'A subpath must be provided'),
|
||||
});
|
||||
}
|
||||
|
||||
output.spinner('Retrieving deployments…');
|
||||
@@ -278,9 +284,7 @@ export default async function bisect(client: Client): Promise<number> {
|
||||
if (openEnabled) {
|
||||
await open(testUrl);
|
||||
}
|
||||
const answer = await client.prompt({
|
||||
type: 'expand',
|
||||
name: 'action',
|
||||
action = await client.input.expand({
|
||||
message: 'Select an action:',
|
||||
choices: [
|
||||
{ key: 'g', name: 'Good', value: 'good' },
|
||||
@@ -288,7 +292,6 @@ export default async function bisect(client: Client): Promise<number> {
|
||||
{ key: 's', name: 'Skip', value: 'skip' },
|
||||
],
|
||||
});
|
||||
action = answer.action;
|
||||
}
|
||||
|
||||
if (action === 'good') {
|
||||
@@ -338,19 +341,3 @@ function getCommit(deployment: Deployment) {
|
||||
deployment.meta?.bitbucketCommitMessage;
|
||||
return { sha, message };
|
||||
}
|
||||
|
||||
async function prompt(client: Client, message: string): Promise<string> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { val } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'val',
|
||||
message,
|
||||
});
|
||||
if (val) {
|
||||
return val;
|
||||
} else {
|
||||
client.output.error('A value must be specified');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import Client from '../../util/client';
|
||||
import getScope from '../../util/get-scope';
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from '../../util/get-pagination-opts';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import getCerts from '../../util/certs/get-certs';
|
||||
import strlen from '../../util/strlen';
|
||||
import type { Cert } from '@vercel-internals/types';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
@@ -70,11 +69,7 @@ async function ls(
|
||||
function formatCertsTable(certsList: Cert[]) {
|
||||
return `${table(
|
||||
[formatCertsTableHead(), ...formatCertsTableBody(certsList)],
|
||||
{
|
||||
align: ['l', 'l', 'r', 'c', 'r'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ align: ['l', 'l', 'r', 'c', 'r'], hsep: 2 }
|
||||
).replace(/^(.*)/gm, ' $1')}\n`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import plural from 'pluralize';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import type { Cert } from '@vercel-internals/types';
|
||||
import * as ERRORS from '../../util/errors-ts';
|
||||
import { Output } from '../../util/output';
|
||||
@@ -98,11 +98,11 @@ function readConfirmation(output: Output, msg: string, certs: Cert[]) {
|
||||
output.print(
|
||||
`${table(certs.map(formatCertRow), {
|
||||
align: ['l', 'r', 'l'],
|
||||
hsep: ' '.repeat(6),
|
||||
hsep: 6,
|
||||
}).replace(/^(.*)/gm, ' $1')}\n`
|
||||
);
|
||||
output.print(
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('(y/N) ')}`
|
||||
);
|
||||
process.stdin
|
||||
.on('data', d => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
getSupportedNodeVersion,
|
||||
scanParentDirs,
|
||||
} from '@vercel/build-utils';
|
||||
import { isZeroConfigBuild } from '../../util/is-zero-config-build';
|
||||
import {
|
||||
fileNameSymbol,
|
||||
VALID_ARCHIVE_FORMATS,
|
||||
@@ -304,7 +303,10 @@ export default async (client: Client): Promise<number> => {
|
||||
}
|
||||
|
||||
// build `--prebuilt`
|
||||
let vercelOutputDir: string | undefined;
|
||||
if (argv['--prebuilt']) {
|
||||
vercelOutputDir = join(cwd, '.vercel/output');
|
||||
|
||||
// For repo-style linking, update `cwd` to be the Project
|
||||
// subdirectory when `rootDirectory` setting is defined
|
||||
if (
|
||||
@@ -312,10 +314,10 @@ export default async (client: Client): Promise<number> => {
|
||||
link.repoRoot &&
|
||||
link.project.rootDirectory
|
||||
) {
|
||||
cwd = join(cwd, link.project.rootDirectory);
|
||||
vercelOutputDir = join(cwd, link.project.rootDirectory, '.vercel/output');
|
||||
}
|
||||
|
||||
const prebuiltExists = await fs.pathExists(join(cwd, '.vercel/output'));
|
||||
const prebuiltExists = await fs.pathExists(vercelOutputDir);
|
||||
if (!prebuiltExists) {
|
||||
error(
|
||||
`The ${param(
|
||||
@@ -327,7 +329,7 @@ export default async (client: Client): Promise<number> => {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const prebuiltBuild = await getPrebuiltJson(cwd);
|
||||
const prebuiltBuild = await getPrebuiltJson(vercelOutputDir);
|
||||
|
||||
// Ensure that there was not a build error
|
||||
const prebuiltError =
|
||||
@@ -518,23 +520,6 @@ export default async (client: Client): Promise<number> => {
|
||||
);
|
||||
}
|
||||
|
||||
const { packageJson } = await scanParentDirs(
|
||||
join(cwd, project?.rootDirectory ?? ''),
|
||||
true,
|
||||
cwd
|
||||
);
|
||||
let nodeVersion: string | undefined;
|
||||
if (packageJson?.engines?.node) {
|
||||
try {
|
||||
const { range } = await getSupportedNodeVersion(packageJson.engines.node);
|
||||
nodeVersion = range;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
output.warn(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// if this flag is not set, use `undefined` to allow the project setting to be used
|
||||
const autoAssignCustomDomains = argv['--skip-domain'] ? false : undefined;
|
||||
@@ -546,6 +531,7 @@ export default async (client: Client): Promise<number> => {
|
||||
forceNew: argv['--force'],
|
||||
withCache: argv['--with-cache'],
|
||||
prebuilt: argv['--prebuilt'],
|
||||
vercelOutputDir,
|
||||
rootDirectory,
|
||||
quiet,
|
||||
wantsPublic: Boolean(argv['--public'] || localConfig.public),
|
||||
@@ -565,15 +551,14 @@ export default async (client: Client): Promise<number> => {
|
||||
autoAssignCustomDomains,
|
||||
};
|
||||
|
||||
if (isZeroConfigBuild(localConfig.builds)) {
|
||||
if (!localConfig.builds || localConfig.builds.length === 0) {
|
||||
// Only add projectSettings for zero config deployments
|
||||
createArgs.projectSettings = {
|
||||
sourceFilesOutsideRootDirectory,
|
||||
rootDirectory,
|
||||
nodeVersion,
|
||||
};
|
||||
|
||||
if (status !== 'not_linked') {
|
||||
if (status === 'linked') {
|
||||
createArgs.projectSettings = {
|
||||
...createArgs.projectSettings,
|
||||
...localConfigurationOverrides,
|
||||
@@ -581,6 +566,30 @@ export default async (client: Client): Promise<number> => {
|
||||
}
|
||||
}
|
||||
|
||||
// Read the `engines.node` field from `package.json` and send as a
|
||||
// `projectSettings` property as an optimization (so that the API
|
||||
// does not need to retrieve the file to do this check).
|
||||
const { packageJson } = await scanParentDirs(
|
||||
join(cwd, project?.rootDirectory ?? ''),
|
||||
true,
|
||||
cwd
|
||||
);
|
||||
let nodeVersion: string | undefined;
|
||||
if (packageJson?.engines?.node) {
|
||||
try {
|
||||
const { range } = await getSupportedNodeVersion(
|
||||
packageJson.engines.node
|
||||
);
|
||||
nodeVersion = range;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
output.warn(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!createArgs.projectSettings) createArgs.projectSettings = {};
|
||||
createArgs.projectSettings.nodeVersion = nodeVersion;
|
||||
|
||||
deployment = await createDeploy(
|
||||
client,
|
||||
now,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import type { DNSRecord } from '@vercel-internals/types';
|
||||
import { Output } from '../../util/output';
|
||||
import Client from '../../util/client';
|
||||
@@ -71,11 +71,11 @@ function readConfirmation(
|
||||
output.print(
|
||||
`${table([getDeleteTableRow(domainName, record)], {
|
||||
align: ['l', 'r', 'l'],
|
||||
hsep: ' '.repeat(6),
|
||||
hsep: 6,
|
||||
}).replace(/^(.*)/gm, ' $1')}\n`
|
||||
);
|
||||
output.print(
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('(y/N) ')}`
|
||||
);
|
||||
process.stdin
|
||||
.on('data', d => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import getDomainPrice from '../../util/domains/get-domain-price';
|
||||
import getDomainStatus from '../../util/domains/get-domain-status';
|
||||
import getScope from '../../util/get-scope';
|
||||
import param from '../../util/output/param';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import purchaseDomain from '../../util/domains/purchase-domain';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
@@ -86,23 +86,25 @@ export default async function buy(
|
||||
autoRenew = true;
|
||||
} else {
|
||||
if (
|
||||
!(await promptBool(
|
||||
!(await confirm(
|
||||
client,
|
||||
`Buy now for ${chalk.bold(`$${price}`)} (${`${period}yr${
|
||||
period > 1 ? 's' : ''
|
||||
}`})?`,
|
||||
client
|
||||
false
|
||||
))
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
autoRenew = await promptBool(
|
||||
autoRenew = await confirm(
|
||||
client,
|
||||
renewalPrice.period === 1
|
||||
? `Auto renew yearly for ${chalk.bold(`$${price}`)}?`
|
||||
: `Auto renew every ${renewalPrice.period} years for ${chalk.bold(
|
||||
`$${price}`
|
||||
)}?`,
|
||||
{ ...client, defaultValue: true }
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import textInput from '../../util/input/text';
|
||||
import param from '../../util/output/param';
|
||||
import getDomainAliases from '../../util/alias/get-domain-aliases';
|
||||
import getDomainByName from '../../util/domains/get-domain-by-name';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import getTeams from '../../util/teams/get-teams';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
@@ -67,11 +67,12 @@ export default async function move(
|
||||
)} will have 24 hours to accept your move request before it expires.`
|
||||
);
|
||||
if (
|
||||
!(await promptBool(
|
||||
!(await confirm(
|
||||
client,
|
||||
`Are you sure you want to move ${param(domainName)} to ${param(
|
||||
destination
|
||||
)}?`,
|
||||
client
|
||||
false
|
||||
))
|
||||
) {
|
||||
output.log('Canceled');
|
||||
@@ -88,9 +89,10 @@ export default async function move(
|
||||
)} will be removed. Run ${getCommandName(`alias ls`)} to list them.`
|
||||
);
|
||||
if (
|
||||
!(await promptBool(
|
||||
!(await confirm(
|
||||
client,
|
||||
`Are you sure you want to move ${param(domainName)}?`,
|
||||
client
|
||||
false
|
||||
))
|
||||
) {
|
||||
output.log('Canceled');
|
||||
|
||||
@@ -13,7 +13,7 @@ import removeDomainByName from '../../util/domains/remove-domain-by-name';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import * as ERRORS from '../../util/errors-ts';
|
||||
import param from '../../util/output/param';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import setCustomSuffix from '../../util/domains/set-custom-suffix';
|
||||
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
@@ -81,9 +81,10 @@ export default async function rm(
|
||||
const skipConfirmation = opts['--yes'] || false;
|
||||
if (
|
||||
!skipConfirmation &&
|
||||
!(await promptBool(
|
||||
!(await confirm(
|
||||
client,
|
||||
`Are you sure you want to remove ${param(domainName)}?`,
|
||||
client
|
||||
false
|
||||
))
|
||||
) {
|
||||
output.log('Canceled');
|
||||
@@ -222,7 +223,11 @@ async function removeDomain(
|
||||
|
||||
if (
|
||||
!skipConfirmation &&
|
||||
!(await promptBool(`Remove conflicts associated with domain?`, client))
|
||||
!(await confirm(
|
||||
client,
|
||||
`Remove conflicts associated with domain?`,
|
||||
false
|
||||
))
|
||||
) {
|
||||
output.log('Canceled');
|
||||
return 0;
|
||||
|
||||
@@ -9,7 +9,7 @@ import stamp from '../../util/output/stamp';
|
||||
import getAuthCode from '../../util/domains/get-auth-code';
|
||||
import getDomainPrice from '../../util/domains/get-domain-price';
|
||||
import checkTransfer from '../../util/domains/check-transfer';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import isRootDomain from '../../util/is-root-domain';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
@@ -67,11 +67,12 @@ export default async function transferIn(
|
||||
|
||||
const authCode = await getAuthCode(opts['--code']);
|
||||
|
||||
const shouldTransfer = await promptBool(
|
||||
const shouldTransfer = await confirm(
|
||||
client,
|
||||
transferPolicy === 'no-change'
|
||||
? `Transfer now for ${chalk.bold(`$${price}`)}?`
|
||||
: `Transfer now with 1yr renewal for ${chalk.bold(`$${price}`)}?`,
|
||||
client
|
||||
false
|
||||
);
|
||||
if (!shouldTransfer) {
|
||||
return 0;
|
||||
|
||||
48
packages/cli/src/commands/env/add.ts
vendored
48
packages/cli/src/commands/env/add.ts
vendored
@@ -20,6 +20,7 @@ import { isAPIError } from '../../util/errors-ts';
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
'--sensitive': boolean;
|
||||
'--force': boolean;
|
||||
};
|
||||
|
||||
export default async function add(
|
||||
@@ -29,9 +30,6 @@ export default async function add(
|
||||
args: string[],
|
||||
output: Output
|
||||
) {
|
||||
// improve the way we show inquirer prompts
|
||||
await import('../../util/input/patch-inquirer');
|
||||
|
||||
const stdInput = await readStandardInput(client.stdin);
|
||||
let [envName, envTargetArg, envGitBranch] = args;
|
||||
|
||||
@@ -66,18 +64,11 @@ export default async function add(
|
||||
envTargets.push(envTargetArg);
|
||||
}
|
||||
|
||||
while (!envName) {
|
||||
const { inputName } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputName',
|
||||
if (!envName) {
|
||||
envName = await client.input.text({
|
||||
message: `What’s the name of the variable?`,
|
||||
validate: val => (val ? true : 'Name cannot be empty'),
|
||||
});
|
||||
|
||||
envName = inputName;
|
||||
|
||||
if (!inputName) {
|
||||
output.error('Name cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
const { envs } = await getEnvRecords(
|
||||
@@ -91,7 +82,7 @@ export default async function add(
|
||||
);
|
||||
const choices = envTargetChoices.filter(c => !existing.has(c.value));
|
||||
|
||||
if (choices.length === 0) {
|
||||
if (choices.length === 0 && !opts['--force']) {
|
||||
output.error(
|
||||
`The variable ${param(
|
||||
envName
|
||||
@@ -107,26 +98,18 @@ export default async function add(
|
||||
if (stdInput) {
|
||||
envValue = stdInput;
|
||||
} else {
|
||||
const { inputValue } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputValue',
|
||||
envValue = await client.input.text({
|
||||
message: `What’s the value of ${envName}?`,
|
||||
});
|
||||
|
||||
envValue = inputValue || '';
|
||||
}
|
||||
|
||||
while (envTargets.length === 0) {
|
||||
const { inputTargets } = await client.prompt({
|
||||
name: 'inputTargets',
|
||||
type: 'checkbox',
|
||||
envTargets = await client.input.checkbox({
|
||||
message: `Add ${envName} to which Environments (select multiple)?`,
|
||||
choices,
|
||||
});
|
||||
|
||||
envTargets = inputTargets;
|
||||
|
||||
if (inputTargets.length === 0) {
|
||||
if (envTargets.length === 0) {
|
||||
output.error('Please select at least one Environment');
|
||||
}
|
||||
}
|
||||
@@ -137,15 +120,13 @@ export default async function add(
|
||||
envTargets.length === 1 &&
|
||||
envTargets[0] === 'preview'
|
||||
) {
|
||||
const { inputValue } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputValue',
|
||||
envGitBranch = await client.input.text({
|
||||
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
|
||||
});
|
||||
envGitBranch = inputValue || '';
|
||||
}
|
||||
|
||||
const type = opts['--sensitive'] ? 'sensitive' : 'encrypted';
|
||||
const upsert = opts['--force'] ? 'true' : '';
|
||||
|
||||
const addStamp = stamp();
|
||||
try {
|
||||
@@ -154,6 +135,7 @@ export default async function add(
|
||||
output,
|
||||
client,
|
||||
project.id,
|
||||
upsert,
|
||||
type,
|
||||
envName,
|
||||
envValue,
|
||||
@@ -170,9 +152,11 @@ export default async function add(
|
||||
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`Added Environment Variable ${chalk.bold(
|
||||
envName
|
||||
)} to Project ${chalk.bold(project.name)} ${chalk.gray(addStamp())}`,
|
||||
`${
|
||||
opts['--force'] ? 'Overrode' : 'Added'
|
||||
} Environment Variable ${chalk.bold(envName)} to Project ${chalk.bold(
|
||||
project.name
|
||||
)} ${chalk.gray(addStamp())}`,
|
||||
emoji('success')
|
||||
)}\n`
|
||||
);
|
||||
|
||||
12
packages/cli/src/commands/env/command.ts
vendored
12
packages/cli/src/commands/env/command.ts
vendored
@@ -43,6 +43,14 @@ export const envCommand: Command = {
|
||||
deprecated: false,
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
name: 'force',
|
||||
description: 'Force overwrites when a command would normally fail',
|
||||
shorthand: null,
|
||||
type: 'boolean',
|
||||
deprecated: false,
|
||||
multi: false,
|
||||
},
|
||||
],
|
||||
examples: [],
|
||||
},
|
||||
@@ -126,6 +134,10 @@ export const envCommand: Command = {
|
||||
`${packageName} env add DB_PASS production`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Override an existing Environment Variable of same target (production, preview, deployment)',
|
||||
value: `${packageName} env add API_TOKEN --force`,
|
||||
},
|
||||
{
|
||||
name: 'Add a sensitive Environment Variable',
|
||||
value: `${packageName} env add API_TOKEN --sensitive`,
|
||||
|
||||
1
packages/cli/src/commands/env/index.ts
vendored
1
packages/cli/src/commands/env/index.ts
vendored
@@ -35,6 +35,7 @@ export default async function main(client: Client) {
|
||||
'--environment': String,
|
||||
'--git-branch': String,
|
||||
'--sensitive': Boolean,
|
||||
'--force': Boolean,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
|
||||
21
packages/cli/src/commands/env/rm.ts
vendored
21
packages/cli/src/commands/env/rm.ts
vendored
@@ -29,9 +29,6 @@ export default async function rm(
|
||||
args: string[],
|
||||
output: Output
|
||||
) {
|
||||
// improve the way we show inquirer prompts
|
||||
await import('../../util/input/patch-inquirer');
|
||||
|
||||
if (args.length > 3) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${getCommandName(
|
||||
@@ -43,19 +40,11 @@ export default async function rm(
|
||||
|
||||
let [envName, envTarget, envGitBranch] = args;
|
||||
|
||||
while (!envName) {
|
||||
const { inputName } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputName',
|
||||
if (!envName) {
|
||||
envName = await client.input.text({
|
||||
message: `What’s the name of the variable?`,
|
||||
validate: val => (val ? true : 'Name cannot be empty'),
|
||||
});
|
||||
|
||||
if (!inputName) {
|
||||
output.error(`Name cannot be empty`);
|
||||
continue;
|
||||
}
|
||||
|
||||
envName = inputName;
|
||||
}
|
||||
|
||||
if (!isValidEnvTarget(envTarget)) {
|
||||
@@ -86,9 +75,7 @@ export default async function rm(
|
||||
}
|
||||
|
||||
while (envs.length > 1) {
|
||||
const { id } = await client.prompt({
|
||||
name: 'id',
|
||||
type: 'list',
|
||||
const id = await client.input.select({
|
||||
message: `Remove ${envName} from which Environments?`,
|
||||
choices: envs.map(env => ({ value: env.id, name: formatEnvTarget(env) })),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { LOGO, NAME } from '@vercel-internals/constants';
|
||||
import Table, { CellOptions } from 'cli-table3';
|
||||
import { noBorderChars } from '../util/output/table';
|
||||
|
||||
const INDENT = ' '.repeat(2);
|
||||
const NEWLINE = '\n';
|
||||
@@ -39,23 +40,7 @@ type _CellOptions = CellOptions & {
|
||||
};
|
||||
|
||||
const tableOptions = {
|
||||
chars: {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: '',
|
||||
},
|
||||
chars: noBorderChars,
|
||||
style: {
|
||||
'padding-left': 0,
|
||||
'padding-right': 0,
|
||||
|
||||
@@ -6,7 +6,7 @@ import chalk from 'chalk';
|
||||
// @ts-ignore
|
||||
import listInput from '../../util/input/list';
|
||||
import listItem from '../../util/output/list-item';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import toHumanPath from '../../util/humanize-path';
|
||||
import Client from '../../util/client';
|
||||
import info from '../../util/output/info';
|
||||
@@ -46,6 +46,10 @@ export default async function init(
|
||||
const exampleList = examples.filter(x => x.visible).map(x => x.name);
|
||||
|
||||
if (!name) {
|
||||
if (client.stdin.isTTY !== true) {
|
||||
client.output.print(`No framework provided`);
|
||||
return 0;
|
||||
}
|
||||
const chosen = await chooseFromDropdown(
|
||||
client,
|
||||
'Select example:',
|
||||
@@ -122,7 +126,7 @@ async function extractExample(
|
||||
ver: string = 'v2'
|
||||
) {
|
||||
const { output } = client;
|
||||
const folder = prepareFolder(process.cwd(), dir || name, force);
|
||||
const folder = prepareFolder(client.cwd, dir || name, force);
|
||||
output.spinner(`Fetching ${name}`);
|
||||
|
||||
const url = `${EXAMPLE_API}/${ver}/download/${name}.tar.gz`;
|
||||
@@ -147,7 +151,7 @@ async function extractExample(
|
||||
const successLog = `Initialized "${chalk.bold(
|
||||
name
|
||||
)}" example in ${chalk.bold(toHumanPath(folder))}.`;
|
||||
const folderRel = path.relative(process.cwd(), folder);
|
||||
const folderRel = path.relative(client.cwd, folder);
|
||||
const deployHint =
|
||||
folderRel === ''
|
||||
? listItem(`To deploy, run ${getCommandName()}.`)
|
||||
@@ -209,14 +213,14 @@ async function guess(client: Client, exampleList: string[], name: string) {
|
||||
)} to see the list of available examples.`
|
||||
);
|
||||
|
||||
if (process.stdout.isTTY !== true) {
|
||||
if (client.stdin.isTTY !== true) {
|
||||
throw GuessError;
|
||||
}
|
||||
|
||||
const found = didYouMean(name, exampleList, 0.7);
|
||||
|
||||
if (typeof found === 'string') {
|
||||
if (await promptBool(`Did you mean ${chalk.bold(found)}?`, client)) {
|
||||
if (await confirm(client, `Did you mean ${chalk.bold(found)}?`, false)) {
|
||||
return found;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import title from 'title';
|
||||
import Now from '../../util';
|
||||
import getArgs from '../../util/get-args';
|
||||
import { handleError } from '../../util/error';
|
||||
import elapsed from '../../util/output/elapsed';
|
||||
import strlen from '../../util/strlen';
|
||||
import toHost from '../../util/to-host';
|
||||
import parseMeta from '../../util/parse-meta';
|
||||
import { isValidName } from '../../util/is-valid-name';
|
||||
@@ -275,11 +274,7 @@ export default async function list(client: Client) {
|
||||
app === null ? filterUniqueApps() : () => true
|
||||
),
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'l', 'l', 'l'],
|
||||
hsep: ' '.repeat(5),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ hsep: 5 }
|
||||
).replace(/^/gm, ' ')}\n\n`
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import type { Project } from '@vercel-internals/types';
|
||||
import Client from '../../util/client';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import strlen from '../../util/strlen';
|
||||
import { NODE_VERSIONS } from '@vercel/build-utils';
|
||||
|
||||
export default async function list(
|
||||
@@ -100,11 +99,7 @@ export default async function list(
|
||||
])
|
||||
.flat(),
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(3),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ hsep: 3 }
|
||||
).replace(/^/gm, ' ');
|
||||
output.print(`\n${tablePrint}\n\n`);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export default async function requestPromote({
|
||||
|
||||
if (deployment.target !== 'production' && !yes) {
|
||||
const question =
|
||||
'This deployment does not target production, therefore promotion will not apply production environment variables. Are you sure you want to continue?';
|
||||
'This deployment is not a production deployment and cannot be directly promoted. A new deployment will be built using your production environment. Are you sure you want to continue?';
|
||||
const answer = await confirm(client, question, false);
|
||||
if (!answer) {
|
||||
output.error('Canceled');
|
||||
@@ -43,7 +43,7 @@ export default async function requestPromote({
|
||||
}
|
||||
|
||||
// request the promotion
|
||||
await client.fetch(`/v9/projects/${project.id}/promote/${deployment.id}`, {
|
||||
await client.fetch(`/v10/projects/${project.id}/promote/${deployment.id}`, {
|
||||
body: {}, // required
|
||||
json: false,
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import plural from 'pluralize';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import Now from '../../util';
|
||||
import getAliases from '../../util/alias/get-aliases';
|
||||
import elapsed from '../../util/output/elapsed';
|
||||
@@ -169,7 +169,7 @@ export default async function remove(client: Client) {
|
||||
`or projects matching ` +
|
||||
`${ids
|
||||
.map(id => chalk.bold(`"${id}"`))
|
||||
.join(', ')}. Run ${getCommandName('ls')} to list.`
|
||||
.join(', ')}. Run ${getCommandName('projects ls')} to list.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ function readConfirmation(
|
||||
const url = depl.url ? chalk.underline(`https://${depl.url}`) : '';
|
||||
return [` ${depl.id}`, url, time];
|
||||
}),
|
||||
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
|
||||
{ align: ['l', 'r', 'l'], hsep: 6 }
|
||||
);
|
||||
output.print(`${deploymentTable}\n`);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ function readConfirmation(
|
||||
}
|
||||
|
||||
output.print(
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('(y/N) ')}`
|
||||
);
|
||||
|
||||
process.stdin
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import isErrnoException from '@vercel/error-utils';
|
||||
import chalk from 'chalk';
|
||||
import table from 'text-table';
|
||||
import table from '../../util/output/table';
|
||||
import ms from 'ms';
|
||||
import strlen from '../../util/strlen';
|
||||
import { handleError, error } from '../../util/error';
|
||||
import NowSecrets from '../../util/secrets';
|
||||
import getScope from '../../util/get-scope';
|
||||
@@ -124,11 +123,7 @@ async function run({ output, contextName, currentTeam, client }) {
|
||||
chalk.gray(`${ms(cur - new Date(secret.created))} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ hsep: 2 }
|
||||
);
|
||||
|
||||
if (out) {
|
||||
@@ -285,7 +280,7 @@ async function readConfirmation(client, output, secret, contextName) {
|
||||
const time = chalk.gray(`${ms(new Date() - new Date(secret.created))} ago`);
|
||||
const tbl = table([[chalk.bold(secret.name), time]], {
|
||||
align: ['r', 'l'],
|
||||
hsep: ' '.repeat(6),
|
||||
hsep: 6,
|
||||
});
|
||||
|
||||
output.print(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import chars from '../../util/output/chars';
|
||||
import table from '../../util/output/table';
|
||||
import { gray } from 'chalk';
|
||||
import getUser from '../../util/get-user';
|
||||
import getTeams from '../../util/teams/get-teams';
|
||||
import { packageName } from '../../util/pkg-name';
|
||||
@@ -53,7 +54,7 @@ export default async function list(client: Client): Promise<number> {
|
||||
id,
|
||||
name,
|
||||
value: slug,
|
||||
current: id === currentTeam ? chars.tick : '',
|
||||
prefix: id === currentTeam ? chars.tick : ' ',
|
||||
}));
|
||||
|
||||
if (user.version !== 'northstar') {
|
||||
@@ -61,7 +62,7 @@ export default async function list(client: Client): Promise<number> {
|
||||
id: user.id,
|
||||
name: user.email,
|
||||
value: user.username || user.email,
|
||||
current: accountIsCurrent ? chars.tick : '',
|
||||
prefix: accountIsCurrent ? chars.tick : ' ',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,14 +77,22 @@ export default async function list(client: Client): Promise<number> {
|
||||
output.stopSpinner();
|
||||
client.stdout.write('\n'); // empty line
|
||||
|
||||
table(
|
||||
['', 'id', 'email / name'],
|
||||
teamList.map(team => [team.current, team.value, team.name]),
|
||||
[1, 5],
|
||||
(str: string) => {
|
||||
client.stdout.write(str);
|
||||
}
|
||||
const teamTable = table(
|
||||
[
|
||||
['id', 'email / name'].map(str => gray(str)),
|
||||
...teamList.map(team => [team.value, team.name]),
|
||||
],
|
||||
{ hsep: 5 }
|
||||
);
|
||||
client.stderr.write(
|
||||
currentTeam
|
||||
? teamTable
|
||||
.split('\n')
|
||||
.map((line, i) => `${i > 0 ? teamList[i - 1].prefix : ' '} ${line}`)
|
||||
.join('\n')
|
||||
: teamTable
|
||||
);
|
||||
client.stderr.write('\n');
|
||||
|
||||
if (pagination?.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d']);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { bold } from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { bold, gray } from 'chalk';
|
||||
import checkbox from '@inquirer/checkbox';
|
||||
import confirm from '@inquirer/confirm';
|
||||
import expand from '@inquirer/expand';
|
||||
import input from '@inquirer/input';
|
||||
import select from '@inquirer/select';
|
||||
import { EventEmitter } from 'events';
|
||||
import { URL } from 'url';
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
@@ -66,8 +70,8 @@ export default class Client extends EventEmitter implements Stdio {
|
||||
agent?: Agent;
|
||||
localConfig?: VercelConfig;
|
||||
localConfigPath?: string;
|
||||
prompt!: inquirer.PromptModule;
|
||||
requestIdCounter: number;
|
||||
input;
|
||||
|
||||
constructor(opts: ClientOptions) {
|
||||
super();
|
||||
@@ -83,7 +87,29 @@ export default class Client extends EventEmitter implements Stdio {
|
||||
this.localConfig = opts.localConfig;
|
||||
this.localConfigPath = opts.localConfigPath;
|
||||
this.requestIdCounter = 1;
|
||||
this._createPromptModule();
|
||||
|
||||
const theme = {
|
||||
prefix: gray('?'),
|
||||
style: { answer: gray },
|
||||
};
|
||||
this.input = {
|
||||
text: (opts: Parameters<typeof input>[0]) =>
|
||||
input({ theme, ...opts }, { input: this.stdin, output: this.stderr }),
|
||||
checkbox: <T>(opts: Parameters<typeof checkbox<T>>[0]) =>
|
||||
checkbox<T>(
|
||||
{ theme, ...opts },
|
||||
{ input: this.stdin, output: this.stderr }
|
||||
),
|
||||
expand: (opts: Parameters<typeof expand>[0]) =>
|
||||
expand({ theme, ...opts }, { input: this.stdin, output: this.stderr }),
|
||||
confirm: (opts: Parameters<typeof confirm>[0]) =>
|
||||
confirm({ theme, ...opts }, { input: this.stdin, output: this.stderr }),
|
||||
select: <T>(opts: Parameters<typeof select<T>>[0]) =>
|
||||
select<T>(
|
||||
{ theme, ...opts },
|
||||
{ input: this.stdin, output: this.stderr }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
|
||||
@@ -229,13 +255,6 @@ export default class Client extends EventEmitter implements Stdio {
|
||||
this.output.debug(`Retrying: ${error}\n${error.stack}`);
|
||||
};
|
||||
|
||||
_createPromptModule() {
|
||||
this.prompt = inquirer.createPromptModule({
|
||||
input: this.stdin as NodeJS.ReadStream,
|
||||
output: this.stderr as NodeJS.WriteStream,
|
||||
});
|
||||
}
|
||||
|
||||
get cwd(): string {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async function getPrebuiltJson(
|
||||
directory: string
|
||||
): Promise<BuildsManifest | null> {
|
||||
try {
|
||||
return await fs.readJSON(join(directory, '.vercel/output/builds.json'));
|
||||
return await fs.readJSON(join(directory, 'builds.json'));
|
||||
} catch (error) {
|
||||
// ignoring error
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export default async function processDeployment({
|
||||
withCache?: boolean;
|
||||
org: Org;
|
||||
prebuilt: boolean;
|
||||
vercelOutputDir?: string;
|
||||
projectName: string;
|
||||
isSettingUpProject: boolean;
|
||||
archive?: ArchiveFormat;
|
||||
@@ -71,6 +72,7 @@ export default async function processDeployment({
|
||||
withCache,
|
||||
quiet,
|
||||
prebuilt,
|
||||
vercelOutputDir,
|
||||
rootDirectory,
|
||||
} = args;
|
||||
|
||||
@@ -92,6 +94,7 @@ export default async function processDeployment({
|
||||
force,
|
||||
withCache,
|
||||
prebuilt,
|
||||
vercelOutputDir,
|
||||
rootDirectory,
|
||||
skipAutoDetectionConfirmation,
|
||||
archive,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import type { DNSRecordData } from '@vercel-internals/types';
|
||||
import textInput from '../input/text';
|
||||
import promptBool from '../input/prompt-bool';
|
||||
import confirm from '../input/confirm';
|
||||
import Client from '../client';
|
||||
|
||||
const RECORD_TYPES = ['A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'SRV', 'TXT'];
|
||||
@@ -89,7 +89,7 @@ export default async function getDNSData(
|
||||
}
|
||||
|
||||
async function verifyData(client: Client) {
|
||||
return promptBool('Is this correct?', client);
|
||||
return confirm(client, 'Is this correct?', false);
|
||||
}
|
||||
|
||||
async function getRecordName(type: string) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import Client from '../client';
|
||||
import eraseLines from '../output/erase-lines';
|
||||
import getDomainPrice from './get-domain-price';
|
||||
import getDomainStatus from './get-domain-status';
|
||||
import promptBool from '../input/prompt-bool';
|
||||
import confirm from '../input/confirm';
|
||||
import purchaseDomain from './purchase-domain';
|
||||
import stamp from '../output/stamp';
|
||||
import * as ERRORS from '../errors-ts';
|
||||
@@ -51,11 +51,12 @@ export default async function purchaseDomainIfAvailable(
|
||||
);
|
||||
|
||||
if (
|
||||
!(await promptBool(
|
||||
!(await confirm(
|
||||
client,
|
||||
`Buy ${chalk.underline(domain)} for ${chalk.bold(
|
||||
`$${price}`
|
||||
)} (${plural('yr', period, true)})?`,
|
||||
client
|
||||
false
|
||||
))
|
||||
) {
|
||||
output.print(eraseLines(1));
|
||||
|
||||
7
packages/cli/src/util/env/add-env-record.ts
vendored
7
packages/cli/src/util/env/add-env-record.ts
vendored
@@ -10,14 +10,16 @@ export default async function addEnvRecord(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string,
|
||||
upsert: string,
|
||||
type: ProjectEnvType,
|
||||
key: string,
|
||||
value: string,
|
||||
targets: ProjectEnvTarget[],
|
||||
gitBranch: string
|
||||
): Promise<void> {
|
||||
const actionWord = upsert ? 'Overriding' : 'Adding';
|
||||
output.debug(
|
||||
`Adding ${type} Environment Variable ${key} to ${targets.length} targets`
|
||||
`${actionWord} ${type} Environment Variable ${key} to ${targets.length} targets`
|
||||
);
|
||||
const body: Omit<ProjectEnvVariable, 'id'> = {
|
||||
type,
|
||||
@@ -26,7 +28,8 @@ export default async function addEnvRecord(
|
||||
target: targets,
|
||||
gitBranch: gitBranch || undefined,
|
||||
};
|
||||
const url = `/v8/projects/${projectId}/env`;
|
||||
const args = upsert ? `?upsert=${upsert}` : '';
|
||||
const url = `/v10/projects/${projectId}/env${args}`;
|
||||
await client.fetch(url, {
|
||||
method: 'POST',
|
||||
body,
|
||||
|
||||
2
packages/cli/src/util/env/get-env-records.ts
vendored
2
packages/cli/src/util/env/get-env-records.ts
vendored
@@ -48,7 +48,7 @@ export default async function getEnvRecords(
|
||||
query.set('source', source);
|
||||
}
|
||||
|
||||
const url = `/v8/projects/${projectId}/env?${query}`;
|
||||
const url = `/v10/projects/${projectId}/env?${query}`;
|
||||
|
||||
return client.fetch<{ envs: ProjectEnvVariable[] }>(url);
|
||||
}
|
||||
|
||||
28
packages/cli/src/util/env/known-error.ts
vendored
28
packages/cli/src/util/env/known-error.ts
vendored
@@ -1,12 +1,34 @@
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const knownErrorsCodes = new Set([
|
||||
'PAYMENT_REQUIRED',
|
||||
'BAD_REQUEST',
|
||||
'SYSTEM_ENV_WITH_VALUE',
|
||||
'RESERVED_ENV_VARIABLE',
|
||||
'ENV_ALREADY_EXISTS',
|
||||
'ENV_CONFLICT',
|
||||
'ENV_SHOULD_BE_A_SECRET',
|
||||
'EXISTING_KEY_AND_TARGET',
|
||||
'FORBIDDEN',
|
||||
'ID_NOT_FOUND',
|
||||
'INVALID_KEY',
|
||||
'INVALID_VALUE',
|
||||
'KEY_INVALID_CHARACTERS',
|
||||
'KEY_INVALID_LENGTH',
|
||||
'KEY_RESERVED',
|
||||
'RESERVED_ENV_VARIABLE',
|
||||
'MAX_ENVS_EXCEEDED',
|
||||
'MISSING_ID',
|
||||
'MISSING_KEY',
|
||||
'MISSING_TARGET',
|
||||
'MISSING_VALUE',
|
||||
'NOT_AUTHORIZED',
|
||||
'NOT_DECRYPTABLE',
|
||||
'SECRET_MISSING',
|
||||
'SYSTEM_ENV_WITH_VALUE',
|
||||
'TEAM_NOT_FOUND',
|
||||
'TOO_MANY_IDS',
|
||||
'TOO_MANY_KEYS',
|
||||
'UNKNOWN_ERROR',
|
||||
'VALUE_INVALID_LENGTH',
|
||||
'VALUE_INVALID_TYPE',
|
||||
]);
|
||||
|
||||
export function isKnownError(error: unknown) {
|
||||
|
||||
@@ -10,9 +10,9 @@ export default async function removeEnvRecord(
|
||||
): Promise<void> {
|
||||
output.debug(`Removing Environment Variable ${env.key}`);
|
||||
|
||||
const urlProject = `/v8/projects/${projectId}/env/${env.id}`;
|
||||
const url = `/v10/projects/${projectId}/env/${env.id}`;
|
||||
|
||||
await client.fetch<ProjectEnvVariable>(urlProject, {
|
||||
await client.fetch<ProjectEnvVariable>(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import chalk from 'chalk';
|
||||
import table from 'text-table';
|
||||
import strlen from './strlen';
|
||||
import table from './output/table';
|
||||
import { gray } from 'chalk';
|
||||
|
||||
const HEADER = ['name', 'type', 'value'].map(v => chalk.gray(v));
|
||||
const HEADER = ['name', 'type', 'value'].map(v => gray(v));
|
||||
|
||||
export default function formatDNSTable(
|
||||
rows: string[][],
|
||||
{ extraSpace = '' } = {}
|
||||
) {
|
||||
return table([HEADER, ...rows], {
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(8),
|
||||
stringLength: strlen,
|
||||
}).replace(/^(.*)/gm, `${extraSpace}$1`);
|
||||
export default function formatDNSTable(rows: string[][]) {
|
||||
return table([HEADER, ...rows], { hsep: 8 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import table from 'text-table';
|
||||
import strlen from './strlen';
|
||||
import table from './output/table';
|
||||
import chars from './output/chars';
|
||||
|
||||
export default function formatNSTable(
|
||||
@@ -35,10 +34,6 @@ export default function formatNSTable(
|
||||
],
|
||||
...rows,
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'l', 'l'],
|
||||
hsep: ' '.repeat(4),
|
||||
stringLength: strlen,
|
||||
}
|
||||
{ hsep: 4 }
|
||||
).replace(/^(.*)/gm, `${extraSpace}$1`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import table from 'text-table';
|
||||
import table from './output/table';
|
||||
import strlen from './strlen';
|
||||
|
||||
// header:
|
||||
@@ -19,9 +19,8 @@ import strlen from './strlen';
|
||||
// ]
|
||||
export default function formatTable(
|
||||
header: string[],
|
||||
align: Array<'l' | 'r' | 'c' | '.'>,
|
||||
blocks: { name?: string; rows: string[][] }[],
|
||||
hsep = ' '
|
||||
align: Array<'l' | 'r' | 'c'>,
|
||||
blocks: { name?: string; rows: string[][] }[]
|
||||
) {
|
||||
const nrCols = header.length;
|
||||
const padding = [];
|
||||
@@ -57,7 +56,7 @@ export default function formatTable(
|
||||
rows[i][j] = al === 'l' ? col + pad : pad + col;
|
||||
}
|
||||
}
|
||||
out += table(rows, { align, hsep, stringLength: strlen });
|
||||
out += table(rows, { align, hsep: 4 });
|
||||
}
|
||||
out += '\n\n';
|
||||
}
|
||||
|
||||
86
packages/cli/src/util/git-helpers.ts
Normal file
86
packages/cli/src/util/git-helpers.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
/** Defines the options for executing Git commands */
|
||||
export type GitExecOptions = Readonly<{
|
||||
/** If set to true, the function will throw
|
||||
* an error if any occurs during the execution of the Git command. By default,
|
||||
* it is set to false, meaning errors are caught and handled gracefully.*/
|
||||
unsafe?: boolean;
|
||||
/** Specifies the current working directory
|
||||
* from which the Git command should be executed.
|
||||
*/
|
||||
cwd: string;
|
||||
}>;
|
||||
|
||||
const DEFAULT_GIT_EXEC_OPTS = {
|
||||
unsafe: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to retrieve the Git directory for the specified working directory.
|
||||
*
|
||||
* This function runs the Git command to find the `.git` directory associated
|
||||
* with the current or specified working directory. This is useful for
|
||||
* determining if the current working environment is within a Git repository.
|
||||
*
|
||||
* @param {GitExecOptions} opts - The options for executing the Git command.
|
||||
* @returns {string | null} The path to the Git directory if found; otherwise, null.
|
||||
* If `opts.unsafe` is set to true and an error occurs, the function will throw
|
||||
* an error instead of returning null.
|
||||
*
|
||||
* @throws {Error} Can throw an error if `opts.unsafe` is set to `true`
|
||||
*/
|
||||
export function getGitDirectory(opts: GitExecOptions): string | null {
|
||||
const { cwd, unsafe } = { ...DEFAULT_GIT_EXEC_OPTS, ...opts };
|
||||
|
||||
try {
|
||||
const gitConfigPath = execSync('git rev-parse --git-dir', {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
return gitConfigPath;
|
||||
} catch (error) {
|
||||
if (unsafe) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given directory is a Git worktree or Git submodule.
|
||||
*
|
||||
* A Git worktree is a linked working tree, which allows you to have multiple
|
||||
* working trees attached to the same repository. This function checks if the
|
||||
* specified directory (or the current working directory if none is specified)
|
||||
* is a Git worktree by looking for the '.git/worktrees/' path in the Git
|
||||
* configuration.
|
||||
*
|
||||
* A Git submodule is a repository embedded inside another repository. This
|
||||
* function checks if the current working directory or the specified directory
|
||||
* is part of a Git submodule by looking for the '.git/modules/' path in the
|
||||
* Git configuration.
|
||||
*
|
||||
* @param {GitExecOptions} [opts={}] The options to use. Options include:
|
||||
* - `cwd`: The directory to check. Defaults to `process.cwd()`.
|
||||
* - `unsafe`: If true, throws if an error occurs during execution.
|
||||
* Defaults to `false`.
|
||||
* @returns {boolean} Returns `true` if the directory is a Git worktree or Git
|
||||
* Submodule, otherwise `false`.
|
||||
*
|
||||
* @throws {Error} Can throw an error if `opts.unsafe` is set to `true`
|
||||
*/
|
||||
export function isGitWorktreeOrSubmodule(opts: GitExecOptions): boolean {
|
||||
const gitDir = getGitDirectory(opts);
|
||||
|
||||
if (gitDir === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isGitWorktree = gitDir.includes('.git/worktrees/');
|
||||
const isGitSubmodule = gitDir.includes('.git/modules/');
|
||||
|
||||
return isGitWorktree || isGitSubmodule;
|
||||
}
|
||||
@@ -17,6 +17,10 @@ export default function handleError(error: unknown, { debug = false } = {}) {
|
||||
console.log(`> [debug] handling error: ${stack}`);
|
||||
}
|
||||
|
||||
if (message === 'User force closed the prompt with 0 null') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
console.error(
|
||||
errorOutput(
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface CreateOptions {
|
||||
project?: string;
|
||||
wantsPublic: boolean;
|
||||
prebuilt?: boolean;
|
||||
vercelOutputDir?: string;
|
||||
rootDirectory?: string | null;
|
||||
meta: Dictionary<string>;
|
||||
gitMetadata?: GitMetadata;
|
||||
@@ -117,6 +118,7 @@ export default class Now extends EventEmitter {
|
||||
name,
|
||||
project,
|
||||
prebuilt = false,
|
||||
vercelOutputDir,
|
||||
rootDirectory,
|
||||
wantsPublic,
|
||||
meta,
|
||||
@@ -179,6 +181,7 @@ export default class Now extends EventEmitter {
|
||||
skipAutoDetectionConfirmation,
|
||||
cwd,
|
||||
prebuilt,
|
||||
vercelOutputDir,
|
||||
rootDirectory,
|
||||
noWait,
|
||||
});
|
||||
|
||||
@@ -5,14 +5,8 @@ export default async function confirm(
|
||||
message: string,
|
||||
preferred: boolean
|
||||
): Promise<boolean> {
|
||||
await import('./patch-inquirer');
|
||||
|
||||
const answers = await client.prompt({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
return client.input.confirm({
|
||||
message,
|
||||
default: preferred,
|
||||
});
|
||||
|
||||
return answers.value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import inquirer from 'inquirer';
|
||||
import confirm from './confirm';
|
||||
import chalk from 'chalk';
|
||||
import { frameworkList, Framework } from '@vercel/frameworks';
|
||||
@@ -125,48 +124,26 @@ export default async function editProjectSettings(
|
||||
return settings;
|
||||
}
|
||||
|
||||
const choices = settingKeys.reduce<Array<{ name: string; value: string }>>(
|
||||
(acc, setting) => {
|
||||
const skip =
|
||||
setting === 'framework' ||
|
||||
setting === 'commandForIgnoringBuildStep' ||
|
||||
setting === 'installCommand' ||
|
||||
localConfigurationOverrides?.[setting];
|
||||
if (!skip) {
|
||||
acc.push({ name: settingMap[setting], value: setting });
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const choices = settingKeys.reduce((acc, setting) => {
|
||||
const skip =
|
||||
setting === 'framework' ||
|
||||
setting === 'commandForIgnoringBuildStep' ||
|
||||
setting === 'installCommand' ||
|
||||
localConfigurationOverrides?.[setting];
|
||||
if (skip) return acc;
|
||||
return [...acc, { name: settingMap[setting], value: setting }];
|
||||
}, [] as { name: string; value: ConfigKeys }[]);
|
||||
|
||||
const { settingFields } = await inquirer.prompt<{
|
||||
settingFields: Array<
|
||||
Exclude<
|
||||
ConfigKeys,
|
||||
'framework' | 'commandForIgnoringBuildStep' | 'installCommand'
|
||||
>
|
||||
>;
|
||||
}>({
|
||||
name: 'settingFields',
|
||||
type: 'checkbox',
|
||||
const settingFields = await client.input.checkbox({
|
||||
message: 'Which settings would you like to overwrite (select multiple)?',
|
||||
choices,
|
||||
});
|
||||
|
||||
for (let setting of settingFields) {
|
||||
const field = settingMap[setting];
|
||||
const answers = await inquirer.prompt<{
|
||||
[k in Exclude<
|
||||
ConfigKeys,
|
||||
'framework' | 'commandForIgnoringBuildStep' | 'installCommand'
|
||||
>]: string;
|
||||
}>({
|
||||
type: 'input',
|
||||
name: setting,
|
||||
settings[setting] = await client.input.text({
|
||||
message: `What's your ${chalk.bold(field)}?`,
|
||||
});
|
||||
settings[setting] = answers[setting];
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -75,70 +75,37 @@ export default async function inputProject(
|
||||
|
||||
if (shouldLinkProject) {
|
||||
// user wants to link a project
|
||||
let project: Project | ProjectNotFound | null = null;
|
||||
|
||||
while (!project || project instanceof ProjectNotFound) {
|
||||
const answers = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'existingProjectName',
|
||||
message: `What’s the name of your existing project?`,
|
||||
});
|
||||
const projectName = answers.existingProjectName as string;
|
||||
|
||||
if (!projectName) {
|
||||
output.error(`Project name cannot be empty`);
|
||||
continue;
|
||||
}
|
||||
|
||||
output.spinner('Verifying project name…', 1000);
|
||||
try {
|
||||
project = await getProjectByIdOrName(client, projectName, org.id);
|
||||
} finally {
|
||||
output.stopSpinner();
|
||||
}
|
||||
|
||||
if (project instanceof ProjectNotFound) {
|
||||
output.error(`Project not found`);
|
||||
}
|
||||
}
|
||||
|
||||
return project;
|
||||
let toLink: Project;
|
||||
await client.input.text({
|
||||
message: 'What’s the name of your existing project?',
|
||||
validate: async val => {
|
||||
if (!val) {
|
||||
return 'Project name cannot be empty';
|
||||
}
|
||||
const project = await getProjectByIdOrName(client, val, org.id);
|
||||
if (project instanceof ProjectNotFound) {
|
||||
return 'Project not found';
|
||||
}
|
||||
toLink = project;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return toLink!;
|
||||
}
|
||||
|
||||
// user wants to create a new project
|
||||
let newProjectName: string | null = null;
|
||||
|
||||
while (!newProjectName) {
|
||||
const answers = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'newProjectName',
|
||||
message: `What’s your project’s name?`,
|
||||
default: !detectedProject ? slugifiedName : undefined,
|
||||
});
|
||||
newProjectName = answers.newProjectName as string;
|
||||
|
||||
if (!newProjectName) {
|
||||
output.error(`Project name cannot be empty`);
|
||||
continue;
|
||||
}
|
||||
|
||||
output.spinner('Verifying project name…', 1000);
|
||||
let existingProject: Project | ProjectNotFound;
|
||||
try {
|
||||
existingProject = await getProjectByIdOrName(
|
||||
client,
|
||||
newProjectName,
|
||||
org.id
|
||||
);
|
||||
} finally {
|
||||
output.stopSpinner();
|
||||
}
|
||||
|
||||
if (existingProject && !(existingProject instanceof ProjectNotFound)) {
|
||||
output.print(`Project already exists`);
|
||||
newProjectName = null;
|
||||
}
|
||||
}
|
||||
|
||||
return newProjectName;
|
||||
return await client.input.text({
|
||||
message: `What’s your project’s name?`,
|
||||
default: !detectedProject ? slugifiedName : undefined,
|
||||
validate: async val => {
|
||||
if (!val) {
|
||||
return 'Project name cannot be empty';
|
||||
}
|
||||
const project = await getProjectByIdOrName(client, val, org.id);
|
||||
if (!(project instanceof ProjectNotFound)) {
|
||||
return 'Project already exists';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ export async function inputRootDirectory(
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { rootDirectory } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'rootDirectory',
|
||||
const rootDirectory = await client.input.text({
|
||||
message: `In which directory is your code located?`,
|
||||
transformer: (input: string) => {
|
||||
return `${chalk.dim(`./`)}${input}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import inquirer from 'inquirer';
|
||||
import { Separator } from '@inquirer/select';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import Client from '../client';
|
||||
import eraseLines from '../output/erase-lines';
|
||||
@@ -14,7 +14,7 @@ interface ListSeparator {
|
||||
separator: string;
|
||||
}
|
||||
|
||||
export type ListChoice = ListEntry | ListSeparator | typeof inquirer.Separator;
|
||||
export type ListChoice = ListEntry | ListSeparator | typeof Separator;
|
||||
|
||||
interface ListOptions {
|
||||
message: string;
|
||||
@@ -54,8 +54,6 @@ export default async function list(
|
||||
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
|
||||
}: ListOptions
|
||||
): Promise<string> {
|
||||
await import('./patch-inquirer');
|
||||
|
||||
let biggestLength = 0;
|
||||
let selected: string | undefined;
|
||||
|
||||
@@ -70,14 +68,14 @@ export default async function list(
|
||||
}
|
||||
|
||||
const choices = _choices.map(choice => {
|
||||
if (choice instanceof inquirer.Separator) {
|
||||
if (choice instanceof Separator) {
|
||||
return choice;
|
||||
}
|
||||
|
||||
if ('separator' in choice) {
|
||||
const prefix = `── ${choice.separator} `;
|
||||
const suffix = '─'.repeat(biggestLength - getLength(prefix));
|
||||
return new inquirer.Separator(`${prefix}${suffix}`);
|
||||
return new Separator(`${prefix}${suffix}`);
|
||||
}
|
||||
|
||||
if ('short' in choice) {
|
||||
@@ -93,11 +91,11 @@ export default async function list(
|
||||
|
||||
if (separator) {
|
||||
for (let i = 0; i < choices.length; i += 2) {
|
||||
choices.splice(i, 0, new inquirer.Separator(' '));
|
||||
choices.splice(i, 0, new Separator(' '));
|
||||
}
|
||||
}
|
||||
|
||||
const cancelSeparator = new inquirer.Separator('─'.repeat(biggestLength));
|
||||
const cancelSeparator = new Separator('─'.repeat(biggestLength));
|
||||
const _cancel = {
|
||||
name: 'Cancel',
|
||||
value: '',
|
||||
@@ -110,18 +108,16 @@ export default async function list(
|
||||
choices.push(cancelSeparator, _cancel);
|
||||
}
|
||||
|
||||
const answer = await client.prompt({
|
||||
name: 'value',
|
||||
type: 'list',
|
||||
default: selected,
|
||||
const answer = await client.input.select({
|
||||
message,
|
||||
choices,
|
||||
pageSize,
|
||||
default: selected,
|
||||
});
|
||||
|
||||
if (eraseFinalAnswer === true) {
|
||||
process.stdout.write(eraseLines(2));
|
||||
}
|
||||
|
||||
return answer.value;
|
||||
return answer;
|
||||
}
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import Prompt from 'inquirer/lib/prompts/base';
|
||||
import Choice from 'inquirer/lib/objects/choice';
|
||||
import Separator from 'inquirer/lib/objects/separator';
|
||||
|
||||
/**
|
||||
* Here we patch inquirer with some tweaks:
|
||||
* - update "list" to use ● and ○ and hide tips
|
||||
* - update "checkbox" to use ◻︎ and ◼︎ and hide tips
|
||||
* - use '?' before questions
|
||||
* - do not apply color to question's answer
|
||||
*/
|
||||
|
||||
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/base.js#L126
|
||||
const getQuestion = function (this: Prompt) {
|
||||
let message = `${chalk.gray('?')} ${this.opt.message} `;
|
||||
|
||||
if (this.opt.type === 'confirm') {
|
||||
if (this.opt.default === 'y/N') {
|
||||
message += `[y/${chalk.bold('N')}] `;
|
||||
} else {
|
||||
message += `[${chalk.bold('Y')}/n] `;
|
||||
}
|
||||
}
|
||||
|
||||
// Append the default if available, and if question isn't answered
|
||||
else if (this.opt.default != null && this.status !== 'answered') {
|
||||
message += chalk.dim(`(${this.opt.default}) `);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
inquirer.prompt.prompts.list.prototype.getQuestion = getQuestion;
|
||||
inquirer.prompt.prompts.checkbox.prototype.getQuestion = getQuestion;
|
||||
inquirer.prompt.prompts.input.prototype.getQuestion = getQuestion;
|
||||
inquirer.prompt.prompts.confirm.prototype.getQuestion = getQuestion;
|
||||
|
||||
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/list.js#L80
|
||||
inquirer.prompt.prompts.list.prototype.render = function () {
|
||||
// Render question
|
||||
let message = this.getQuestion();
|
||||
|
||||
// Render choices or answer depending on the state
|
||||
if (this.status === 'answered') {
|
||||
message += this.opt.choices.getChoice(this.selected).short;
|
||||
} else {
|
||||
let choicesStr = listRender(this.opt.choices, this.selected);
|
||||
let indexPosition = this.opt.choices.indexOf(
|
||||
this.opt.choices.getChoice(this.selected)
|
||||
);
|
||||
message +=
|
||||
'\n' +
|
||||
this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize);
|
||||
}
|
||||
|
||||
this.firstRender = false;
|
||||
|
||||
this.screen.render(message);
|
||||
};
|
||||
|
||||
function listRender(choices: (Choice | Separator)[], pointer: number) {
|
||||
let output = '';
|
||||
let separatorOffset = 0;
|
||||
|
||||
choices.forEach((choice, i) => {
|
||||
if (choice.type === 'separator') {
|
||||
separatorOffset++;
|
||||
output += ' ' + choice + '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.disabled) {
|
||||
separatorOffset++;
|
||||
output += ' - ' + choice.name;
|
||||
output +=
|
||||
' (' +
|
||||
(typeof choice.disabled === 'string' ? choice.disabled : 'Disabled') +
|
||||
')';
|
||||
output += '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
let isSelected = i - separatorOffset === pointer;
|
||||
let line = (isSelected ? '● ' : '○ ') + choice.name;
|
||||
|
||||
if (isSelected) {
|
||||
line = chalk.cyan(line);
|
||||
}
|
||||
output += line + ' \n';
|
||||
});
|
||||
|
||||
return output.replace(/\n$/, '');
|
||||
}
|
||||
|
||||
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/checkbox.js#L84
|
||||
inquirer.prompt.prompts.checkbox.prototype.render = function (error?: string) {
|
||||
// Render question
|
||||
let message = this.getQuestion();
|
||||
let bottomContent = '';
|
||||
|
||||
if (!this.spaceKeyPressed) {
|
||||
message +=
|
||||
'(Press ' +
|
||||
chalk.cyan.bold('<space>') +
|
||||
' to select, ' +
|
||||
chalk.cyan.bold('<a>') +
|
||||
' to toggle all, ' +
|
||||
chalk.cyan.bold('<i>') +
|
||||
' to invert selection)';
|
||||
}
|
||||
|
||||
// Render choices or answer depending on the state
|
||||
if (this.status === 'answered') {
|
||||
message += this.selection.length > 0 ? this.selection.join(', ') : 'None';
|
||||
} else {
|
||||
let choicesStr = renderChoices(this.opt.choices, this.pointer);
|
||||
let indexPosition = this.opt.choices.indexOf(
|
||||
this.opt.choices.getChoice(this.pointer)
|
||||
);
|
||||
message +=
|
||||
'\n' +
|
||||
this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
bottomContent = chalk.red('>> ') + error;
|
||||
}
|
||||
|
||||
this.screen.render(message, bottomContent);
|
||||
};
|
||||
|
||||
function renderChoices(choices: (Choice | Separator)[], pointer: number) {
|
||||
let output = '';
|
||||
let separatorOffset = 0;
|
||||
|
||||
choices.forEach(function (choice, i) {
|
||||
if (choice.type === 'separator') {
|
||||
separatorOffset++;
|
||||
output += '' + choice + '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.disabled) {
|
||||
separatorOffset++;
|
||||
output += '- ' + choice.name;
|
||||
output +=
|
||||
' (' +
|
||||
(typeof choice.disabled === 'string' ? choice.disabled : 'Disabled') +
|
||||
')';
|
||||
} else {
|
||||
if (i - separatorOffset === pointer) {
|
||||
output += chalk.cyan(
|
||||
(choice.checked ? '› ▪︎' : '› ▫︎') + ' ' + choice.name
|
||||
);
|
||||
} else {
|
||||
output += chalk.cyan(
|
||||
(choice.checked ? ' ▪︎' : ' ▫︎') + ' ' + choice.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
output += '\n';
|
||||
});
|
||||
|
||||
return output.replace(/\n$/, '');
|
||||
}
|
||||
|
||||
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/input.js#L44
|
||||
inquirer.prompt.prompts.input.prototype.render = function (error?: string) {
|
||||
let bottomContent = '';
|
||||
let appendContent = '';
|
||||
let message = this.getQuestion();
|
||||
let transformer = this.opt.transformer;
|
||||
let isFinal = this.status === 'answered';
|
||||
|
||||
if (isFinal) {
|
||||
appendContent = this.answer;
|
||||
} else {
|
||||
appendContent = this.rl.line;
|
||||
}
|
||||
|
||||
if (transformer) {
|
||||
message += transformer(appendContent, this.answers, { isFinal });
|
||||
} else {
|
||||
message += appendContent;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
bottomContent = chalk.red('>> ') + error;
|
||||
}
|
||||
|
||||
this.screen.render(message, bottomContent);
|
||||
};
|
||||
|
||||
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/confirm.js#L64
|
||||
inquirer.prompt.prompts.confirm.prototype.render = function (answer?: boolean) {
|
||||
let message = this.getQuestion();
|
||||
|
||||
if (this.status === 'answered') {
|
||||
message += answer ? 'y' : 'n';
|
||||
} else {
|
||||
message += this.rl.line;
|
||||
}
|
||||
|
||||
this.screen.render(message);
|
||||
|
||||
return this;
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import type { ReadableTTY, WritableTTY } from '@vercel-internals/types';
|
||||
|
||||
type Options = {
|
||||
abortSequences?: Set<string>;
|
||||
defaultValue?: boolean;
|
||||
noChar?: string;
|
||||
resolveChars?: Set<string>;
|
||||
stdin: ReadableTTY;
|
||||
stdout: WritableTTY;
|
||||
trailing?: string;
|
||||
yesChar?: string;
|
||||
};
|
||||
|
||||
export default async function promptBool(label: string, options: Options) {
|
||||
const {
|
||||
stdin,
|
||||
stdout,
|
||||
defaultValue = false,
|
||||
abortSequences = new Set(['\u0003']),
|
||||
resolveChars = new Set(['\r']),
|
||||
yesChar = 'y',
|
||||
noChar = 'n',
|
||||
trailing = '',
|
||||
} = options;
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const isRaw = Boolean(stdin && stdin.isRaw);
|
||||
|
||||
if (stdin) {
|
||||
if (stdin.setRawMode) {
|
||||
stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
stdin.resume();
|
||||
}
|
||||
|
||||
function restore() {
|
||||
stdout.write(trailing);
|
||||
|
||||
if (stdin) {
|
||||
if (stdin.setRawMode) {
|
||||
stdin.setRawMode(isRaw);
|
||||
}
|
||||
|
||||
stdin.pause();
|
||||
stdin.removeListener('data', onData);
|
||||
}
|
||||
}
|
||||
|
||||
function onData(buffer: Buffer) {
|
||||
const data = buffer.toString();
|
||||
if (data[0].toLowerCase() === yesChar) {
|
||||
restore();
|
||||
stdout.write(`\n`);
|
||||
resolve(true);
|
||||
} else if (data[0].toLowerCase() === noChar) {
|
||||
stdout.write(`\n`);
|
||||
restore();
|
||||
resolve(false);
|
||||
} else if (abortSequences.has(data)) {
|
||||
stdout.write(`\n`);
|
||||
restore();
|
||||
resolve(false);
|
||||
} else if (resolveChars.has(data[0])) {
|
||||
stdout.write(`\n`);
|
||||
restore();
|
||||
resolve(defaultValue);
|
||||
} else {
|
||||
// ignore extraneous input
|
||||
}
|
||||
}
|
||||
|
||||
const defaultText =
|
||||
defaultValue === null
|
||||
? `[${yesChar}|${noChar}]`
|
||||
: defaultValue
|
||||
? `[${chalk.bold(yesChar.toUpperCase())}|${noChar}]`
|
||||
: `[${yesChar}|${chalk.bold(noChar.toUpperCase())}]`;
|
||||
stdout.write(`${chalk.gray('>')} ${label} ${chalk.gray(defaultText)} `);
|
||||
|
||||
if (stdin) {
|
||||
stdin.on('data', onData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export default async function selectOrg(
|
||||
question: string,
|
||||
autoConfirm?: boolean
|
||||
): Promise<Org> {
|
||||
await import('./patch-inquirer');
|
||||
const {
|
||||
output,
|
||||
config: { currentTeam },
|
||||
@@ -52,14 +51,9 @@ export default async function selectOrg(
|
||||
return choices[defaultChoiceIndex].value;
|
||||
}
|
||||
|
||||
const answers = await client.prompt({
|
||||
type: 'list',
|
||||
name: 'org',
|
||||
return await client.input.select({
|
||||
message: question,
|
||||
choices,
|
||||
default: defaultChoiceIndex,
|
||||
default: choices[defaultChoiceIndex].value,
|
||||
});
|
||||
|
||||
const org = answers.org;
|
||||
return org;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Builder } from '@vercel/build-utils';
|
||||
|
||||
export function isZeroConfigBuild(builds?: Builder[]): boolean {
|
||||
return (
|
||||
!builds ||
|
||||
builds.length === 0 ||
|
||||
// If the zeroConfig property is set on all builds, still consider it as
|
||||
// zero config deployment
|
||||
builds.every(buildConfig => buildConfig.config?.zeroConfig)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { Separator } from '@inquirer/checkbox';
|
||||
import pluralize from 'pluralize';
|
||||
import { homedir } from 'os';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
@@ -22,6 +22,7 @@ import { detectProjects } from '../projects/detect-projects';
|
||||
import { repoInfoToUrl } from '../git/repo-info-to-url';
|
||||
import { connectGitProvider, parseRepoUrl } from '../git/connect-git-provider';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
import { isGitWorktreeOrSubmodule } from '../git-helpers';
|
||||
|
||||
const home = homedir();
|
||||
|
||||
@@ -144,16 +145,13 @@ export async function ensureRepoLink(
|
||||
if (yes) {
|
||||
remoteName = defaultRemote;
|
||||
} else {
|
||||
const answer = await client.prompt({
|
||||
type: 'list',
|
||||
name: 'value',
|
||||
remoteName = await client.input.select({
|
||||
message: 'Which Git remote should be used?',
|
||||
choices: remoteNames.map(name => {
|
||||
return { name: name, value: name };
|
||||
}),
|
||||
default: defaultRemote,
|
||||
});
|
||||
remoteName = answer.value;
|
||||
}
|
||||
}
|
||||
const repoUrl = remoteUrls[remoteName];
|
||||
@@ -222,15 +220,13 @@ export async function ensureRepoLink(
|
||||
selected = projects;
|
||||
} else {
|
||||
const addSeparators = projects.length > 0 && detectedProjectsCount > 0;
|
||||
const answer = await client.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
selected = await client.input.checkbox<Project | NewProject>({
|
||||
message: `Which Projects should be ${
|
||||
projects.length ? 'linked to' : 'created'
|
||||
}?`,
|
||||
choices: [
|
||||
...(addSeparators
|
||||
? [new inquirer.Separator('----- Existing Projects -----')]
|
||||
? [new Separator('----- Existing Projects -----')]
|
||||
: []),
|
||||
...projects.map(project => {
|
||||
return {
|
||||
@@ -240,7 +236,7 @@ export async function ensureRepoLink(
|
||||
};
|
||||
}),
|
||||
...(addSeparators
|
||||
? [new inquirer.Separator('----- New Projects to be created -----')]
|
||||
? [new Separator('----- New Projects to be created -----')]
|
||||
: []),
|
||||
...Array.from(detectedProjects.entries()).flatMap(
|
||||
([rootDirectory, frameworks]) =>
|
||||
@@ -264,12 +260,11 @@ export async function ensureRepoLink(
|
||||
},
|
||||
// Checked by default when there are no other existing Projects
|
||||
checked: projects.length === 0,
|
||||
};
|
||||
} as const;
|
||||
})
|
||||
),
|
||||
],
|
||||
});
|
||||
selected = answer.selected;
|
||||
}
|
||||
|
||||
if (selected.length === 0) {
|
||||
@@ -365,7 +360,13 @@ export async function findRepoRoot(
|
||||
): Promise<string | undefined> {
|
||||
const { debug } = client.output;
|
||||
const REPO_JSON_PATH = join(VERCEL_DIR, VERCEL_DIR_REPO);
|
||||
const GIT_CONFIG_PATH = normalize('.git/config');
|
||||
/**
|
||||
* If the current repo is a git submodule or git worktree '.git' is a file
|
||||
* with a pointer to the "parent" git repository instead of a directory.
|
||||
*/
|
||||
const GIT_PATH = isGitWorktreeOrSubmodule({ cwd: client.cwd })
|
||||
? normalize('.git')
|
||||
: normalize('.git/config');
|
||||
|
||||
for (const current of traverseUpDirectories({ start })) {
|
||||
if (current === home) {
|
||||
@@ -389,12 +390,12 @@ export async function findRepoRoot(
|
||||
|
||||
// if `.git/config` exists (unlinked),
|
||||
// then consider this the repo root
|
||||
const gitConfigPath = join(current, GIT_CONFIG_PATH);
|
||||
const gitConfigPath = join(current, GIT_PATH);
|
||||
stat = await lstat(gitConfigPath).catch(err => {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
});
|
||||
if (stat) {
|
||||
debug(`Found "${GIT_CONFIG_PATH}" - detected "${current}" as repo root`);
|
||||
debug(`Found "${GIT_PATH}" - detected "${current}" as repo root`);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { join, basename } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { remove } from 'fs-extra';
|
||||
import { ProjectLinkResult, ProjectSettings } from '@vercel-internals/types';
|
||||
import { isZeroConfigBuild } from '../is-zero-config-build';
|
||||
import {
|
||||
getLinkedProject,
|
||||
linkFolderToProject,
|
||||
@@ -161,7 +160,8 @@ export default async function setupAndLink(
|
||||
}
|
||||
|
||||
config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
const isZeroConfig = !localConfig || isZeroConfigBuild(localConfig.builds);
|
||||
const isZeroConfig =
|
||||
!localConfig || !localConfig.builds || localConfig.builds.length === 0;
|
||||
|
||||
try {
|
||||
let settings: ProjectSettings = {};
|
||||
|
||||
@@ -62,12 +62,7 @@ export async function readInput(
|
||||
|
||||
while (!input) {
|
||||
try {
|
||||
const { val } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'val',
|
||||
message,
|
||||
});
|
||||
input = val;
|
||||
input = await client.input.text({ message });
|
||||
} catch (err: any) {
|
||||
console.log(); // \n
|
||||
|
||||
|
||||
@@ -1,38 +1,52 @@
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
const printLine = (data: string[], sizes: number[]) =>
|
||||
data.reduce((line, col, i) => line + col.padEnd(sizes[i]), '');
|
||||
const defaultStyle = {
|
||||
'padding-left': 0,
|
||||
'padding-right': 2,
|
||||
};
|
||||
|
||||
export const noBorderChars = {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: '',
|
||||
};
|
||||
|
||||
const alignMap = {
|
||||
l: 'left',
|
||||
c: 'center',
|
||||
r: 'right',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Print a table.
|
||||
*/
|
||||
export default function table(
|
||||
fieldNames: string[] = [],
|
||||
data: string[][] = [],
|
||||
margins: number[] = [],
|
||||
print: (str: string) => void
|
||||
rows: string[][],
|
||||
opts?: { hsep?: number; align?: ('l' | 'c' | 'r')[] }
|
||||
) {
|
||||
// Compute size of each column
|
||||
const sizes = data
|
||||
.reduce(
|
||||
(acc, row) =>
|
||||
row.map((col, i) => {
|
||||
const currentMaxColSize = acc[i] || 0;
|
||||
const colSize = (col && col.length) || 0;
|
||||
return Math.max(currentMaxColSize, colSize);
|
||||
}),
|
||||
fieldNames.map(col => col.length)
|
||||
const table = new Table({
|
||||
style: {
|
||||
...defaultStyle,
|
||||
'padding-right': opts?.hsep ?? defaultStyle['padding-right'],
|
||||
},
|
||||
chars: noBorderChars,
|
||||
});
|
||||
table.push(
|
||||
...rows.map(row =>
|
||||
row.map((cell, i) => ({
|
||||
content: cell,
|
||||
hAlign: alignMap[opts?.align?.[i] ?? 'l'],
|
||||
}))
|
||||
)
|
||||
// Add margin to all columns except the last
|
||||
.map((size, i) => (i < margins.length && size + margins[i]) || size);
|
||||
|
||||
// Print header
|
||||
print(chalk.grey(printLine(fieldNames, sizes)));
|
||||
print('\n');
|
||||
|
||||
// Print content
|
||||
for (const row of data) {
|
||||
print(printLine(row, sizes));
|
||||
print('\n');
|
||||
}
|
||||
);
|
||||
return table.toString();
|
||||
}
|
||||
|
||||
@@ -97,16 +97,13 @@ async function getProjectLinkFromRepoLink(
|
||||
} else {
|
||||
const selectableProjects =
|
||||
projects.length > 0 ? projects : repoLink.repoConfig.projects;
|
||||
const { p } = await client.prompt({
|
||||
name: 'p',
|
||||
type: 'list',
|
||||
project = await client.input.select({
|
||||
message: `Please select a Project:`,
|
||||
choices: selectableProjects.map(p => ({
|
||||
value: p,
|
||||
name: p.name,
|
||||
})),
|
||||
});
|
||||
project = p;
|
||||
}
|
||||
if (project) {
|
||||
return {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
handler() {
|
||||
echo "Hello, from Bash!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default (req: Request) => {
|
||||
return new Response('Hello, from Deno!');
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "16.x"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/user.sh": {
|
||||
"runtime": "vercel-bash@3.0.8"
|
||||
"api/user.ts": {
|
||||
"runtime": "vercel-deno@3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { name } from '../backend/app'
|
||||
|
||||
export default async function handler(request, response) {
|
||||
return response.status(200).send(`Hello, ${name}!`);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const name = 'Batman'
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "vercel-ts-test",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
4
packages/cli/test/dev/fixtures/vercel-ts-test/yarn.lock
Normal file
4
packages/cli/test/dev/fixtures/vercel-ts-test/yarn.lock
Normal file
@@ -0,0 +1,4 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import execa from 'execa';
|
||||
import { isIP } from 'net';
|
||||
const { exec, fixture, testFixture, testFixtureStdio } = require('./utils.js');
|
||||
|
||||
@@ -125,13 +126,24 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] Use custom runtime from the "functions" property',
|
||||
testFixtureStdio('custom-runtime', async (testPath: any) => {
|
||||
await testPath(200, `/api/user`, /Hello, from Bash!/m);
|
||||
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
|
||||
})
|
||||
);
|
||||
test('[vercel dev] Use custom runtime from the "functions" property', async () => {
|
||||
const origPATH = process.env.PATH;
|
||||
try {
|
||||
// "deno" needs to be installed for this test
|
||||
await execa('curl -fsSL https://deno.land/install.sh | sh', {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
process.env.PATH = `${process.env.HOME}/.deno/bin:${origPATH}`;
|
||||
const tester = testFixtureStdio('custom-runtime', async (testPath: any) => {
|
||||
await testPath(200, `/api/user`, /Hello, from Deno!/m);
|
||||
await testPath(200, `/api/user.ts`, /Hello, from Deno!/m);
|
||||
});
|
||||
await tester();
|
||||
} finally {
|
||||
process.env.PATH = origPATH;
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'[vercel dev] Should work with nested `tsconfig.json` files',
|
||||
@@ -182,7 +194,7 @@ test(
|
||||
test(
|
||||
'[vercel dev] Should support `*.go` API serverless functions with `go.work` and lib',
|
||||
testFixtureStdio('go-work-with-shared', async (testPath: any) => {
|
||||
await testPath(200, `/api`, 'hello:go1.20.2');
|
||||
await testPath(200, `/api`, 'hello:go1.20.14');
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -544,4 +544,18 @@ describe('[vercel dev] ESM serverless functions', () => {
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] TypeScript importing another TS file, type=commonjs',
|
||||
testFixtureStdio(
|
||||
'vercel-ts-test',
|
||||
async (_testPath: any, port: number) => {
|
||||
const res = await fetch(`http://localhost:${port}/api/test`);
|
||||
validateResponseHeaders(res);
|
||||
const text = await res.text();
|
||||
expect(text).toEqual('Hello, Batman!');
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ const { satisfies } = require('semver');
|
||||
const stripAnsi = require('strip-ansi');
|
||||
const {
|
||||
fetchCachedToken,
|
||||
disableSSO,
|
||||
} = require('../../../../test/lib/deployment/now-deploy');
|
||||
const { spawnSync, execFileSync } = require('child_process');
|
||||
|
||||
@@ -399,9 +398,6 @@ function testFixtureStdio(
|
||||
// Expect the deploy succeeded with exit of 0;
|
||||
expect(deployResult.exitCode).toBe(0);
|
||||
deploymentUrl = new URL(deployResult.stdout).host;
|
||||
|
||||
// Disable the Project SSO Protection
|
||||
await disableSSO(deployResult.stdout, true);
|
||||
} finally {
|
||||
if (!hasGitignore) {
|
||||
await fs.remove(gitignore);
|
||||
|
||||
4
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/.vercel/project.json
vendored
Normal file
4
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/.vercel/project.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"orgId": "team_dummy",
|
||||
"projectId": "legacy-builds"
|
||||
}
|
||||
1
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/index.js
vendored
Normal file
1
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = (req, res) => res.end('Vercel');
|
||||
8
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/package.json
vendored
Normal file
8
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "node",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "18.x"
|
||||
}
|
||||
}
|
||||
5
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/vercel.json
vendored
Normal file
5
packages/cli/test/fixtures/unit/commands/deploy/legacy-builds/vercel.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"builds": [
|
||||
{ "src": "index.js", "use": "@vercel/node" }
|
||||
]
|
||||
}
|
||||
@@ -405,6 +405,9 @@ module.exports = async function prepare(session, binaryPath, tmpFixturesDir) {
|
||||
'project-sensitive-env-vars': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
'project-override-env-vars': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
'dev-proxy-headers-and-env': {
|
||||
'package.json': JSON.stringify({}),
|
||||
'server.js': `require('http').createServer((req, res) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import type { CLIProcess } from './types';
|
||||
|
||||
function getPromptErrorDetails(
|
||||
@@ -54,7 +55,7 @@ export default async function waitForPrompt(
|
||||
};
|
||||
|
||||
const onData = (rawChunk: Buffer) => {
|
||||
const chunk = rawChunk.toString();
|
||||
const chunk = stripAnsi(rawChunk.toString());
|
||||
|
||||
mostRecentChunk = chunk;
|
||||
console.log('> ' + chunk);
|
||||
|
||||
14
packages/cli/test/integration-1.test.ts
vendored
14
packages/cli/test/integration-1.test.ts
vendored
@@ -5,10 +5,7 @@ import fetch, { RequestInit } from 'node-fetch';
|
||||
import retry from 'async-retry';
|
||||
import fs from 'fs-extra';
|
||||
import sleep from '../src/util/sleep';
|
||||
import {
|
||||
disableSSO,
|
||||
fetchTokenWithRetry,
|
||||
} from '../../../test/lib/deployment/now-deploy';
|
||||
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
|
||||
import waitForPrompt from './helpers/wait-for-prompt';
|
||||
import { listTmpDirs } from './helpers/get-tmp-dir';
|
||||
import getGlobalDir from './helpers/get-global-dir';
|
||||
@@ -405,7 +402,6 @@ test('default command should work with --cwd option', async () => {
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
const url = stdout;
|
||||
await disableSSO(url, false);
|
||||
|
||||
const deploymentResult = await fetch(`${url}/README.md`);
|
||||
const body = await deploymentResult.text();
|
||||
@@ -435,7 +431,6 @@ test('should allow deploying a directory that was built with a target environmen
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
const url = stdout;
|
||||
await disableSSO(url, false);
|
||||
|
||||
const deploymentResult = await fetch(`${url}/README.md`);
|
||||
const body = await deploymentResult.text();
|
||||
@@ -463,7 +458,6 @@ test('should allow deploying a directory that was prebuilt, but has no builds.js
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
const url = stdout;
|
||||
await disableSSO(url, false);
|
||||
|
||||
const deploymentResult = await fetch(`${url}/README.md`);
|
||||
const body = await deploymentResult.text();
|
||||
@@ -527,7 +521,6 @@ test('deploy using only now.json with `redirects` defined', async () => {
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
const url = stdout;
|
||||
await disableSSO(url, false);
|
||||
const res = await fetch(`${url}/foo/bar`, { redirect: 'manual' });
|
||||
const location = res.headers.get('location');
|
||||
expect(location).toBe('https://example.com/foo/bar');
|
||||
@@ -549,7 +542,6 @@ test('deploy using --local-config flag v2', async () => {
|
||||
|
||||
const { host } = new URL(stdout);
|
||||
expect(host).toMatch(/secondary/gm);
|
||||
await disableSSO(host, false);
|
||||
|
||||
const testRes = await fetch(`https://${host}/test-${contextName}.html`);
|
||||
const testText = await testRes.text();
|
||||
@@ -585,7 +577,7 @@ test('deploy fails using --local-config flag with non-existent path', async () =
|
||||
expect(stderr).toMatch(/does-not-exist\.json/);
|
||||
});
|
||||
|
||||
test('deploy using --local-config flag above target', async () => {
|
||||
test('deploy from a nested directory', async () => {
|
||||
const root = await setupE2EFixture('zero-config-next-js-nested');
|
||||
const projectName = `project-link-dev-${
|
||||
Math.random().toString(36).split('.')[1]
|
||||
@@ -631,7 +623,6 @@ test('deploy using --local-config flag above target', async () => {
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
const { host } = new URL(stdout);
|
||||
await disableSSO(host, false);
|
||||
|
||||
const testRes = await fetch(`https://${host}/index.html`);
|
||||
const testText = await testRes.text();
|
||||
@@ -853,7 +844,6 @@ test('Deploy `api-env` fixture and test `vercel env` command', async () => {
|
||||
});
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
const { host } = new URL(stdout);
|
||||
await disableSSO(host, false);
|
||||
|
||||
const apiUrl = `https://${host}/api/get-env`;
|
||||
const apiRes = await fetch(apiUrl);
|
||||
|
||||
85
packages/cli/test/integration-2.test.ts
vendored
85
packages/cli/test/integration-2.test.ts
vendored
@@ -11,10 +11,7 @@ import fs, {
|
||||
mkdir,
|
||||
} from 'fs-extra';
|
||||
import sleep from '../src/util/sleep';
|
||||
import {
|
||||
disableSSO,
|
||||
fetchTokenWithRetry,
|
||||
} from '../../../test/lib/deployment/now-deploy';
|
||||
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
|
||||
import waitForPrompt from './helpers/wait-for-prompt';
|
||||
import { execCli } from './helpers/exec';
|
||||
import getGlobalDir from './helpers/get-global-dir';
|
||||
@@ -330,8 +327,7 @@ test('should show prompts to set up project during first deploy', async () => {
|
||||
'README.txt'
|
||||
).toBe(true);
|
||||
|
||||
const { host, href } = new URL(output.stdout);
|
||||
await disableSSO(host, false);
|
||||
const { href } = new URL(output.stdout);
|
||||
|
||||
// Send a test request to the deployment
|
||||
const response = await fetch(href);
|
||||
@@ -646,8 +642,7 @@ test('use `rootDirectory` from project when deploying', async () => {
|
||||
const secondResult = await execCli(binaryPath, [directory, '--public']);
|
||||
expect(secondResult.exitCode, formatOutput(secondResult)).toBe(0);
|
||||
|
||||
const { host, href } = new URL(secondResult.stdout);
|
||||
await disableSSO(host, false);
|
||||
const { href } = new URL(secondResult.stdout);
|
||||
|
||||
const pageResponse1 = await fetch(href);
|
||||
expect(pageResponse1.status).toBe(200);
|
||||
@@ -728,6 +723,72 @@ test('add a sensitive env var', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('override an existing env var', async () => {
|
||||
const dir = await setupE2EFixture('project-override-env-vars');
|
||||
const projectName = `project-override-env-vars-${
|
||||
Math.random().toString(36).split('.')[1]
|
||||
}`;
|
||||
|
||||
// remove previously linked project if it exists
|
||||
await remove(path.join(dir, '.vercel'));
|
||||
|
||||
const vc = execCli(binaryPath, ['link'], {
|
||||
cwd: dir,
|
||||
env: {
|
||||
FORCE_TTY: '1',
|
||||
},
|
||||
});
|
||||
|
||||
await setupProject(vc, projectName, {
|
||||
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
|
||||
outputDirectory: 'o',
|
||||
});
|
||||
|
||||
await vc;
|
||||
|
||||
const link = require(path.join(dir, '.vercel/project.json'));
|
||||
const options = {
|
||||
env: {
|
||||
VERCEL_ORG_ID: link.orgId,
|
||||
VERCEL_PROJECT_ID: link.projectId,
|
||||
},
|
||||
};
|
||||
|
||||
// 1. Initial add
|
||||
const addEnvCommand = execCli(
|
||||
binaryPath,
|
||||
['env', 'add', 'envVarName', 'production'],
|
||||
options
|
||||
);
|
||||
|
||||
await waitForPrompt(addEnvCommand, /What’s the value of [^?]+\?/);
|
||||
addEnvCommand.stdin?.write('test\n');
|
||||
|
||||
const output = await addEnvCommand;
|
||||
|
||||
expect(output.exitCode, formatOutput(output)).toBe(0);
|
||||
expect(output.stderr).toContain(
|
||||
'Added Environment Variable envVarName to Project'
|
||||
);
|
||||
|
||||
// 2. Override
|
||||
const overrideEnvCommand = execCli(
|
||||
binaryPath,
|
||||
['env', 'add', 'envVarName', 'production', '--force'],
|
||||
options
|
||||
);
|
||||
|
||||
await waitForPrompt(overrideEnvCommand, /What’s the value of [^?]+\?/);
|
||||
overrideEnvCommand.stdin?.write('test\n');
|
||||
|
||||
const outputOverride = await overrideEnvCommand;
|
||||
|
||||
expect(outputOverride.exitCode, formatOutput(outputOverride)).toBe(0);
|
||||
expect(outputOverride.stderr).toContain(
|
||||
'Overrode Environment Variable envVarName to Project'
|
||||
);
|
||||
});
|
||||
|
||||
test('whoami with `VERCEL_ORG_ID` should favor `--scope` and should error', async () => {
|
||||
if (!token) {
|
||||
throw new Error('Shared state "token" not set.');
|
||||
@@ -778,7 +839,6 @@ test('deploys with only now.json and README.md', async () => {
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
const { host } = new URL(stdout);
|
||||
await disableSSO(host, false);
|
||||
const res = await fetch(`https://${host}/README.md`);
|
||||
const text = await res.text();
|
||||
expect(text).toMatch(/readme contents/);
|
||||
@@ -801,7 +861,6 @@ test('deploys with only vercel.json and README.md', async () => {
|
||||
);
|
||||
|
||||
const { host } = new URL(stdout);
|
||||
await disableSSO(host, false);
|
||||
const res = await fetch(`https://${host}/README.md`);
|
||||
const text = await res.text();
|
||||
expect(text).toMatch(/readme contents/);
|
||||
@@ -887,7 +946,6 @@ test('deploy pnpm twice using pnp and symlink=false', async () => {
|
||||
'--public',
|
||||
'--yes',
|
||||
]);
|
||||
await disableSSO(res.stdout, false);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1344,8 +1402,6 @@ test('vercel.json configuration overrides in a new project prompt user and merge
|
||||
const deployment = await vc;
|
||||
expect(deployment.exitCode, formatOutput(deployment)).toBe(0);
|
||||
// assert the command were executed
|
||||
await disableSSO(deployment.stdout, false);
|
||||
|
||||
let page = await fetch(deployment.stdout);
|
||||
let text = await page.text();
|
||||
expect(text).toBe('1\n');
|
||||
@@ -1376,8 +1432,7 @@ test('vercel.json configuration overrides in an existing project do not prompt u
|
||||
// auto-confirm this deployment
|
||||
let deployment = await deploy(true);
|
||||
|
||||
const { host, href } = new URL(deployment.stdout);
|
||||
await disableSSO(host, false);
|
||||
const { href } = new URL(deployment.stdout);
|
||||
let page = await fetch(href);
|
||||
let text = await page.text();
|
||||
expect(text).toBe('0');
|
||||
|
||||
138
packages/cli/test/integration-3.test.ts
vendored
138
packages/cli/test/integration-3.test.ts
vendored
@@ -9,15 +9,12 @@ import { runNpmInstall } from '@vercel/build-utils';
|
||||
import { execCli } from './helpers/exec';
|
||||
import fetch, { RequestInit, RequestInfo } from 'node-fetch';
|
||||
import retry from 'async-retry';
|
||||
import fs, { ensureDir } from 'fs-extra';
|
||||
import fs from 'fs-extra';
|
||||
import { logo } from '../src/util/pkg-name';
|
||||
import sleep from '../src/util/sleep';
|
||||
import humanizePath from '../src/util/humanize-path';
|
||||
import pkg from '../package.json';
|
||||
import {
|
||||
disableSSO,
|
||||
fetchTokenWithRetry,
|
||||
} from '../../../test/lib/deployment/now-deploy';
|
||||
import { fetchTokenWithRetry } from '../../../test/lib/deployment/now-deploy';
|
||||
import waitForPrompt from './helpers/wait-for-prompt';
|
||||
import { getNewTmpDir, listTmpDirs } from './helpers/get-tmp-dir';
|
||||
import getGlobalDir from './helpers/get-global-dir';
|
||||
@@ -38,8 +35,6 @@ const deployHelpMessage = `${logo} vercel [options] <command | path>`;
|
||||
let session = 'temp-session';
|
||||
let secretName: string | undefined;
|
||||
|
||||
const createFile = (dest: fs.PathLike) => fs.closeSync(fs.openSync(dest, 'w'));
|
||||
|
||||
function fetchTokenInformation(token: string, retries = 3) {
|
||||
const url = `https://api.vercel.com/v2/user`;
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
@@ -314,7 +309,6 @@ test('should add secret with hyphen prefix', async () => {
|
||||
|
||||
expect(targetCall.exitCode, formatOutput(targetCall)).toBe(0);
|
||||
const { host } = new URL(targetCall.stdout);
|
||||
await disableSSO(host, false);
|
||||
const response = await fetch(`https://${host}`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.text()).toBe(`${value}\n`);
|
||||
@@ -343,7 +337,6 @@ test('ignore files specified in .nowignore', async () => {
|
||||
});
|
||||
|
||||
const { host } = new URL(targetCall.stdout);
|
||||
await disableSSO(host, false);
|
||||
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
|
||||
expect(ignoredFile.status).toBe(404);
|
||||
|
||||
@@ -360,7 +353,6 @@ test('ignore files specified in .nowignore via allowlist', async () => {
|
||||
});
|
||||
|
||||
const { host } = new URL(targetCall.stdout);
|
||||
await disableSSO(host, false);
|
||||
const ignoredFile = await fetch(`https://${host}/ignored.txt`);
|
||||
expect(ignoredFile.status).toBe(404);
|
||||
|
||||
@@ -377,7 +369,7 @@ test('list the scopes', async () => {
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
const include = new RegExp(`✔ ${contextName}\\s+${email}`);
|
||||
expect(stdout).toMatch(include);
|
||||
expect(stderr).toMatch(include);
|
||||
});
|
||||
|
||||
test('domains inspect', async () => {
|
||||
@@ -557,7 +549,6 @@ test('ensure we render a warning for deployments with no files', async () => {
|
||||
|
||||
// Ensure the exit code is right
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
await disableSSO(host, false);
|
||||
|
||||
// Send a test request to the deployment
|
||||
const res = await fetch(href);
|
||||
@@ -617,7 +608,7 @@ test('ensure we render a prompt when deploying home directory', async () => {
|
||||
binaryPath,
|
||||
[directory, '--public', '--name', session, '--force'],
|
||||
{
|
||||
input: 'N',
|
||||
input: 'N\n',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -625,7 +616,7 @@ test('ensure we render a prompt when deploying home directory', async () => {
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
expect(stderr).toContain(
|
||||
'You are deploying your home directory. Do you want to continue? [y/N]'
|
||||
'You are deploying your home directory. Do you want to continue?'
|
||||
);
|
||||
expect(stderr).toContain('Canceled');
|
||||
});
|
||||
@@ -879,118 +870,6 @@ test('initialize example "angular"', async () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('initialize example ("angular") to specified directory', async () => {
|
||||
const cwd = getNewTmpDir();
|
||||
const goal = '> Success! Initialized "angular" example in';
|
||||
|
||||
const { exitCode, stdout, stderr } = await execCli(
|
||||
binaryPath,
|
||||
['init', 'angular', 'ang'],
|
||||
{
|
||||
cwd,
|
||||
}
|
||||
);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
expect(stderr).toContain(goal);
|
||||
|
||||
expect(
|
||||
fs.existsSync(path.join(cwd, 'ang', 'package.json')),
|
||||
'package.json'
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(cwd, 'ang', 'tsconfig.json')),
|
||||
'tsconfig.json'
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(cwd, 'ang', 'angular.json')),
|
||||
'angular.json'
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('initialize example to existing directory with "-f"', async () => {
|
||||
const cwd = getNewTmpDir();
|
||||
const goal = '> Success! Initialized "angular" example in';
|
||||
|
||||
await ensureDir(path.join(cwd, 'angular'));
|
||||
createFile(path.join(cwd, 'angular', '.gitignore'));
|
||||
const { exitCode, stdout, stderr } = await execCli(
|
||||
binaryPath,
|
||||
['init', 'angular', '-f'],
|
||||
{
|
||||
cwd,
|
||||
}
|
||||
);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
expect(stderr).toContain(goal);
|
||||
|
||||
expect(
|
||||
fs.existsSync(path.join(cwd, 'angular', 'package.json')),
|
||||
'package.json'
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(cwd, 'angular', 'tsconfig.json')),
|
||||
'tsconfig.json'
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(cwd, 'angular', 'angular.json')),
|
||||
'angular.json'
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('try to initialize example to existing directory', async () => {
|
||||
const cwd = getNewTmpDir();
|
||||
const goal =
|
||||
'Error: Destination path "angular" already exists and is not an empty directory. You may use `--force` or `-f` to override it.';
|
||||
|
||||
await ensureDir(path.join(cwd, 'angular'));
|
||||
createFile(path.join(cwd, 'angular', '.gitignore'));
|
||||
const { exitCode, stdout, stderr } = await execCli(
|
||||
binaryPath,
|
||||
['init', 'angular'],
|
||||
{
|
||||
cwd,
|
||||
input: '\n',
|
||||
}
|
||||
);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
||||
expect(stderr).toContain(goal);
|
||||
});
|
||||
|
||||
test('try to initialize misspelled example (noce) in non-tty', async () => {
|
||||
const cwd = getNewTmpDir();
|
||||
const goal =
|
||||
'Error: No example found for noce, run `vercel init` to see the list of available examples.';
|
||||
|
||||
const { stdout, stderr, exitCode } = await execCli(
|
||||
binaryPath,
|
||||
['init', 'noce'],
|
||||
{ cwd }
|
||||
);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
||||
expect(stderr).toContain(goal);
|
||||
});
|
||||
|
||||
test('try to initialize example "example-404"', async () => {
|
||||
const cwd = getNewTmpDir();
|
||||
const goal =
|
||||
'Error: No example found for example-404, run `vercel init` to see the list of available examples.';
|
||||
|
||||
const { exitCode, stdout, stderr } = await execCli(
|
||||
binaryPath,
|
||||
['init', 'example-404'],
|
||||
{
|
||||
cwd,
|
||||
}
|
||||
);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(1);
|
||||
expect(stderr).toContain(goal);
|
||||
});
|
||||
|
||||
test('fail to add a domain without a project', async () => {
|
||||
const output = await execCli(binaryPath, [
|
||||
'domains',
|
||||
@@ -1021,7 +900,6 @@ test('try to revert a deployment and assign the automatic aliases', async () =>
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
await disableSSO(deploymentUrl, false);
|
||||
await waitForDeployment(deploymentUrl);
|
||||
await sleep(20000);
|
||||
|
||||
@@ -1036,7 +914,6 @@ test('try to revert a deployment and assign the automatic aliases', async () =>
|
||||
'--yes',
|
||||
]);
|
||||
const deploymentUrl = stdout;
|
||||
await disableSSO(deploymentUrl, false);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
@@ -1056,7 +933,6 @@ test('try to revert a deployment and assign the automatic aliases', async () =>
|
||||
'--yes',
|
||||
]);
|
||||
const deploymentUrl = stdout;
|
||||
await disableSSO(deploymentUrl, false);
|
||||
|
||||
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
|
||||
|
||||
@@ -1328,7 +1204,6 @@ test('deploy a Lambda with 128MB of memory', async () => {
|
||||
expect(output.exitCode, formatOutput(output)).toBe(0);
|
||||
|
||||
const { host: url } = new URL(output.stdout);
|
||||
await disableSSO(url, false);
|
||||
const response = await fetch('https://' + url + '/api/memory');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -1355,7 +1230,6 @@ test('deploy a Lambda with 3 seconds of maxDuration', async () => {
|
||||
expect(output.exitCode, formatOutput(output)).toBe(0);
|
||||
|
||||
const url = new URL(output.stdout);
|
||||
await disableSSO(url.host, false);
|
||||
|
||||
// Should time out
|
||||
url.pathname = '/api/wait-for/5';
|
||||
@@ -1394,7 +1268,6 @@ test('deploy a Lambda with a specific runtime', async () => {
|
||||
expect(output.exitCode, formatOutput(output)).toBe(0);
|
||||
|
||||
const url = new URL(output.stdout);
|
||||
await disableSSO(url.host, false);
|
||||
const res = await fetch(`${url}/api/test`);
|
||||
const text = await res.text();
|
||||
expect(text).toBe('Hello from PHP');
|
||||
@@ -1425,7 +1298,6 @@ test('use build-env', async () => {
|
||||
// Test if the output is really a URL
|
||||
const deploymentUrl = pickUrl(stdout);
|
||||
const { href } = new URL(deploymentUrl);
|
||||
await disableSSO(deploymentUrl, false);
|
||||
|
||||
await waitForDeployment(href);
|
||||
|
||||
|
||||
@@ -94,8 +94,6 @@ export class MockClient extends Client {
|
||||
this.stderr.pause();
|
||||
this.stderr.isTTY = true;
|
||||
|
||||
this._createPromptModule();
|
||||
|
||||
this.output = new Output(this.stderr);
|
||||
|
||||
this.argv = [];
|
||||
|
||||
BIN
packages/cli/test/mocks/example-list-tars/astro.tar.gz
Normal file
BIN
packages/cli/test/mocks/example-list-tars/astro.tar.gz
Normal file
Binary file not shown.
3
packages/cli/test/mocks/example-list-tars/readme.md
Normal file
3
packages/cli/test/mocks/example-list-tars/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
These mock the example-list server used during `init`. To make a new one, simply download the source in your browser.
|
||||
|
||||
Eg. visit `https://now-example-files.zeit.sh/v2/download/astro.tar.gz` and copy the download intro this directory.
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import type { Readable } from 'stream';
|
||||
import type { MatcherState } from '@vitest/expect';
|
||||
import type { MatcherHintOptions } from 'jest-matcher-utils';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
export async function toOutput(
|
||||
this: MatcherState,
|
||||
@@ -30,7 +31,7 @@ export async function toOutput(
|
||||
matcherHint(matcherName, 'stream', 'test', matcherHintOptions) + '\n\n';
|
||||
|
||||
function onData(data: string) {
|
||||
output += data;
|
||||
output += stripAnsi(data);
|
||||
if (output.includes(test)) {
|
||||
cleanup();
|
||||
resolve({
|
||||
|
||||
@@ -270,7 +270,7 @@ export function useProject(
|
||||
});
|
||||
}
|
||||
);
|
||||
client.scenario.get(`/v8/projects/${project.id}/env`, (req, res) => {
|
||||
client.scenario.get(`/v10/projects/${project.id}/env`, (req, res) => {
|
||||
const target: ProjectEnvTarget | undefined =
|
||||
typeof req.query.target === 'string'
|
||||
? parseEnvironment(req.query.target)
|
||||
@@ -291,14 +291,14 @@ export function useProject(
|
||||
|
||||
res.json({ envs: targetEnvs });
|
||||
});
|
||||
client.scenario.post(`/v8/projects/${project.id}/env`, (req, res) => {
|
||||
client.scenario.post(`/v10/projects/${project.id}/env`, (req, res) => {
|
||||
const envObj = req.body;
|
||||
envObj.id = envObj.key;
|
||||
envs.push(envObj);
|
||||
res.json({ envs });
|
||||
});
|
||||
client.scenario.delete(
|
||||
`/v8/projects/${project.id}/env/:envId`,
|
||||
`/v10/projects/${project.id}/env/:envId`,
|
||||
(req, res) => {
|
||||
const envId = req.params.envId;
|
||||
for (const [i, env] of envs.entries()) {
|
||||
|
||||
@@ -466,6 +466,54 @@ describe('deploy', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should send `projectSettings.nodeVersion` based on `engines.node` package.json field with `builds` in `vercel.json`', async () => {
|
||||
const user = useUser();
|
||||
useTeams('team_dummy');
|
||||
useProject({
|
||||
...defaultProject,
|
||||
name: 'legacy-builds',
|
||||
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_',
|
||||
});
|
||||
});
|
||||
client.scenario.get(`/v13/deployments/dpl_`, (req, res) => {
|
||||
res.json({
|
||||
creator: {
|
||||
uid: user.id,
|
||||
username: user.username,
|
||||
},
|
||||
id: 'dpl_',
|
||||
readyState: 'READY',
|
||||
aliasAssigned: true,
|
||||
alias: [],
|
||||
});
|
||||
});
|
||||
|
||||
const repoRoot = setupUnitFixture('commands/deploy/legacy-builds');
|
||||
client.cwd = repoRoot;
|
||||
client.setArgv('deploy');
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(0);
|
||||
expect(body).toMatchObject({
|
||||
source: 'cli',
|
||||
version: 2,
|
||||
projectSettings: {
|
||||
nodeVersion: '18.x',
|
||||
sourceFilesOutsideRootDirectory: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should send latest supported node version when given a >low-node-version based on `engines.node` package.json field', async () => {
|
||||
const user = useUser();
|
||||
useTeams('team_dummy');
|
||||
|
||||
@@ -527,7 +527,7 @@ describe('git', () => {
|
||||
`Found a repository in your local Git Config: https://github.com/user/repo`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Do you still want to connect https://github.com/user2/repo2? [y/N]`
|
||||
`Do you still want to connect https://github.com/user2/repo2? (y/N)`
|
||||
);
|
||||
client.stdin.write('y\n');
|
||||
await expect(client.stderr).toOutput(
|
||||
@@ -575,7 +575,7 @@ describe('git', () => {
|
||||
`Found multiple Git repositories in your local Git config:\n • origin: https://github.com/user/repo.git\n • secondary: https://github.com/user/repo2.git`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Do you still want to connect https://github.com/user3/repo3? [y/N]`
|
||||
`Do you still want to connect https://github.com/user3/repo3? (y/N)`
|
||||
);
|
||||
client.stdin.write('y\n');
|
||||
|
||||
|
||||
278
packages/cli/test/unit/commands/init.test.ts
Executable file
278
packages/cli/test/unit/commands/init.test.ts
Executable file
@@ -0,0 +1,278 @@
|
||||
import init from '../../../src/commands/init';
|
||||
import { client } from '../../mocks/client';
|
||||
import { setupTmpDir } from '../../helpers/setup-unit-fixture';
|
||||
import { FetchOptions } from '../../../src/util/client';
|
||||
import fs from 'fs-extra';
|
||||
import { Response } from 'node-fetch';
|
||||
import { join } from 'path';
|
||||
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
||||
import type { MockInstance } from 'vitest';
|
||||
|
||||
// path to mock tar
|
||||
const mockPath = join(
|
||||
process.cwd(),
|
||||
'test',
|
||||
'mocks',
|
||||
'example-list-tars',
|
||||
'astro.tar.gz'
|
||||
);
|
||||
|
||||
let mock: MockInstance<
|
||||
[url: string, opts?: FetchOptions | undefined],
|
||||
Promise<unknown>
|
||||
>;
|
||||
beforeEach(() => {
|
||||
// The examples list endpoint comes from an API that we don't typically mock
|
||||
mock = vi.spyOn(client, 'fetch').mockImplementation(async url => {
|
||||
const url2 = new URL(url);
|
||||
if (url2.pathname === '/v2/list.json') {
|
||||
return Promise.resolve([
|
||||
{ name: 'angular', visible: true, suggestions: [] },
|
||||
{ name: 'astro', visible: true, suggestions: [] },
|
||||
]);
|
||||
}
|
||||
if (url2.pathname === '/v2/download/astro.tar.gz') {
|
||||
return new Response(fs.createReadStream(mockPath), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fetch request for url ${url}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should allow selecting a framework to download the source into the expected folder', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
client.stdin.write('\x1B[B'); // Down arrow
|
||||
client.stdin.write('\r'); // Return key to select astro
|
||||
|
||||
await expect(client.stderr).toOutput(`Fetching astro`);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Success! Initialized "astro" example`
|
||||
);
|
||||
|
||||
const promiseResult = await exitCodePromise;
|
||||
expect(promiseResult).toEqual(0);
|
||||
const contents = await fs.readdirSync(join(cwd, 'astro'));
|
||||
expect(contents).toContain('package.json');
|
||||
});
|
||||
describe('when stdin is not a TTY', () => {
|
||||
it('should exit 0 with a helpful message when no framework argument is provided', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.stdin.isTTY = false;
|
||||
client.cwd = cwd;
|
||||
|
||||
client.setArgv('init');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(`No framework provided`);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
});
|
||||
it('should exit 1 with a helpful message when the framework isnt found', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.stdin.isTTY = false;
|
||||
client.cwd = cwd;
|
||||
|
||||
client.setArgv('init', 'astroz');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(`No example found`);
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
});
|
||||
describe('providing the framework argument', () => {
|
||||
it('should succeed', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
client.setArgv('init', 'astro');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(`Fetching astro`);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
||||
const promiseResult = await exitCodePromise;
|
||||
expect(promiseResult).toEqual(0);
|
||||
|
||||
const contents = await fs.readdirSync(join(cwd, 'astro'));
|
||||
expect(contents).toContain('package.json');
|
||||
expect(contents).toContain('astro.config.mjs');
|
||||
});
|
||||
it('should succeed when specifying a target directory', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
const targetDirectory = 'my-astro';
|
||||
|
||||
client.setArgv('init', 'astro', targetDirectory);
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(`Fetching astro`);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
||||
const promiseResult = await exitCodePromise;
|
||||
expect(promiseResult).toEqual(0);
|
||||
|
||||
const contents = await fs.readdirSync(join(cwd, targetDirectory));
|
||||
expect(contents).toContain('package.json');
|
||||
expect(contents).toContain('astro.config.mjs');
|
||||
});
|
||||
it('should fail when a file matching the framework already exists in the target location', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
// Create a file at the expected destination...
|
||||
await fs.outputFile(
|
||||
join(cwd, 'astro'),
|
||||
JSON.stringify({ name: 'some-package' })
|
||||
);
|
||||
|
||||
client.setArgv('init', 'astro');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Destination path "astro" already exists and is not a directory.`
|
||||
);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
it('should fail when a non-empty folder matching the framework already exists in the target location', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
// Create a folder with some content at the expected destination...
|
||||
await fs.outputFile(
|
||||
join(cwd, 'astro', 'package.json'),
|
||||
JSON.stringify({ name: 'some-package' })
|
||||
);
|
||||
|
||||
client.setArgv('init', 'astro');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Destination path "astro" already exists and is not an empty directory`
|
||||
);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
it('should succeed when an empty folder matching the framework already exists in the target location', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
await fs.mkdirSync(join(cwd, 'astro'));
|
||||
client.setArgv('init', 'astro');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(`Fetching astro`);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
||||
const promiseResult = await exitCodePromise;
|
||||
expect(promiseResult).toEqual(0);
|
||||
|
||||
const contents = await fs.readdirSync(join(cwd, 'astro'));
|
||||
expect(contents).toContain('package.json');
|
||||
});
|
||||
it("should fail when providing the framework argument which is so incorrect that it can't be guessed", async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
const frameworkName = 'some-unguessable-framework-name';
|
||||
client.setArgv('init', frameworkName);
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`No example found for ${frameworkName}, run \`vercel init\` to see the list of available examples.`
|
||||
);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
describe('using --force', () => {
|
||||
it('should fail when a file matching the framework already exists in the target location', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
// Create a file at the expected destination...
|
||||
await fs.outputFile(
|
||||
join(cwd, 'astro'),
|
||||
JSON.stringify({ name: 'some-package' })
|
||||
);
|
||||
|
||||
client.setArgv('init', 'astro', '--force');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Destination path "astro" already exists and is not a directory.`
|
||||
);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
await expect(exitCodePromise).resolves.toEqual(1);
|
||||
});
|
||||
it('should succeed when a non-empty folder matching the framework already exists in the target location', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
// Create a folder with some content at the expected destination...
|
||||
await fs.outputFile(
|
||||
join(cwd, 'astro', 'package.json'),
|
||||
JSON.stringify({ name: 'some-package' })
|
||||
);
|
||||
|
||||
client.stderr.pipe(process.stderr);
|
||||
client.setArgv('init', 'astro', '--force');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Success! Initialized "astro" example`
|
||||
);
|
||||
|
||||
const promiseResult = await exitCodePromise;
|
||||
expect(promiseResult).toEqual(0);
|
||||
|
||||
const contents = await fs.readdirSync(join(cwd, 'astro'));
|
||||
expect(contents).toContain('package.json');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('triggering the guess prompt with by incorrectly spelling "astroz"', () => {
|
||||
it('should succeed when accepting the suggestion', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
client.setArgv('init', 'astroz');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput('? Did you mean astro? (y/N)');
|
||||
client.stdin.write('y');
|
||||
client.stdin.write('\r'); // Return key
|
||||
|
||||
await expect(client.stderr).toOutput(`Fetching astro`);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
||||
const promiseResult = await exitCodePromise;
|
||||
expect(promiseResult).toEqual(0);
|
||||
|
||||
const contents = await fs.readdirSync(join(cwd, 'astro'));
|
||||
expect(contents).toContain('package.json');
|
||||
});
|
||||
it('should fail when rejecting the suggestion', async () => {
|
||||
const cwd = setupTmpDir();
|
||||
client.cwd = cwd;
|
||||
|
||||
client.setArgv('init', 'astroz');
|
||||
const exitCodePromise = init(client);
|
||||
|
||||
await expect(client.stderr).toOutput('? Did you mean astro? (y/N)');
|
||||
|
||||
client.stdin.write('\r'); // Return key
|
||||
await expect(client.stderr).toOutput(`> No changes made`);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import { defaultProject, useProject } from '../../mocks/project';
|
||||
import { client } from '../../mocks/client';
|
||||
import type { Project } from '@vercel-internals/types';
|
||||
import { parseSpacedTableRow } from '../../helpers/parse-table';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('project', () => {
|
||||
describe('list', () => {
|
||||
@@ -47,55 +46,6 @@ describe('project', () => {
|
||||
expect(data).toEqual([project.project.name, 'https://foobar.com']);
|
||||
});
|
||||
|
||||
it('should list projects running on an soon-to-be-deprecated Node.js version', async () => {
|
||||
vi.useFakeTimers().setSystemTime(new Date('2023-12-08'));
|
||||
|
||||
const user = useUser();
|
||||
useTeams('team_dummy');
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
nodeVersion: '16.x',
|
||||
});
|
||||
|
||||
client.setArgv('project', 'ls', '--update-required');
|
||||
await projects(client);
|
||||
|
||||
const lines = createLineIterator(client.stderr);
|
||||
|
||||
let line = await lines.next();
|
||||
expect(line.value).toEqual(`Fetching projects in ${user.username}`);
|
||||
|
||||
line = await lines.next();
|
||||
expect(line.value).toEqual(
|
||||
'WARN! The following Node.js versions will be deprecated soon: 16.x. Please upgrade your projects immediately.'
|
||||
);
|
||||
line = await lines.next();
|
||||
expect(line.value).toEqual(
|
||||
'> For more information visit: https://vercel.com/docs/functions/serverless-functions/runtimes/node-js#node.js-version'
|
||||
);
|
||||
line = await lines.next();
|
||||
expect(line.value).toContain(user.username);
|
||||
|
||||
// empty line
|
||||
line = await lines.next();
|
||||
expect(line.value).toEqual('');
|
||||
|
||||
line = await lines.next();
|
||||
const header = parseSpacedTableRow(line.value!);
|
||||
expect(header).toEqual([
|
||||
'Project Name',
|
||||
'Latest Production URL',
|
||||
'Updated',
|
||||
]);
|
||||
|
||||
line = await lines.next();
|
||||
const data = parseSpacedTableRow(line.value!);
|
||||
data.pop();
|
||||
expect(data).toEqual([project.project.name, 'https://foobar.com']);
|
||||
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
it('should list projects when there is no production deployment', async () => {
|
||||
const user = useUser();
|
||||
useTeams('team_dummy');
|
||||
|
||||
@@ -115,7 +115,9 @@ describe('promote', () => {
|
||||
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
'? This deployment does not target production, therefore promotion will not apply\n production environment variables. Are you sure you want to continue?'
|
||||
'? This deployment is not a production deployment and cannot be directly \n' +
|
||||
'promoted. A new deployment will be built using your production environment. Are \n' +
|
||||
'you sure you want to continue? (y/N)'
|
||||
);
|
||||
|
||||
// say "no" to the prompt
|
||||
@@ -138,7 +140,9 @@ describe('promote', () => {
|
||||
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
'? This deployment does not target production, therefore promotion will not apply\n production environment variables. Are you sure you want to continue?'
|
||||
'? This deployment is not a production deployment and cannot be directly \n' +
|
||||
'promoted. A new deployment will be built using your production environment. Are \n' +
|
||||
'you sure you want to continue? (y/N)'
|
||||
);
|
||||
|
||||
// say "yes" to the prompt
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('teams', () => {
|
||||
const user = useUser();
|
||||
useTeams(undefined, { apiVersion: 2 });
|
||||
const exitCodePromise = teamsList(client);
|
||||
await expect(client.stdout).toOutput(user.username);
|
||||
await expect(client.stderr).toOutput(user.username);
|
||||
await expect(exitCodePromise).resolves.toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user