Compare commits

...

52 Commits

Author SHA1 Message Date
Sean Massa
4773ff5efd Publish Stable
- @vercel/build-utils@5.0.5
 - vercel@27.3.0
 - @vercel/client@12.1.4
 - @vercel/go@2.0.9
 - @vercel/hydrogen@0.0.6
 - @vercel/next@3.1.9
 - @vercel/node@2.5.0
 - @vercel/python@3.1.1
 - @vercel/redwood@1.0.10
 - @vercel/remix@1.0.11
 - @vercel/ruby@1.3.17
 - @vercel/static-build@1.0.10
2022-07-27 13:33:34 -05:00
Gal Schlezinger
d8c7308eb6 [node] Add WebAssembly importing support for the @vercel/node builder (#8204)
* [node] Add WebAssembly importing support for the @vercel/node builder

* add comment about the original source

* trim down values to only what is required for esbuild wrapping logic

* Revert "trim down values to only what is required for esbuild wrapping logic"

This reverts commit c04dacad99f25156938dccdf2f29aac6e8282564.

Co-authored-by: Sean Massa <EndangeredMassa@gmail.com>
2022-07-27 13:17:32 -05:00
Steven
5df1c89138 [tests] Fix next runtime test (#8258)
This test fails with `vc build`
2022-07-27 02:51:43 +00:00
Ethan Arrowood
f5d879143c [static-build] Add .vercel to static-build ignore list regardless of config (#8255)
### Related Issues

Adds `.vercel` path to the ignore list regardless if `zeroConfig` is enabled. This fixes a bug where the `.vercel` folder was being copied into the resulting `.vercel/output/static` directory after running `vc build` with `distDir: "."` configured for static-build.

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-26 22:04:25 +00:00
Matthew Stanciu
9a55809515 [cli] Minor vc inspect visual updates (#8160)
Featuring:

- Aliases are now shown
- `readyState` –> `status` in "General" section
- `createdAt` –> `created` in "General" section
- Update `stateString` to support more states & be more visually appealing

### Before

<img width="754" alt="Screen Shot 2022-07-14 at 3 10 40 PM" src="https://user-images.githubusercontent.com/14811170/179097164-df7acd8a-d677-4e75-b7d5-b8ec6cf9bb12.png">

### After

<img width="748" alt="Screen Shot 2022-07-14 at 3 10 29 PM" src="https://user-images.githubusercontent.com/14811170/179097170-137d8977-60d6-402d-825e-8b0fb3025969.png">

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-26 18:15:38 +00:00
Nathan Rajlich
56adf15823 [all] Update @vercel/ncc to v0.34.0 (#8248)
The version of `@vercel/ncc` that was being used in this repo is rather old. Let's update to the latest version. Specifically, this is a baby step towards allowing CLI to become ESM format.
2022-07-26 08:24:01 +00:00
Steven
1acab3d06c [tests] Update node tests to run concurrently (#8247)
This applies the same chunking algorithm from `@vercel/static-build` tests to `@vercel/node` tests that we can run up to 5 integration tests concurrently.
2022-07-26 00:06:59 +00:00
Nathan Rajlich
081b38466b [cli] Update to TypeScript v4.7.4 (#8232) 2022-07-25 16:08:14 -07:00
Nathan Rajlich
c397fd1856 [cli] Apply functions configuration to Serverless Functions in "vc build" (#8243) 2022-07-25 23:04:49 +00:00
Steven
afd303b94a [tests] Fix turbo cache for test changes (#8246)
When we make changes in `test/lib/**`, it should run all the E2E tests again for every package.

https://turborepo.org/docs/reference/configuration
2022-07-25 17:14:33 -04:00
Ethan Arrowood
b12387034a [cli] fix zero config resolution in vc build (#8244)
### Related Issues

Adds a `isZeroConfig` check to static-build so that when `"zeroConfig": true` is enabled the correct settings are resolved.

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-25 19:27:12 +00:00
Steven
5af65d5a24 Revert "[python] support Sanic >=21 and python >= 3.10" (#8241)
- Reverts vercel/vercel#8045
- Fixes vercel/vercel#8231
2022-07-25 17:40:21 +00:00
Steven
1ee9a96a62 [build-utils] Fix package.json and lockfile detection (#8230)
This PR fixes a couple issues where `vercel build` was not correctly detecting the package.json files

```
Error: @vercel/node:test: ERROR: frontend/index.ts(1,12): error TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
```

It also fixes an issue where all deployments were incorrectly detecting the lock file because the lock file doesn't always live in the same directory as the package.json file. So we need to do 2 passes: one to find the nearest package.json and one to find the nearest lock file.
2022-07-25 16:29:24 +00:00
Nathan Rajlich
76130faf26 [cli] Do not load .env in "vercel build" command (#8228)
Only consider the `.vercel/.env.${target}.local` file when loading env vars at the beginning of `vercel build`.

If a project-level `.env` (or `.env.production` or whatever) file needs to be loaded then it is the responsibility of the frontend framework's build command to consider those files (i.e. CRA build, `next build`, etc.)
2022-07-24 05:08:13 +00:00
Nathan Rajlich
fb3601d178 [cli] Add "dev" script, remove "build-dev" script (#8222)
I don't think anyone is using this command anymore in the world of M1.

Anyways, we should be using `ts-node` to execute the CLI from source code instead of skipping steps at build-time to iterate quickly.
2022-07-22 22:39:10 +00:00
Steven
aebfb6812d [cli][build-utils] Fix dependencies for boxen (#8210)
The `boxen` package was incorrectly listed as a dependency of build-utils so I moved it to cli, which is only used by the [`vercel bisect`](3d3774ee7e/packages/cli/src/commands/bisect/index.ts (L2)) command.
2022-07-22 22:08:51 +00:00
Matthew Stanciu
73999e7253 [cli] Log delta in vc env pull (#8170)
This PR adds a new message to the end of `vc env pull` which shows delta of what was added, changed, and removed.

Example: you have a local `.env` file in a directory linked to a Vercel project. You change `TEST_1`, remove `TEST_2` via `vc env rm TEST_2`, and add `TEST_3` via `vc env add`. Then you run `vc env pull`. Here's what you will see:

```
> Updating existing .env file
> Downloading `development` Environment Variables for Project name
  Updated .env file

+ TEST_3
~ TEST_1
- TEST_2
```

<img width="562" alt="Screen Shot 2022-07-20 at 11 09 56 AM" src="https://user-images.githubusercontent.com/14811170/180064534-2ff5facb-95cb-4712-aaad-cbb47490cebe.png">

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-22 19:16:56 +00:00
Nathan Rajlich
989dad5570 [cli] Use ts-node with SWC instead of ts-eager (#8224)
`ts-eager` isn't really maintained anymore and doesn't support ESM packages.

`ts-node` with SWC is more or less just as fast, so let's use that instead since it's well maintained any we can dogfood SWC more.
2022-07-22 17:53:15 +00:00
Nathan Rajlich
68c2dea601 [cli] Add support for Vercel Analytics in "vercel build" command (#8220)
Pull the Vercel Analytics ID in the `vercel pull` command, so that `vercel build` can set the `VERCEL_ANALYTICS_ID` environment variable at build-time for the frontend framework to utilize.
2022-07-22 17:23:29 +00:00
JJ Kasper
63f2da2f68 [next] Update size limit test (#8226)
This test is failing due to a decrease in the server layer so it is no longer over the limit as expected so this adds some more data to the specific route to ensure it does hit the limit. 

Fixes: https://github.com/vercel/vercel/runs/7469021922?check_suite_focus=true#step:9:6800
2022-07-22 11:05:53 -04:00
57e5f81361 [docs] Fix CONTRIBUTING file local development guideline, change dir after git clone (#8225)
Update CONTRIBUTING.md
2022-07-22 09:07:29 -04:00
Mosaad
fd5e440533 [next] Fix error message typo (#8218)
User the correct indefinite articles.

Co-authored-by: Steven <steven@ceriously.com>
2022-07-22 08:44:48 -04:00
Sean Massa
2a45805b26 [tests] increase timeout for test that's failing on windows (#8221)
This test sometimes times out on Windows. When it succeeds, it takes ~6 seconds. When it fails, we don't know how long it would have taken, but the current timeout is 7 seconds. Let's try 10 seconds and see if that's better.
2022-07-22 08:52:28 +00:00
Nathan Rajlich
5523383e50 [cli] Prevent vc deploy --prebuilt when vc build failed (#8194)
If `vc build` failed then don't allow a deployment to be created with `vc deploy --prebuilt`.
2022-07-22 07:31:52 +00:00
Sean Massa
0ecbb24cab [utils] allow a github PAT token to get past rate limit (#8209)
If you run `yarn changelog` when your IP is already rate limited by github, you'll get an error. This allows you to set a [Github PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) to get past the generic rate limit.

Given a PAT with value `MY_PAT_TOKEN`, you can run:

```
$ GITHUB_TOKEN=MY_PAT_TOKEN yarn changelog
```

If you do get an error, it now actually shows up in the output:

```
$  yarn changelog
yarn run v1.22.18
$ node utils/changelog.js
Error: Failed to fetch releases from github: API rate limit exceeded for 98.139.180.149. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)
```
2022-07-22 06:14:59 +00:00
Nathan Rajlich
922223bd19 [cli] Lazy-load "metrics" object to fix jest reporting open handle (#7866)
Jest was saying that there's an open handle in the unit tests due to the `crypto.pbkdf2Sync()` call in this metrics file, so lazy-load that object in the case when metrics should not be collected.
2022-07-22 04:02:26 +00:00
JJ Kasper
0ad7fd34f4 [next][node][redwood][remix] Update @vercel/nft to 0.21.0 (#8208)
### Related Issues

Updates to the latest version of `@vercel/nft` which adds fs concurrency limits to help alleviate memory usage. 

x-ref: https://github.com/vercel/nft/pull/301

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-22 00:45:53 +00:00
Sean Massa
3d3774ee7e Publish Stable
- @vercel/build-utils@5.0.4
 - vercel@27.2.0
 - @vercel/client@12.1.3
 - @vercel/go@2.0.8
 - @vercel/hydrogen@0.0.5
 - @vercel/next@3.1.8
 - @vercel/node@2.4.5
 - @vercel/python@3.1.0
 - @vercel/redwood@1.0.9
 - @vercel/remix@1.0.10
 - @vercel/ruby@1.3.16
 - @vercel/static-build@1.0.9
2022-07-21 15:03:11 -05:00
Sean Massa
50f8eec7cb [cli][dev] support environment variables in edge functions during vc dev (#8207)
Edge Function support in `vc dev` was not passing through the environment variables, which is supported by [production Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions/edge-functions-api#environment-variables).

This PR passes those through. I updated a test for it and manually tested on a separate project.
2022-07-21 17:53:52 +00:00
Sean Massa
45374e2f90 [cli] improve isBundledBuilder logic (#8086)
The logic that determines if a builder needs to be installed has a check to see if the builder should already be bundled: `isBundledBuilder`. This was looking for specific conditions that made it (1) a bit hard to follow and (2) very sensitive to `canary` tags (and having "canary" in the version specifier).

This causes general development problems because local changes weren't always used by local CLI builds. Depedendant packages (like `@vercel/node`) would be installed from the latest `canary` release instead.

This caused problems in CI and released CLI versions where the latest `canary` of dependant packages might be rather old, causing that old code to be used instead of the latest non-canary releases.

The issue was mitigated for now by publishing canary releases for all packages.

---

Paired with @styfle @MatthewStanciu.

@TooTallNate: Is this change too broad? Are there cases where we wouldn't want to do this?
2022-07-21 15:45:29 +00:00
Steven
fd9142b6f3 [cli] Bump @vercel/fun to 1.0.4 (#8198)
Bump `@vercel/fun` to [1.0.4](https://github.com/vercel/fun/releases/tag/1.0.4)
2022-07-21 00:00:03 +00:00
JJ Kasper
8cf67b549b [next] Ensure manifests are specific to the included pages (#8172)
### Related Issues

This updates to filter the `routes-manifest` and `pages-manifest` to only include entries for the pages that are being included in the specific serverless function. This fixes the case where multiple dynamic routes could match a path but one is configured with `fallback: false` so shouldn't match when executing for a different dynamic route. 

A regression test for this specific scenario has been added in the `00-mixed-dynamic-routes` fixture. 

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-20 23:06:44 +00:00
Sean Massa
5dc6f48e44 [cli][dev] handle no response from edge functions (#8095)
When an edge function has no response during `vc dev`, we were seeing an unhelpful error message:

> The event listener did not respond.

Now, we'll see a much more specific error message:

> Unhandled rejection: Edge Function "api/edge-no-response.ts" did not return a response.
> Error! Failed to complete request to /api/edge-no-response: Error: socket hang up
2022-07-20 20:09:29 +00:00
Kevin Tan
66c8544e8f [python] support Sanic >=21 and python >= 3.10 (#8045)
### Related Issues

1. exception from python 3.10:
```
TypeError: As of 3.10, the *loop* parameter was removed from Queue() since it is no longer necessary`
```

Remove the deprecated argument `loop` from `Queue`, which can also be omitted in python version < 3.10

2. exception from Sanic > 21.3:
```
File "C:\Users\Kevin\AppData\Local\Temp\zeit-fun-03f18b2d2c7d7\sanic\signals.py", line 93, in get
    group, param_basket = self.find_route(
TypeError: 'NoneType' object is not callable
```
As of Sanic > 21.3, it cannot serve requests immediately after initializing, instead, we need implement the [ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) and wait for the startup event completed.  

here I complemented the protocol copied from (same source of the previous HTTP procotol): <https://github.com/jordaneremieff/mangum/blob/main/mangum/protocols/lifespan.py>


### Related link:
https://github.com/encode/uvicorn/pull/498

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR


Co-authored-by: Steven <steven@ceriously.com>
2022-07-20 09:50:53 -04:00
Matthew Stanciu
0140db38fa [cli] Support multiple remote URLs in Git config (#8145)
Two features that handle a user's local Git config have been shipped:

- #8100 
- #7910 

Both of these features currently pull only from the user's remote origin URL. This covers 90% of cases, but there are cases in which the user has more than one remote URL, and may want to use one other than the origin URL, especially in `vc git connect`. This PR:

- Adds support for multiple remote URLs in a user's Git config
- Updates `vc git connect` to prompt the user to select a URL if they have multiple remote URLs
- Updates `createGitMeta` to send the connected Git repository url by default, origin url otherwise

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-20 07:17:53 +00:00
Chris Barber
e5421c27e8 [cli][client][build-utils][node][static-build] updated node-fetch to fix high severity security vulnerability (#8180)
Update `node-fetch 2.6.1 -> 2.6.7` to fix high severity security vulnerability: Exposure of Sensitive Information to an Unauthorized Actor (https://github.com/advisories/GHSA-r683-j2x4-v87g).

`node-fetch` was updated in the root, `api`, `build-utils`, `cli`, `client`, `node`, and `static-build`.

### Related Issues

> https://linear.app/vercel/issue/VCCLI-196/update-vercelnode-dep-node-fetch-261-267

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-19 22:15:09 +00:00
Matthew Stanciu
5afc527233 [cli] Add --environment flag to vc env pull (#8162)
Right now, `vc env pull` only pulls development environment variables. This PR adds a new flag, `--environment,` which allows users to specify which environment to pull from.

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-19 20:02:13 +00:00
Nathan Rajlich
de9518b010 [cli] Write top-level error to builds.json file in "vc build" (#8193)
If an error happens outside of a Builder (i.e. `detectBuilders()` function fails), then write the serialized error into the `builds.json` file at the top-level of the file (there is no `builds[]` when an error happens at the top-level).
2022-07-19 19:30:17 +00:00
Nathan Rajlich
c322d1dbba [cli] Set ignoreBuildScript: true option in "vc build" command (#8184)
This matches the behavior in production and prevents the error:

```
> Error! Your `package.json` file is missing a `build` property inside the `scripts` property.
```
2022-07-19 18:52:33 +00:00
Steven
18c19ead76 [tests] Upgrade python tests to 3.9 (#8181)
New python 3.6 deployments will fail today per the previous announcement https://vercel.com/changelog/python-3-6-is-being-deprecated

This PR updates the tests to use python 3.9 instead.
2022-07-19 17:43:20 +00:00
Steven
9d80c27382 [cli] Print full error when unknown/unexpected (#8059)
If we don’t know the error, we should not assume it has a stack prop and instead print the whole thing to avoid accidentally printing `undefined`. 

- related to https://github.com/vercel/vercel/discussions/8043
2022-07-18 10:40:09 -04:00
Steven
bef1aec766 Publish Stable
- @vercel/build-utils@5.0.3
 - vercel@27.1.5
 - @vercel/client@12.1.2
 - @vercel/frameworks@1.1.1
 - @vercel/fs-detectors@2.0.1
 - @vercel/go@2.0.7
 - @vercel/hydrogen@0.0.4
 - @vercel/next@3.1.7
 - @vercel/node@2.4.4
 - @vercel/python@3.0.7
 - @vercel/redwood@1.0.8
 - @vercel/remix@1.0.9
 - @vercel/routing-utils@2.0.0
 - @vercel/ruby@1.3.15
 - @vercel/static-build@1.0.8
2022-07-15 15:40:37 -04:00
Steven
4f4a42813f [build-utils][node][python][ruby] Update error message for EOL runtimes (#8167)
This PR updates the error message when the runtime version detected is EOL
2022-07-15 15:38:38 -04:00
Steven
181a492d91 [routing-utils] MAJOR refactor getTransformedRoutes and types (#8155)
This is a semver major change to the public API for `@vercel/routing-utils` which includes the following breaking changes.

1. `getTransformedRoutes({ nowConfig })` props changed to `getTransformedRoutes(nowConfig)`
2. `type Source` renamed `type RouteWithSrc`
3. `type Handler` renamed `type RouteWithHandle`
4. `interface VercelConfig` removed
5. `type NowConfig` removed
6. `type NowRewrite` removed
7. `type NowRedirect` removed
8. `type NowHeader` removed
9. `type NowHeaderKeyValue` removed
2022-07-15 14:05:08 -04:00
Sean Massa
1be7a80bb8 Publish Stable
- vercel@27.1.4
 - @vercel/next@3.1.6
2022-07-15 11:20:56 -05:00
Sean Massa
0428d4744e [cli] write config.json when exiting because of error in builder (#8163)
Co-authored-by: Steven <steven@ceriously.com>
2022-07-15 11:16:31 -05:00
JJ Kasper
2a929a4bb9 [next] Update allowQuery for prerendered paths (#8158)
This updates our `allowQuery` generating to ignore all query values for build-time prerender paths as these will match before dynamic routes since they are filesystem routes and the query values will not be overridden properly like they are for fallback prerender paths. This also adds testing for both prerender path types with on-demand ISR to ensure the cache is updated as expected regardless of the query.  

Deployment with patch can be seen here https://nextjs-issue-odr-simple-hrjt2dagm-ijjk-testing.vercel.app/

### Related Issues

x-ref: https://github.com/vercel/next.js/issues/38306
x-ref: https://github.com/vercel/next.js/issues/38653
2022-07-15 11:37:05 -04:00
JJ Kasper
accd308dc5 [tests] Update log for update-canary-tag script (#8156)
### Related Issues

Noticed this log was not being converted to a string so we're losing some context so this corrects in case we have a failure on this step in the future. 

Fixes: https://github.com/vercel/vercel/runs/7344626478?check_suite_focus=true#step:8:258

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-15 02:16:51 +00:00
Sean Massa
e2d4efab08 Publish Stable
- vercel@27.1.3
2022-07-14 12:04:05 -05:00
JJ Kasper
7e0dd6f808 [tests] Update to latest version of turbo (#8152)
### Related Issues

Updates to latest turbo which includes patches for cached files. 

x-ref: [slack thread](https://vercel.slack.com/archives/C02CDC2ALJH/p1657767763630359?thread_ts=1657757803.039099&cid=C02CDC2ALJH)

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-14 12:41:28 +00:00
Nathan Rajlich
8971e02e49 [cli] Normalize output file paths in vercel build (#8149)
When a Builder returns an output asset that contains irregular slashes (multiple slashes or leading/trailing slashes), then have them be removed from the file path before creating the output asset.

This fixes an edge case where `@vercel/next` could end up outputting a Serverless Function with a trailing slash (i.e. `en-US/`). Before this PR, that would be serialized to the filesystem at `en-US/.func`, but after this fix it's saved in the correct `en-US.func` directory.
2022-07-14 07:58:20 +00:00
Nathan Rajlich
10c91c8579 [cli] Store build error in "builds.json" file in vc build (#8148)
When a build fails, store the serialized Error in the "builds.json" file under the "build" object of the Builder that failed.

Example:

```json
{
  "//": "This file was generated by the `vercel build` command. It is not part of the Build Output API.",
  "target": "preview",
  "argv": [
    "/usr/local/bin/node",
    "/Users/nrajlich/Code/vercel/vercel/packages/cli/src/index.ts",
    "build",
    "--cwd",
    "/Users/nrajlich/Downloads/vc-build-next-repro/"
  ],
  "builds": [
    {
      "require": "@vercel/next",
      "requirePath": "/Users/nrajlich/Code/vercel/vercel/packages/next/dist/index",
      "apiVersion": 2,
      "src": "package.json",
      "use": "@vercel/next",
      "config": {
        "zeroConfig": true,
        "framework": "nextjs"
      },
      "error": {
        "name": "Error",
        "message": "Command \"pnpm run build\" exited with 1",
        "stack": "Error: Command \"pnpm run build\" exited with 1\n    at ChildProcess.<anonymous> (/Users/nrajlich/Code/vercel/vercel/packages/build-utils/dist/index.js:20591:20)\n    at ChildProcess.emit (node:events:527:28)\n    at ChildProcess.emit (node:domain:475:12)\n    at maybeClose (node:internal/child_process:1092:16)\n    at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)",
        "hideStackTrace": true,
        "code": "BUILD_UTILS_SPAWN_1"
      }
    }
  ]
}
```
2022-07-14 01:05:00 +00:00
280 changed files with 4326 additions and 2562 deletions

View File

@@ -12,6 +12,7 @@ To get started, execute the following:
``` ```
git clone https://github.com/vercel/vercel git clone https://github.com/vercel/vercel
cd vercel
yarn install yarn install
yarn bootstrap yarn bootstrap
yarn build yarn build

View File

@@ -1,4 +1,5 @@
# https://prettier.io/docs/en/ignore.html # https://prettier.io/docs/en/ignore.html
# ignore this file with an intentional syntax error # ignore these files with an intentional syntax error
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js
packages/cli/test/fixtures/unit/commands/build/node-error/api/typescript.ts

View File

@@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@sentry/node": "5.11.1", "@sentry/node": "5.11.1",
"got": "10.2.1", "got": "10.2.1",
"node-fetch": "2.6.1", "node-fetch": "2.6.7",
"parse-github-url": "1.0.2", "parse-github-url": "1.0.2",
"tar-fs": "2.0.0", "tar-fs": "2.0.0",
"unzip-stream": "0.3.0" "unzip-stream": "0.3.0"

View File

@@ -26,12 +26,12 @@
"jest": "28.0.2", "jest": "28.0.2",
"json5": "2.1.1", "json5": "2.1.1",
"lint-staged": "9.2.5", "lint-staged": "9.2.5",
"node-fetch": "2.6.1", "node-fetch": "2.6.7",
"npm-package-arg": "6.1.0", "npm-package-arg": "6.1.0",
"prettier": "2.6.2", "prettier": "2.6.2",
"ts-eager": "2.0.2", "ts-eager": "2.0.2",
"ts-jest": "28.0.5", "ts-jest": "28.0.5",
"turbo": "1.3.1" "turbo": "1.3.2-canary.1"
}, },
"scripts": { "scripts": {
"lerna": "lerna", "lerna": "lerna",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/build-utils", "name": "@vercel/build-utils",
"version": "5.0.2", "version": "5.0.5",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",
@@ -31,11 +31,10 @@
"@types/node-fetch": "^2.1.6", "@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@types/yazl": "2.4.2", "@types/yazl": "2.4.2",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.34.0",
"aggregate-error": "3.0.1", "aggregate-error": "3.0.1",
"async-retry": "1.2.3", "async-retry": "1.2.3",
"async-sema": "2.1.4", "async-sema": "2.1.4",
"boxen": "4.2.0",
"cross-spawn": "6.0.5", "cross-spawn": "6.0.5",
"end-of-stream": "1.4.1", "end-of-stream": "1.4.1",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
@@ -44,7 +43,7 @@
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"multistream": "2.1.1", "multistream": "2.1.1",
"node-fetch": "2.6.1", "node-fetch": "2.6.7",
"semver": "6.1.1", "semver": "6.1.1",
"typescript": "4.3.4", "typescript": "4.3.4",
"yazl": "2.5.1" "yazl": "2.5.1"

View File

@@ -33,9 +33,6 @@ function getHint(isAuto = false) {
: `Please set "engines": { "node": "${range}" } in your \`package.json\` file to use Node.js ${major}.`; : `Please set "engines": { "node": "${range}" } in your \`package.json\` file to use Node.js ${major}.`;
} }
const upstreamProvider =
'This change is the result of a decision made by an upstream infrastructure provider (AWS).';
export function getLatestNodeVersion() { export function getLatestNodeVersion() {
return allOptions[0]; return allOptions[0];
} }
@@ -75,7 +72,7 @@ export async function getSupportedNodeVersion(
throw new NowBuildError({ throw new NowBuildError({
code: 'BUILD_UTILS_NODE_VERSION_DISCONTINUED', code: 'BUILD_UTILS_NODE_VERSION_DISCONTINUED',
link: 'http://vercel.link/node-version', link: 'http://vercel.link/node-version',
message: `${intro} ${getHint(isAuto)} ${upstreamProvider}`, message: `${intro} ${getHint(isAuto)}`,
}); });
} }
@@ -86,9 +83,9 @@ export async function getSupportedNodeVersion(
console.warn( console.warn(
`Error: Node.js version ${ `Error: Node.js version ${
selection.range selection.range
} is deprecated. Deployments created on or after ${d} will fail to build. ${getHint( } has reached End-of-Life. Deployments created on or after ${d} will fail to build. ${getHint(
isAuto isAuto
)} ${upstreamProvider}` )}`
); );
} }

View File

@@ -305,67 +305,46 @@ export async function scanParentDirs(
): Promise<ScanParentDirsResult> { ): Promise<ScanParentDirsResult> {
assert(path.isAbsolute(destPath)); assert(path.isAbsolute(destPath));
let cliType: CliType = 'yarn'; const pkgJsonPath = await walkParentDirs({
let packageJson: PackageJson | undefined; base: '/',
let packageJsonPath: string | undefined; start: destPath,
let currentDestPath = destPath; filename: 'package.json',
});
const packageJson: PackageJson | undefined =
readPackageJson && pkgJsonPath
? JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'))
: undefined;
const [yarnLockPath, npmLockPath, pnpmLockPath] = await walkParentDirsMulti({
base: '/',
start: destPath,
filenames: ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'],
});
let lockfileVersion: number | undefined; let lockfileVersion: number | undefined;
let cliType: CliType = 'yarn';
// eslint-disable-next-line no-constant-condition const [hasYarnLock, packageLockJson, pnpmLockYaml] = await Promise.all([
while (true) { Boolean(yarnLockPath),
packageJsonPath = path.join(currentDestPath, 'package.json'); npmLockPath
// eslint-disable-next-line no-await-in-loop ? readConfigFile<{ lockfileVersion: number }>(npmLockPath)
if (await fs.pathExists(packageJsonPath)) { : null,
// Only read the contents of the *first* `package.json` file found, pnpmLockPath
// since that's the one related to this installation. ? readConfigFile<{ lockfileVersion: number }>(pnpmLockPath)
if (readPackageJson && !packageJson) { : null,
// eslint-disable-next-line no-await-in-loop ]);
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
}
// eslint-disable-next-line no-await-in-loop // Priority order is Yarn > pnpm > npm
const [packageLockJson, hasYarnLock, pnpmLockYaml] = await Promise.all([ if (hasYarnLock) {
fs cliType = 'yarn';
.readJson(path.join(currentDestPath, 'package-lock.json')) } else if (pnpmLockYaml) {
.catch(error => { cliType = 'pnpm';
// If the file doesn't exist, fail gracefully otherwise error // just ensure that it is read as a number and not a string
if (error.code === 'ENOENT') { lockfileVersion = Number(pnpmLockYaml.lockfileVersion);
return null; } else if (packageLockJson) {
} cliType = 'npm';
throw error; lockfileVersion = packageLockJson.lockfileVersion;
}),
fs.pathExists(path.join(currentDestPath, 'yarn.lock')),
readConfigFile<{ lockfileVersion: number }>(
path.join(currentDestPath, 'pnpm-lock.yaml')
),
]);
// Priority order is Yarn > pnpm > npm
// - find highest priority lock file and use that
if (hasYarnLock) {
cliType = 'yarn';
} else if (pnpmLockYaml) {
cliType = 'pnpm';
// just ensure that it is read as a number and not a string
lockfileVersion = Number(pnpmLockYaml.lockfileVersion);
} else if (packageLockJson) {
cliType = 'npm';
lockfileVersion = packageLockJson.lockfileVersion;
}
// Only stop iterating if a lockfile was found, because it's possible
// that the lockfile is in a higher path than where the `package.json`
// file was found.
if (packageLockJson || hasYarnLock || pnpmLockYaml) {
break;
}
}
const newDestPath = path.dirname(currentDestPath);
if (currentDestPath === newDestPath) break;
currentDestPath = newDestPath;
} }
const packageJsonPath = pkgJsonPath || undefined;
return { cliType, packageJson, lockfileVersion, packageJsonPath }; return { cliType, packageJson, lockfileVersion, packageJsonPath };
} }
@@ -387,11 +366,48 @@ export async function walkParentDirs({
} }
parent = path.dirname(current); parent = path.dirname(current);
if (parent === current) {
// Reached root directory of the filesystem
break;
}
} }
return null; return null;
} }
async function walkParentDirsMulti({
base,
start,
filenames,
}: {
base: string;
start: string;
filenames: string[];
}): Promise<(string | undefined)[]> {
let parent = '';
for (let current = start; base.length <= current.length; current = parent) {
const fullPaths = filenames.map(f => path.join(current, f));
const existResults = await Promise.all(
fullPaths.map(f => fs.pathExists(f))
);
const foundOneOrMore = existResults.some(b => b);
if (foundOneOrMore) {
return fullPaths.map((f, i) => (existResults[i] ? f : undefined));
}
parent = path.dirname(current);
if (parent === current) {
// Reached root directory of the filesystem
break;
}
}
return [];
}
function isSet<T>(v: any): v is Set<T> { function isSet<T>(v: any): v is Set<T> {
return v?.constructor?.name === 'Set'; return v?.constructor?.name === 'Set';
} }

View File

@@ -19,7 +19,7 @@ import {
Meta, Meta,
} from '../src'; } from '../src';
jest.setTimeout(7 * 1000); jest.setTimeout(10 * 1000);
async function expectBuilderError(promise: Promise<any>, pattern: string) { async function expectBuilderError(promise: Promise<any>, pattern: string) {
let result; let result;
@@ -387,10 +387,10 @@ it('should warn for deprecated versions, soon to be discontinued', async () => {
12 12
); );
expect(warningMessages).toStrictEqual([ expect(warningMessages).toStrictEqual([
'Error: Node.js version 10.x is deprecated. Deployments created on or after 2021-04-20 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).', 'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
'Error: Node.js version 10.x is deprecated. Deployments created on or after 2021-04-20 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).', 'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
'Error: Node.js version 12.x is deprecated. Deployments created on or after 2022-08-09 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).', 'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-08-09 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
'Error: Node.js version 12.x is deprecated. Deployments created on or after 2022-08-09 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).', 'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-08-09 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
]); ]);
global.Date.now = realDateNow; global.Date.now = realDateNow;
@@ -454,6 +454,7 @@ it('should return lockfileVersion 2 with npm7', async () => {
const result = await scanParentDirs(fixture); const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('npm'); expect(result.cliType).toEqual('npm');
expect(result.lockfileVersion).toEqual(2); expect(result.lockfileVersion).toEqual(2);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
}); });
it('should not return lockfileVersion with yarn', async () => { it('should not return lockfileVersion with yarn', async () => {
@@ -461,6 +462,7 @@ it('should not return lockfileVersion with yarn', async () => {
const result = await scanParentDirs(fixture); const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('yarn'); expect(result.cliType).toEqual('yarn');
expect(result.lockfileVersion).toEqual(undefined); expect(result.lockfileVersion).toEqual(undefined);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
}); });
it('should return lockfileVersion 1 with older versions of npm', async () => { it('should return lockfileVersion 1 with older versions of npm', async () => {
@@ -468,6 +470,7 @@ it('should return lockfileVersion 1 with older versions of npm', async () => {
const result = await scanParentDirs(fixture); const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('npm'); expect(result.cliType).toEqual('npm');
expect(result.lockfileVersion).toEqual(1); expect(result.lockfileVersion).toEqual(1);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
}); });
it('should detect npm Workspaces', async () => { it('should detect npm Workspaces', async () => {
@@ -475,20 +478,45 @@ it('should detect npm Workspaces', async () => {
const result = await scanParentDirs(fixture); const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('npm'); expect(result.cliType).toEqual('npm');
expect(result.lockfileVersion).toEqual(2); expect(result.lockfileVersion).toEqual(2);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
}); });
it('should detect pnpm', async () => { it('should detect pnpm without workspace', async () => {
const fixture = path.join(__dirname, 'fixtures', '22-pnpm'); const fixture = path.join(__dirname, 'fixtures', '22-pnpm');
const result = await scanParentDirs(fixture); const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('pnpm'); expect(result.cliType).toEqual('pnpm');
expect(result.lockfileVersion).toEqual(5.3); expect(result.lockfileVersion).toEqual(5.3);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
}); });
it('should detect pnpm Workspaces', async () => { it('should detect pnpm with workspaces', async () => {
const fixture = path.join(__dirname, 'fixtures', '23-pnpm-workspaces/a'); const fixture = path.join(__dirname, 'fixtures', '23-pnpm-workspaces/c');
const result = await scanParentDirs(fixture); const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('pnpm'); expect(result.cliType).toEqual('pnpm');
expect(result.lockfileVersion).toEqual(5.3); expect(result.lockfileVersion).toEqual(5.3);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
it('should detect package.json in nested backend', async () => {
const fixture = path.join(
__dirname,
'../../node/test/fixtures/18.1-nested-packagejson/backend'
);
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('yarn');
expect(result.lockfileVersion).toEqual(undefined);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
it('should detect package.json in nested frontend', async () => {
const fixture = path.join(
__dirname,
'../../node/test/fixtures/18.1-nested-packagejson/frontend'
);
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('yarn');
expect(result.lockfileVersion).toEqual(undefined);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
}); });
it('should only invoke `runNpmInstall()` once per `package.json` file (serial)', async () => { it('should only invoke `runNpmInstall()` once per `package.json` file (serial)', async () => {

View File

@@ -53,13 +53,13 @@ At this point you can make modifications to the CLI source code and test them ou
cd packages/cli cd packages/cli
``` ```
From within the `packages/cli` directory, you can use the `ts-eager` command line tool to quickly excute Vercel CLI from its TypeScript source code directly (without having to manually compile first). For example: From within the `packages/cli` directory, you can use the "dev" script to quickly execute Vercel CLI from its TypeScript source code directly (without having to manually compile first). For example:
```bash ```bash
npx ts-eager src yarn dev deploy
npx ts-eager src login yarn dev whoami
npx ts-eager src switch --debug yarn dev login
npx ts-eager src dev yarn dev switch --debug
``` ```
When you are satisfied with your changes, make a commit and create a pull request! When you are satisfied with your changes, make a commit and create a pull request!

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel", "name": "vercel",
"version": "27.1.2", "version": "27.3.0",
"preferGlobal": true, "preferGlobal": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "The command-line interface for Vercel", "description": "The command-line interface for Vercel",
@@ -18,8 +18,8 @@
"test-integration-dev": "yarn test test/dev/", "test-integration-dev": "yarn test test/dev/",
"prepublishOnly": "yarn build", "prepublishOnly": "yarn build",
"coverage": "codecov", "coverage": "codecov",
"build": "node -r ts-eager/register ./scripts/build.ts", "build": "ts-node ./scripts/build.ts",
"build-dev": "node -r ts-eager/register ./scripts/build.ts --dev" "dev": "ts-node ./src/index.ts"
}, },
"bin": { "bin": {
"vc": "./dist/index.js", "vc": "./dist/index.js",
@@ -42,16 +42,16 @@
"node": ">= 14" "node": ">= 14"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "5.0.2", "@vercel/build-utils": "5.0.5",
"@vercel/go": "2.0.6", "@vercel/go": "2.0.9",
"@vercel/hydrogen": "0.0.3", "@vercel/hydrogen": "0.0.6",
"@vercel/next": "3.1.5", "@vercel/next": "3.1.9",
"@vercel/node": "2.4.3", "@vercel/node": "2.5.0",
"@vercel/python": "3.0.6", "@vercel/python": "3.1.1",
"@vercel/redwood": "1.0.7", "@vercel/redwood": "1.0.10",
"@vercel/remix": "1.0.8", "@vercel/remix": "1.0.11",
"@vercel/ruby": "1.3.14", "@vercel/ruby": "1.3.17",
"@vercel/static-build": "1.0.7", "@vercel/static-build": "1.0.10",
"update-notifier": "5.1.0" "update-notifier": "5.1.0"
}, },
"devDependencies": { "devDependencies": {
@@ -59,6 +59,7 @@
"@next/env": "11.1.2", "@next/env": "11.1.2",
"@sentry/node": "5.5.0", "@sentry/node": "5.5.0",
"@sindresorhus/slugify": "0.11.0", "@sindresorhus/slugify": "0.11.0",
"@swc/core": "1.2.218",
"@tootallnate/once": "1.1.2", "@tootallnate/once": "1.1.2",
"@types/ansi-escapes": "3.0.0", "@types/ansi-escapes": "3.0.0",
"@types/ansi-regex": "4.0.0", "@types/ansi-regex": "4.0.0",
@@ -96,11 +97,11 @@
"@types/which": "1.3.2", "@types/which": "1.3.2",
"@types/write-json-file": "2.2.1", "@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0", "@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.1.1", "@vercel/client": "12.1.4",
"@vercel/frameworks": "1.1.0", "@vercel/frameworks": "1.1.1",
"@vercel/fs-detectors": "2.0.0", "@vercel/fs-detectors": "2.0.1",
"@vercel/ncc": "0.24.0", "@vercel/fun": "1.0.4",
"@zeit/fun": "0.11.2", "@vercel/ncc": "0.34.0",
"@zeit/source-map-support": "0.6.2", "@zeit/source-map-support": "0.6.2",
"ajv": "6.12.2", "ajv": "6.12.2",
"alpha-sort": "2.0.1", "alpha-sort": "2.0.1",
@@ -111,6 +112,7 @@
"async-retry": "1.1.3", "async-retry": "1.1.3",
"async-sema": "2.1.4", "async-sema": "2.1.4",
"ava": "2.2.0", "ava": "2.2.0",
"boxen": "4.2.0",
"bytes": "3.0.0", "bytes": "3.0.0",
"chalk": "4.1.0", "chalk": "4.1.0",
"chance": "1.1.7", "chance": "1.1.7",
@@ -147,7 +149,7 @@
"minimatch": "3.0.4", "minimatch": "3.0.4",
"mri": "1.1.5", "mri": "1.1.5",
"ms": "2.1.2", "ms": "2.1.2",
"node-fetch": "2.6.1", "node-fetch": "2.6.7",
"npm-package-arg": "6.1.0", "npm-package-arg": "6.1.0",
"open": "8.4.0", "open": "8.4.0",
"ora": "3.4.0", "ora": "3.4.0",
@@ -169,8 +171,8 @@
"title": "3.4.1", "title": "3.4.1",
"tmp-promise": "1.0.3", "tmp-promise": "1.0.3",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
"ts-node": "8.3.0", "ts-node": "10.9.1",
"typescript": "4.3.4", "typescript": "4.7.4",
"universal-analytics": "0.4.20", "universal-analytics": "0.4.20",
"utility-types": "2.1.0", "utility-types": "2.1.0",
"which": "2.0.2", "which": "2.0.2",

View File

@@ -27,40 +27,38 @@ function envToString(key: string) {
} }
async function main() { async function main() {
const isDev = process.argv[2] === '--dev'; // Read the secrets from GitHub Actions and generate a file.
// During local development, these secrets will be empty.
await createConstants();
if (!isDev) { // `vercel dev` uses chokidar to watch the filesystem, but opts-out of the
// Read the secrets from GitHub Actions and generate a file. // `fsevents` feature using `useFsEvents: false`, so delete the module here so
// During local development, these secrets will be empty. // that it is not compiled by ncc, which makes the npm package size larger
await createConstants(); // than necessary.
await remove(join(dirRoot, '../../node_modules/fsevents'));
// `vercel dev` uses chokidar to watch the filesystem, but opts-out of the // Compile the `doT.js` template files for `vercel dev`
// `fsevents` feature using `useFsEvents: false`, so delete the module here so console.log();
// that it is not compiled by ncc, which makes the npm package size larger await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
// than necessary. stdio: 'inherit',
await remove(join(dirRoot, '../../node_modules/fsevents')); });
// Compile the `doT.js` template files for `vercel dev`
console.log();
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
stdio: 'inherit',
});
}
// Do the initial `ncc` build // Do the initial `ncc` build
console.log(); console.log();
const args = ['ncc', 'build', '--external', 'update-notifier']; const args = [
if (isDev) { 'ncc',
args.push('--source-map'); 'build',
} '--external',
args.push('src/index.ts'); 'update-notifier',
'src/index.ts',
];
await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot }); await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot });
// `ncc` has some issues with `@zeit/fun`'s runtime files: // `ncc` has some issues with `@vercel/fun`'s runtime files:
// - Executable bits on the `bootstrap` files appear to be lost: // - Executable bits on the `bootstrap` files appear to be lost:
// https://github.com/zeit/ncc/pull/182 // https://github.com/vercel/ncc/pull/182
// - The `bootstrap.js` asset does not get copied into the output dir: // - The `bootstrap.js` asset does not get copied into the output dir:
// https://github.com/zeit/ncc/issues/278 // https://github.com/vercel/ncc/issues/278
// //
// Aside from those issues, all the same files from the `runtimes` directory // Aside from those issues, all the same files from the `runtimes` directory
// should be copied into the output runtimes dir, specifically the `index.js` // should be copied into the output runtimes dir, specifically the `index.js`
@@ -70,7 +68,7 @@ async function main() {
// with `fun`'s cache invalidation mechanism and they need to be shasum'd. // with `fun`'s cache invalidation mechanism and they need to be shasum'd.
const runtimes = join( const runtimes = join(
dirRoot, dirRoot,
'../../node_modules/@zeit/fun/dist/src/runtimes' '../../node_modules/@vercel/fun/dist/src/runtimes'
); );
await cpy('**/*', join(distRoot, 'runtimes'), { await cpy('**/*', join(distRoot, 'runtimes'), {
parents: true, parents: true,
@@ -79,6 +77,7 @@ async function main() {
// Band-aid to bundle stuff that `ncc` neglects to bundle // Band-aid to bundle stuff that `ncc` neglects to bundle
await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot); await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot);
await cpy(join(dirRoot, 'src/util/dev/builder-worker.js'), distRoot);
console.log('Finished building Vercel CLI'); console.log('Finished building Vercel CLI');
} }

View File

@@ -22,19 +22,7 @@ export default async function ls(
) { ) {
const { output } = client; const { output } = client;
const { '--next': nextTimestamp } = opts; const { '--next': nextTimestamp } = opts;
const { contextName } = await getScope(client);
let contextName = null;
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) { if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
output.error('Please provide a number for flag --next'); output.error('Please provide a number for flag --next');

View File

@@ -23,19 +23,7 @@ export default async function rm(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
const { contextName } = await getScope(client);
let contextName = null;
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const [aliasOrId] = args; const [aliasOrId] = args;

View File

@@ -15,10 +15,9 @@ import { isValidName } from '../../util/is-valid-name';
import handleCertError from '../../util/certs/handle-cert-error'; import handleCertError from '../../util/certs/handle-cert-error';
import isWildcardAlias from '../../util/alias/is-wildcard-alias'; import isWildcardAlias from '../../util/alias/is-wildcard-alias';
import link from '../../util/output/link'; import link from '../../util/output/link';
import { User } from '../../types';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import toHost from '../../util/to-host'; import toHost from '../../util/to-host';
import { VercelConfig } from '../../util/dev/types'; import type { VercelConfig } from '@vercel/client';
type Options = { type Options = {
'--debug': boolean; '--debug': boolean;
@@ -30,23 +29,9 @@ export default async function set(
opts: Partial<Options>, opts: Partial<Options>,
args: string[] args: string[]
) { ) {
const { output, localConfig } = client;
const setStamp = stamp(); const setStamp = stamp();
const { output, localConfig } = client;
let user: User; const { contextName, user } = await getScope(client);
let contextName: string | null = null;
try {
({ contextName, user } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
// If there are more than two args we have to error // If there are more than two args we have to error
if (args.length > 2) { if (args.length > 2) {

View File

@@ -25,7 +25,7 @@ import {
MergeRoutesProps, MergeRoutesProps,
Route, Route,
} from '@vercel/routing-utils'; } from '@vercel/routing-utils';
import { VercelConfig } from '@vercel/client'; import type { VercelConfig } from '@vercel/client';
import pull from './pull'; import pull from './pull';
import { staticFiles as getFiles } from '../util/get-files'; import { staticFiles as getFiles } from '../util/get-files';
@@ -36,7 +36,10 @@ import * as cli from '../util/pkg-name';
import cliPkg from '../util/pkg'; import cliPkg from '../util/pkg';
import readJSONFile from '../util/read-json-file'; import readJSONFile from '../util/read-json-file';
import { CantParseJSONFile } from '../util/errors-ts'; import { CantParseJSONFile } from '../util/errors-ts';
import { readProjectSettings } from '../util/projects/project-settings'; import {
ProjectLinkAndSettings,
readProjectSettings,
} from '../util/projects/project-settings';
import { VERCEL_DIR } from '../util/projects/link'; import { VERCEL_DIR } from '../util/projects/link';
import confirm from '../util/input/confirm'; import confirm from '../util/input/confirm';
import { emoji, prependEmoji } from '../util/emoji'; import { emoji, prependEmoji } from '../util/emoji';
@@ -46,12 +49,31 @@ import {
PathOverride, PathOverride,
writeBuildResult, writeBuildResult,
} from '../util/build/write-build-result'; } from '../util/build/write-build-result';
import { importBuilders, BuilderWithPkg } from '../util/build/import-builders'; import { importBuilders } from '../util/build/import-builders';
import { initCorepack, cleanupCorepack } from '../util/build/corepack'; import { initCorepack, cleanupCorepack } from '../util/build/corepack';
import { sortBuilders } from '../util/build/sort-builders'; import { sortBuilders } from '../util/build/sort-builders';
import { toEnumerableError } from '../util/error';
type BuildResult = BuildResultV2 | BuildResultV3; type BuildResult = BuildResultV2 | BuildResultV3;
interface SerializedBuilder extends Builder {
error?: any;
require?: string;
requirePath?: string;
apiVersion: number;
}
/**
* Contents of the `builds.json` file.
*/
export interface BuildsManifest {
'//': string;
target: string;
argv: string[];
error?: any;
builds?: SerializedBuilder[];
}
const help = () => { const help = () => {
return console.log(` return console.log(`
${chalk.bold(`${cli.logo} ${cli.name} build`)} ${chalk.bold(`${cli.logo} ${cli.name} build`)}
@@ -167,21 +189,83 @@ export default async function main(client: Client): Promise<number> {
project = await readProjectSettings(join(cwd, VERCEL_DIR)); project = await readProjectSettings(join(cwd, VERCEL_DIR));
} }
// TODO: load env vars from the API, fall back to local files if that fails // Delete output directory from potential previous build
const outputDir = argv['--output']
? resolve(argv['--output'])
: join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
const envPath = await checkExists([ const buildsJson: BuildsManifest = {
join(cwd, VERCEL_DIR, `.env.${target}.local`), '//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
join(cwd, `.env`), target,
]); argv: process.argv,
if (envPath) { };
dotenv.config({ path: envPath, debug: client.output.isDebugEnabled() });
output.log(`Loaded env from "${relative(cwd, envPath)}"`); const envToUnset = new Set<string>(['VERCEL', 'NOW_BUILDER']);
try {
const envPath = join(cwd, VERCEL_DIR, `.env.${target}.local`);
// TODO (maybe?): load env vars from the API, fall back to the local file if that fails
const dotenvResult = dotenv.config({
path: envPath,
debug: client.output.isDebugEnabled(),
});
if (dotenvResult.error) {
output.debug(
`Failed loading environment variables: ${dotenvResult.error}`
);
} else if (dotenvResult.parsed) {
for (const key of Object.keys(dotenvResult.parsed)) {
envToUnset.add(key);
}
output.debug(`Loaded environment variables from "${envPath}"`);
}
// For Vercel Analytics support
if (project.settings.analyticsId) {
envToUnset.add('VERCEL_ANALYTICS_ID');
process.env.VERCEL_ANALYTICS_ID = project.settings.analyticsId;
}
// Some build processes use these env vars to platform detect Vercel
process.env.VERCEL = '1';
process.env.NOW_BUILDER = '1';
return await doBuild(client, project, buildsJson, cwd, outputDir);
} catch (err: any) {
output.prettyError(err);
// Write error to `builds.json` file
buildsJson.error = toEnumerableError(err);
const buildsJsonPath = join(outputDir, 'builds.json');
const configJsonPath = join(outputDir, 'config.json');
await fs.outputJSON(buildsJsonPath, buildsJson, {
spaces: 2,
});
await fs.writeJSON(configJsonPath, { version: 3 }, { spaces: 2 });
return 1;
} finally {
// Unset environment variables that were added by dotenv
// (this is mostly for the unit tests)
for (const key of envToUnset) {
delete process.env[key];
}
} }
}
// Some build processes use these env vars to platform detect Vercel /**
process.env.VERCEL = '1'; * Execute the Project's builders. If this function throws an error,
process.env.NOW_BUILDER = '1'; * then it will be serialized into the `builds.json` manifest file.
*/
async function doBuild(
client: Client,
project: ProjectLinkAndSettings,
buildsJson: BuildsManifest,
cwd: string,
outputDir: string
): Promise<number> {
const { output } = client;
const workPath = join(cwd, project.settings.rootDirectory || '.'); const workPath = join(cwd, project.settings.rootDirectory || '.');
// Load `package.json` and `vercel.json` files // Load `package.json` and `vercel.json` files
@@ -199,23 +283,23 @@ export default async function main(client: Client): Promise<number> {
normalizePath(relative(workPath, f)) normalizePath(relative(workPath, f))
); );
const routesResult = getTransformedRoutes({ nowConfig: vercelConfig || {} }); const routesResult = getTransformedRoutes(vercelConfig || {});
if (routesResult.error) { if (routesResult.error) {
output.prettyError(routesResult.error); throw routesResult.error;
return 1;
} }
if (vercelConfig?.builds && vercelConfig.functions) { if (vercelConfig?.builds && vercelConfig.functions) {
output.prettyError({ throw new NowBuildError({
code: 'bad_request',
message: message:
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.', 'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.',
link: 'https://vercel.link/functions-and-builds', link: 'https://vercel.link/functions-and-builds',
}); });
return 1;
} }
let builds = vercelConfig?.builds || []; let builds = vercelConfig?.builds || [];
let zeroConfigRoutes: Route[] = []; let zeroConfigRoutes: Route[] = [];
let isZeroConfig = false;
if (builds.length > 0) { if (builds.length > 0) {
output.warn( output.warn(
@@ -224,17 +308,18 @@ export default async function main(client: Client): Promise<number> {
builds = builds.map(b => expandBuild(files, b)).flat(); builds = builds.map(b => expandBuild(files, b)).flat();
} else { } else {
// Zero config // Zero config
isZeroConfig = true;
// Detect the Vercel Builders that will need to be invoked // Detect the Vercel Builders that will need to be invoked
const detectedBuilders = await detectBuilders(files, pkg, { const detectedBuilders = await detectBuilders(files, pkg, {
...vercelConfig, ...vercelConfig,
projectSettings: project.settings, projectSettings: project.settings,
ignoreBuildScript: true,
featHandleMiss: true, featHandleMiss: true,
}); });
if (detectedBuilders.errors && detectedBuilders.errors.length > 0) { if (detectedBuilders.errors && detectedBuilders.errors.length > 0) {
output.prettyError(detectedBuilders.errors[0]); throw detectedBuilders.errors[0];
return 1;
} }
for (const w of detectedBuilders.warnings) { for (const w of detectedBuilders.warnings) {
@@ -267,13 +352,7 @@ export default async function main(client: Client): Promise<number> {
const builderSpecs = new Set(builds.map(b => b.use)); const builderSpecs = new Set(builds.map(b => b.use));
let buildersWithPkgs: Map<string, BuilderWithPkg>; const buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
try {
buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
} catch (err: any) {
output.prettyError(err);
return 1;
}
// Populate Files -> FileFsRef mapping // Populate Files -> FileFsRef mapping
const filesMap: Files = {}; const filesMap: Files = {};
@@ -283,12 +362,6 @@ export default async function main(client: Client): Promise<number> {
filesMap[path] = new FileFsRef({ mode, fsPath }); filesMap[path] = new FileFsRef({ mode, fsPath });
} }
// Delete output directory from potential previous build
const outputDir = argv['--output']
? resolve(argv['--output'])
: join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
const buildStamp = stamp(); const buildStamp = stamp();
// Create fresh new output directory // Create fresh new output directory
@@ -297,32 +370,31 @@ export default async function main(client: Client): Promise<number> {
const ops: Promise<Error | void>[] = []; const ops: Promise<Error | void>[] = [];
// Write the `detectedBuilders` result to output dir // Write the `detectedBuilders` result to output dir
ops.push( const buildsJsonBuilds = new Map<Builder, SerializedBuilder>(
fs.writeJSON( builds.map(build => {
join(outputDir, 'builds.json'), const builderWithPkg = buildersWithPkgs.get(build.use);
{ if (!builderWithPkg) {
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.', throw new Error(`Failed to load Builder "${build.use}"`);
target,
argv: process.argv,
builds: builds.map(build => {
const builderWithPkg = buildersWithPkgs.get(build.use);
if (!builderWithPkg) {
throw new Error(`Failed to load Builder "${build.use}"`);
}
const { builder, pkg: builderPkg } = builderWithPkg;
return {
require: builderPkg.name,
requirePath: builderWithPkg.path,
apiVersion: builder.version,
...build,
};
}),
},
{
spaces: 2,
} }
) const { builder, pkg: builderPkg } = builderWithPkg;
return [
build,
{
require: builderPkg.name,
requirePath: builderWithPkg.path,
apiVersion: builder.version,
...build,
},
];
})
); );
buildsJson.builds = Array.from(buildsJsonBuilds.values());
const buildsJsonPath = join(outputDir, 'builds.json');
const writeBuildsJsonPromise = fs.writeJSON(buildsJsonPath, buildsJson, {
spaces: 2,
});
ops.push(writeBuildsJsonPromise);
// The `meta` config property is re-used for each Builder // The `meta` config property is re-used for each Builder
// invocation so that Builders can share state between // invocation so that Builders can share state between
@@ -347,51 +419,75 @@ export default async function main(client: Client): Promise<number> {
if (!builderWithPkg) { if (!builderWithPkg) {
throw new Error(`Failed to load Builder "${build.use}"`); throw new Error(`Failed to load Builder "${build.use}"`);
} }
const { builder, pkg: builderPkg } = builderWithPkg;
const buildConfig: Config = { try {
outputDirectory: project.settings.outputDirectory ?? undefined, const { builder, pkg: builderPkg } = builderWithPkg;
...build.config,
projectSettings: project.settings,
installCommand: project.settings.installCommand ?? undefined,
devCommand: project.settings.devCommand ?? undefined,
buildCommand: project.settings.buildCommand ?? undefined,
framework: project.settings.framework,
nodeVersion: project.settings.nodeVersion,
};
const buildOptions: BuildOptions = {
files: filesMap,
entrypoint: build.src,
workPath,
repoRootPath,
config: buildConfig,
meta,
};
output.debug(
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
);
const buildResult = await builder.build(buildOptions);
// Store the build result to generate the final `config.json` after const buildConfig: Config = isZeroConfig
// all builds have completed ? {
buildResults.set(build, buildResult); outputDirectory: project.settings.outputDirectory ?? undefined,
...build.config,
projectSettings: project.settings,
installCommand: project.settings.installCommand ?? undefined,
devCommand: project.settings.devCommand ?? undefined,
buildCommand: project.settings.buildCommand ?? undefined,
framework: project.settings.framework,
nodeVersion: project.settings.nodeVersion,
}
: build.config || {};
const buildOptions: BuildOptions = {
files: filesMap,
entrypoint: build.src,
workPath,
repoRootPath,
config: buildConfig,
meta,
};
output.debug(
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
);
const buildResult = await builder.build(buildOptions);
// Start flushing the file outputs to the filesystem asynchronously // Store the build result to generate the final `config.json` after
ops.push( // all builds have completed
writeBuildResult( buildResults.set(build, buildResult);
outputDir,
buildResult, // Start flushing the file outputs to the filesystem asynchronously
build, ops.push(
builder, writeBuildResult(
builderPkg, outputDir,
vercelConfig?.cleanUrls buildResult,
).then( build,
override => { builder,
if (override) overrides.push(override); builderPkg,
}, vercelConfig
err => err ).then(
) override => {
); if (override) overrides.push(override);
},
err => err
)
);
} catch (err: any) {
const writeConfigJsonPromise = fs.writeJSON(
join(outputDir, 'config.json'),
{ version: 3 },
{ spaces: 2 }
);
await Promise.all([writeBuildsJsonPromise, writeConfigJsonPromise]);
const buildJsonBuild = buildsJsonBuilds.get(build);
if (buildJsonBuild) {
buildJsonBuild.error = toEnumerableError(err);
await fs.writeJSON(buildsJsonPath, buildsJson, {
spaces: 2,
});
}
return 1;
}
} }
if (corepackShimDir) { if (corepackShimDir) {
@@ -400,15 +496,12 @@ export default async function main(client: Client): Promise<number> {
// Wait for filesystem operations to complete // Wait for filesystem operations to complete
// TODO render progress bar? // TODO render progress bar?
let hadError = false;
const errors = await Promise.all(ops); const errors = await Promise.all(ops);
for (const error of errors) { for (const error of errors) {
if (error) { if (error) {
hadError = true; throw error;
output.prettyError(error);
} }
} }
if (hadError) return 1;
// Merge existing `config.json` file into the one that will be produced // Merge existing `config.json` file into the one that will be produced
const configPath = join(outputDir, 'config.json'); const configPath = join(outputDir, 'config.json');
@@ -528,7 +621,7 @@ function mergeImages(
let images: BuildResultV2Typical['images'] = undefined; let images: BuildResultV2Typical['images'] = undefined;
for (const result of buildResults) { for (const result of buildResults) {
if ('images' in result && result.images) { if ('images' in result && result.images) {
images = Object.assign({} || images, result.images); images = Object.assign({}, images, result.images);
} }
} }
return images; return images;
@@ -546,14 +639,3 @@ function mergeWildcard(
} }
return wildcard; return wildcard;
} }
async function checkExists(paths: Iterable<string>) {
for (const path of paths) {
try {
await fs.stat(path);
return path;
} catch (err: any) {
if (err.code !== 'ENOENT') throw err;
}
}
}

View File

@@ -5,6 +5,7 @@ import stamp from '../../util/output/stamp';
import createCertFromFile from '../../util/certs/create-cert-from-file'; import createCertFromFile from '../../util/certs/create-cert-from-file';
import createCertForCns from '../../util/certs/create-cert-for-cns'; import createCertForCns from '../../util/certs/create-cert-for-cns';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import { Cert } from '../../types';
interface Options { interface Options {
'--overwrite'?: boolean; '--overwrite'?: boolean;
@@ -21,7 +22,7 @@ async function add(
const { output } = client; const { output } = client;
const addStamp = stamp(); const addStamp = stamp();
let cert; let cert: Cert | Error;
const { const {
'--overwrite': overwite, '--overwrite': overwite,
@@ -30,18 +31,7 @@ async function add(
'--ca': caPath, '--ca': caPath,
} = opts; } = opts;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (overwite) { if (overwite) {
output.error('Overwrite option is deprecated'); output.error('Overwrite option is deprecated');

View File

@@ -39,18 +39,7 @@ export default async function issue(
'--ca': caPath, '--ca': caPath,
} = opts; } = opts;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (overwite) { if (overwite) {
output.error('Overwrite option is deprecated'); output.error('Overwrite option is deprecated');

View File

@@ -21,18 +21,8 @@ async function ls(
): Promise<number> { ): Promise<number> {
const { output } = client; const { output } = client;
const { '--next': nextTimestamp } = opts; const { '--next': nextTimestamp } = opts;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (typeof nextTimestamp !== 'undefined' && Number.isNaN(nextTimestamp)) { if (typeof nextTimestamp !== 'undefined' && Number.isNaN(nextTimestamp)) {
output.error('Please provide a number for flag --next'); output.error('Please provide a number for flag --next');
return 1; return 1;

View File

@@ -17,21 +17,9 @@ import { getCommandName } from '../../util/pkg-name';
type Options = {}; type Options = {};
async function rm(client: Client, opts: Options, args: string[]) { async function rm(client: Client, opts: Options, args: string[]) {
const { output } = client;
const rmStamp = stamp(); const rmStamp = stamp();
const { output } = client;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (args.length !== 1) { if (args.length !== 1) {
output.error( output.error(

View File

@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import bytes from 'bytes'; import bytes from 'bytes';
import chalk from 'chalk'; import chalk from 'chalk';
import { join, resolve, basename } from 'path'; import { join, resolve, basename } from 'path';
import { Dictionary, fileNameSymbol, VercelConfig } from '@vercel/client'; import { fileNameSymbol, VercelConfig } from '@vercel/client';
import code from '../../util/output/code'; import code from '../../util/output/code';
import highlight from '../../util/output/highlight'; import highlight from '../../util/output/highlight';
import { readLocalConfig } from '../../util/config/files'; import { readLocalConfig } from '../../util/config/files';
@@ -38,6 +38,7 @@ import {
ConflictingPathSegment, ConflictingPathSegment,
BuildError, BuildError,
NotDomainOwner, NotDomainOwner,
isAPIError,
} from '../../util/errors-ts'; } from '../../util/errors-ts';
import { SchemaValidationFailed } from '../../util/errors'; import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available'; import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
@@ -65,6 +66,8 @@ import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
import parseTarget from '../../util/deploy/parse-target'; import parseTarget from '../../util/deploy/parse-target';
import getPrebuiltJson from '../../util/deploy/get-prebuilt-json'; import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
import { createGitMeta } from '../../util/create-git-meta'; import { createGitMeta } from '../../util/create-git-meta';
import { parseEnv } from '../../util/parse-env';
import { errorToString, isErrnoException, isError } from '../../util/is-error';
export default async (client: Client) => { export default async (client: Client) => {
const { output } = client; const { output } = client;
@@ -216,6 +219,22 @@ export default async (client: Client) => {
} }
const prebuiltBuild = await getPrebuiltJson(path); const prebuiltBuild = await getPrebuiltJson(path);
// Ensure that there was not a build error
const prebuiltError =
prebuiltBuild?.error ||
prebuiltBuild?.builds?.find(build => 'error' in build)?.error;
if (prebuiltError) {
output.log(
`Prebuilt deployment cannot be created because ${getCommandName(
'build'
)} failed with error:\n`
);
prettyError(prebuiltError);
return 1;
}
// Ensure that the deploy target matches the build target
const assumedTarget = target || 'preview'; const assumedTarget = target || 'preview';
if (prebuiltBuild?.target && prebuiltBuild.target !== assumedTarget) { if (prebuiltBuild?.target && prebuiltBuild.target !== assumedTarget) {
let specifyTarget = ''; let specifyTarget = '';
@@ -268,8 +287,11 @@ export default async (client: Client) => {
'Which scope do you want to deploy to?', 'Which scope do you want to deploy to?',
autoConfirm autoConfirm
); );
} catch (err) { } catch (err: unknown) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') { if (
isErrnoException(err) &&
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
) {
output.error(err.message); output.error(err.message);
return 1; return 1;
} }
@@ -428,7 +450,7 @@ export default async (client: Client) => {
parseMeta(argv['--meta']) parseMeta(argv['--meta'])
); );
const gitMetadata = await createGitMeta(path, output); const gitMetadata = await createGitMeta(path, output, project);
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments // Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
const deploymentEnv = Object.assign( const deploymentEnv = Object.assign(
@@ -448,8 +470,8 @@ export default async (client: Client) => {
try { try {
await addProcessEnv(log, deploymentEnv); await addProcessEnv(log, deploymentEnv);
await addProcessEnv(log, deploymentBuildEnv); await addProcessEnv(log, deploymentBuildEnv);
} catch (err) { } catch (err: unknown) {
error(err.message); error(errorToString(err));
return 1; return 1;
} }
@@ -610,8 +632,10 @@ export default async (client: Client) => {
error('Uploading failed. Please try again.'); error('Uploading failed. Please try again.');
return 1; return 1;
} }
} catch (err) { } catch (err: unknown) {
debug(`Error: ${err}\n${err.stack}`); if (isError(err)) {
debug(`Error: ${err}\n${err.stack}`);
}
if (err instanceof NotDomainOwner) { if (err instanceof NotDomainOwner) {
output.error(err.message); output.error(err.message);
@@ -678,13 +702,7 @@ export default async (client: Client) => {
return 1; return 1;
} }
if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') { if (isAPIError(err) && err.code === 'size_limit_exceeded') {
const { additionalProperty = '' } = err.params || {};
const message = `Invalid DC name for the scale option: ${additionalProperty}`;
error(message);
}
if (err.code === 'size_limit_exceeded') {
const { sizeLimit = 0 } = err; const { sizeLimit = 0 } = err;
const message = `File size limit exceeded (${bytes(sizeLimit)})`; const message = `File size limit exceeded (${bytes(sizeLimit)})`;
error(message); error(message);
@@ -894,36 +912,3 @@ const printDeploymentStatus = async (
output.print(message + link); output.print(message + link);
} }
}; };
// Converts `env` Arrays, Strings and Objects into env Objects.
const parseEnv = (env?: string[] | Dictionary<string>) => {
if (!env) {
return {};
}
if (typeof env === 'string') {
// a single `--env` arg comes in as a String
env = [env];
}
if (Array.isArray(env)) {
return env.reduce((o, e) => {
let key;
let value;
const equalsSign = e.indexOf('=');
if (equalsSign === -1) {
key = e;
} else {
key = e.slice(0, equalsSign);
value = e.slice(equalsSign + 1);
}
o[key] = value;
return o;
}, {} as Dictionary<string | undefined>);
}
// assume it's already an Object
return env;
};

View File

@@ -15,6 +15,7 @@ import readConfig from '../../util/config/read-config';
import readJSONFile from '../../util/read-json-file'; import readJSONFile from '../../util/read-json-file';
import { getPkgName, getCommandName } from '../../util/pkg-name'; import { getPkgName, getCommandName } from '../../util/pkg-name';
import { CantParseJSONFile } from '../../util/errors-ts'; import { CantParseJSONFile } from '../../util/errors-ts';
import { isErrnoException } from '../../util/is-error';
const COMMAND_CONFIG = { const COMMAND_CONFIG = {
dev: ['dev'], dev: ['dev'],
@@ -136,7 +137,7 @@ export default async function main(client: Client) {
try { try {
return await dev(client, argv, args); return await dev(client, argv, args);
} catch (err) { } catch (err) {
if (err.code === 'ENOTFOUND') { if (isErrnoException(err) && err.code === 'ENOTFOUND') {
// Error message will look like the following: // Error message will look like the following:
// "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com" // "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || ''); const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
@@ -148,7 +149,9 @@ export default async function main(client: Client) {
)} could not be resolved. Please verify your internet connectivity and DNS configuration.` )} could not be resolved. Please verify your internet connectivity and DNS configuration.`
); );
} }
output.debug(err.stack); if (typeof err.stack === 'string') {
output.debug(err.stack);
}
return 1; return 1;
} }
output.prettyError(err); output.prettyError(err);

View File

@@ -21,18 +21,7 @@ export default async function add(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const parsedParams = parseAddDNSRecordArgs(args); const parsedParams = parseAddDNSRecordArgs(args);
if (!parsedParams) { if (!parsedParams) {

View File

@@ -14,18 +14,7 @@ export default async function add(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (args.length !== 2) { if (args.length !== 2) {
output.error( output.error(

View File

@@ -24,18 +24,7 @@ export default async function ls(
) { ) {
const { output } = client; const { output } = client;
const { '--next': nextTimestamp } = opts; const { '--next': nextTimestamp } = opts;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const [domainName] = args; const [domainName] = args;
const lsStamp = stamp(); const lsStamp = stamp();

View File

@@ -14,21 +14,11 @@ type Options = {};
export default async function rm( export default async function rm(
client: Client, client: Client,
opts: Options, _opts: Options,
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
await getScope(client);
try {
await getScope(client);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const [recordId] = args; const [recordId] = args;
if (args.length !== 1) { if (args.length !== 1) {

View File

@@ -26,18 +26,7 @@ export default async function add(
) { ) {
const { output } = client; const { output } = client;
const force = opts['--force']; const force = opts['--force'];
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const project = await getLinkedProject(client).then(result => { const project = await getLinkedProject(client).then(result => {
if (result.status === 'linked') { if (result.status === 'linked') {

View File

@@ -11,6 +11,7 @@ import promptBool from '../../util/input/prompt-bool';
import purchaseDomain from '../../util/domains/purchase-domain'; import purchaseDomain from '../../util/domains/purchase-domain';
import stamp from '../../util/output/stamp'; import stamp from '../../util/output/stamp';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import { errorToString } from '../../util/is-error';
type Options = {}; type Options = {};
@@ -20,18 +21,7 @@ export default async function buy(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const [domainName] = args; const [domainName] = args;
if (!domainName) { if (!domainName) {
@@ -68,6 +58,11 @@ export default async function buy(
return 1; return 1;
} }
if (renewalPrice instanceof Error) {
output.prettyError(renewalPrice);
return 1;
}
if (!(await getDomainStatus(client, domainName)).available) { if (!(await getDomainStatus(client, domainName)).available) {
output.error( output.error(
`The domain ${param(domainName)} is ${chalk.underline( `The domain ${param(domainName)} is ${chalk.underline(
@@ -109,11 +104,11 @@ export default async function buy(
try { try {
buyResult = await purchaseDomain(client, domainName, price, autoRenew); buyResult = await purchaseDomain(client, domainName, price, autoRenew);
} catch (err) { } catch (err: unknown) {
output.error( output.error(
'An unexpected error occurred while purchasing your domain. Please try again later.' 'An unexpected error occurred while purchasing your domain. Please try again later.'
); );
output.debug(`Server response: ${err.message}`); output.debug(`Server response: ${errorToString(err)}`);
return 1; return 1;
} }

View File

@@ -23,19 +23,7 @@ export default async function inspect(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
const { contextName } = await getScope(client);
let contextName = null;
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const [domainName] = args; const [domainName] = args;
const inspectStamp = stamp(); const inspectStamp = stamp();

View File

@@ -25,23 +25,13 @@ export default async function ls(
) { ) {
const { output } = client; const { output } = client;
const { '--next': nextTimestamp } = opts; const { '--next': nextTimestamp } = opts;
let contextName = null;
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) { if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
output.error('Please provide a number for flag --next'); output.error('Please provide a number for flag --next');
return 1; return 1;
} }
try { const { contextName } = await getScope(client);
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const lsStamp = stamp(); const lsStamp = stamp();

View File

@@ -25,20 +25,7 @@ export default async function move(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
let contextName = null; const { contextName, user } = await getScope(client);
let user = null;
try {
({ contextName, user } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const { domainName, destination } = await getArgs(args); const { domainName, destination } = await getArgs(args);
if (!isRootDomain(domainName)) { if (!isRootDomain(domainName)) {
output.error( output.error(

View File

@@ -29,18 +29,7 @@ export default async function rm(
) { ) {
const { output } = client; const { output } = client;
const [domainName] = args; const [domainName] = args;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (!domainName) { if (!domainName) {
output.error( output.error(
@@ -122,10 +111,10 @@ async function removeDomain(
output.debug(`Removing alias ${id}`); output.debug(`Removing alias ${id}`);
try { try {
await removeAliasById(client, id); await removeAliasById(client, id);
} catch (error) { } catch (err: unknown) {
// Ignore if the alias does not exist anymore // Ignore if the alias does not exist anymore
if (error.status !== 404) { if (!ERRORS.isAPIError(err) || err.status !== 404) {
throw error; throw err;
} }
} }
} }
@@ -134,10 +123,10 @@ async function removeDomain(
output.debug(`Removing cert ${id}`); output.debug(`Removing cert ${id}`);
try { try {
await deleteCertById(output, client, id); await deleteCertById(output, client, id);
} catch (error) { } catch (err: unknown) {
// Ignore if the cert does not exist anymore // Ignore if the cert does not exist anymore
if (error.status !== 404) { if (!ERRORS.isAPIError(err) || err.status !== 404) {
throw error; throw err;
} }
} }
} }

View File

@@ -23,18 +23,7 @@ export default async function transferIn(
args: string[] args: string[]
) { ) {
const { output } = client; const { output } = client;
let contextName = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
const [domainName] = args; const [domainName] = args;
if (!domainName) { if (!domainName) {

View File

@@ -1,5 +1,4 @@
import chalk from 'chalk'; import chalk from 'chalk';
import inquirer from 'inquirer';
import { ProjectEnvTarget, Project, ProjectEnvType } from '../../types'; import { ProjectEnvTarget, Project, ProjectEnvType } from '../../types';
import { Output } from '../../util/output'; import { Output } from '../../util/output';
import Client from '../../util/client'; import Client from '../../util/client';
@@ -16,6 +15,7 @@ import param from '../../util/output/param';
import { emoji, prependEmoji } from '../../util/emoji'; import { emoji, prependEmoji } from '../../util/emoji';
import { isKnownError } from '../../util/env/known-error'; import { isKnownError } from '../../util/env/known-error';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import { isAPIError } from '../../util/errors-ts';
type Options = { type Options = {
'--debug': boolean; '--debug': boolean;
@@ -66,7 +66,7 @@ export default async function add(
} }
while (!envName) { while (!envName) {
const { inputName } = await inquirer.prompt({ const { inputName } = await client.prompt({
type: 'input', type: 'input',
name: 'inputName', name: 'inputName',
message: `Whats the name of the variable?`, message: `Whats the name of the variable?`,
@@ -106,7 +106,7 @@ export default async function add(
if (stdInput) { if (stdInput) {
envValue = stdInput; envValue = stdInput;
} else { } else {
const { inputValue } = await inquirer.prompt({ const { inputValue } = await client.prompt({
type: 'input', type: 'input',
name: 'inputValue', name: 'inputValue',
message: `Whats the value of ${envName}?`, message: `Whats the value of ${envName}?`,
@@ -116,7 +116,7 @@ export default async function add(
} }
while (envTargets.length === 0) { while (envTargets.length === 0) {
const { inputTargets } = await inquirer.prompt({ const { inputTargets } = await client.prompt({
name: 'inputTargets', name: 'inputTargets',
type: 'checkbox', type: 'checkbox',
message: `Add ${envName} to which Environments (select multiple)?`, message: `Add ${envName} to which Environments (select multiple)?`,
@@ -136,7 +136,7 @@ export default async function add(
envTargets.length === 1 && envTargets.length === 1 &&
envTargets[0] === ProjectEnvTarget.Preview envTargets[0] === ProjectEnvTarget.Preview
) { ) {
const { inputValue } = await inquirer.prompt({ const { inputValue } = await client.prompt({
type: 'input', type: 'input',
name: 'inputValue', name: 'inputValue',
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`, message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
@@ -157,12 +157,12 @@ export default async function add(
envTargets, envTargets,
envGitBranch envGitBranch
); );
} catch (error) { } catch (err: unknown) {
if (isKnownError(error) && error.serverMessage) { if (isAPIError(err) && isKnownError(err)) {
output.error(error.serverMessage); output.error(err.serverMessage);
return 1; return 1;
} }
throw error; throw err;
} }
output.print( output.print(

View File

@@ -1,7 +1,9 @@
import chalk from 'chalk'; import chalk from 'chalk';
import { ProjectEnvTarget } from '../../types';
import Client from '../../util/client'; import Client from '../../util/client';
import { getEnvTargetPlaceholder } from '../../util/env/env-target'; import {
getEnvTargetPlaceholder,
isValidEnvTarget,
} from '../../util/env/env-target';
import getArgs from '../../util/get-args'; import getArgs from '../../util/get-args';
import getInvalidSubcommand from '../../util/get-invalid-subcommand'; import getInvalidSubcommand from '../../util/get-invalid-subcommand';
import getSubcommand from '../../util/get-subcommand'; import getSubcommand from '../../util/get-subcommand';
@@ -29,6 +31,7 @@ const help = () => {
${chalk.dim('Options:')} ${chalk.dim('Options:')}
-h, --help Output usage information -h, --help Output usage information
--environment Set the Environment (development, preview, production) when pulling Environment Variables
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline( -A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE' 'FILE'
)} Path to the local ${'`vercel.json`'} file )} Path to the local ${'`vercel.json`'} file
@@ -111,6 +114,7 @@ export default async function main(client: Client) {
argv = getArgs(client.argv.slice(2), { argv = getArgs(client.argv.slice(2), {
'--yes': Boolean, '--yes': Boolean,
'-y': '--yes', '-y': '--yes',
'--environment': String,
}); });
} catch (error) { } catch (error) {
handleError(error); handleError(error);
@@ -126,6 +130,17 @@ export default async function main(client: Client) {
const subArgs = argv._.slice(1); const subArgs = argv._.slice(1);
const { subcommand, args } = getSubcommand(subArgs, COMMAND_CONFIG); const { subcommand, args } = getSubcommand(subArgs, COMMAND_CONFIG);
const { output, config } = client; const { output, config } = client;
const target = argv['--environment']?.toLowerCase() || 'development';
if (!isValidEnvTarget(target)) {
output.error(
`Invalid environment \`${chalk.cyan(
target
)}\`. Valid options: ${getEnvTargetPlaceholder()}`
);
return 1;
}
const link = await getLinkedProject(client, cwd); const link = await getLinkedProject(client, cwd);
if (link.status === 'error') { if (link.status === 'error') {
return link.exitCode; return link.exitCode;
@@ -150,7 +165,7 @@ export default async function main(client: Client) {
return pull( return pull(
client, client,
project, project,
ProjectEnvTarget.Development, target,
argv, argv,
args, args,
output, output,

View File

@@ -14,6 +14,11 @@ import param from '../../util/output/param';
import stamp from '../../util/output/stamp'; import stamp from '../../util/output/stamp';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import { EnvRecordsSource } from '../../util/env/get-env-records'; import { EnvRecordsSource } from '../../util/env/get-env-records';
import {
buildDeltaString,
createEnvObject,
} from '../../util/env/diff-env-files';
import { isErrnoException } from '../../util/is-error';
const CONTENTS_PREFIX = '# Created by Vercel CLI\n'; const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
@@ -36,8 +41,8 @@ function readHeadSync(path: string, length: number) {
function tryReadHeadSync(path: string, length: number) { function tryReadHeadSync(path: string, length: number) {
try { try {
return readHeadSync(path, length); return readHeadSync(path, length);
} catch (err) { } catch (err: unknown) {
if (err.code !== 'ENOENT') { if (!isErrnoException(err) || err.code !== 'ENOENT') {
throw err; throw err;
} }
} }
@@ -69,7 +74,7 @@ export default async function pull(
const exists = typeof head !== 'undefined'; const exists = typeof head !== 'undefined';
if (head === CONTENTS_PREFIX) { if (head === CONTENTS_PREFIX) {
output.print(`Overwriting existing ${chalk.bold(filename)} file\n`); output.log(`Overwriting existing ${chalk.bold(filename)} file`);
} else if ( } else if (
exists && exists &&
!skipConfirmation && !skipConfirmation &&
@@ -83,10 +88,10 @@ export default async function pull(
return 0; return 0;
} }
output.print( output.log(
`Downloading "${environment}" Environment Variables for Project ${chalk.bold( `Downloading \`${chalk.cyan(
project.name environment
)}\n` )}\` Environment Variables for Project ${chalk.bold(project.name)}`
); );
const pullStamp = stamp(); const pullStamp = stamp();
@@ -107,6 +112,15 @@ export default async function pull(
environment environment
); );
let deltaString = '';
let oldEnv;
if (exists) {
oldEnv = await createEnvObject(fullPath, output);
if (oldEnv) {
deltaString = buildDeltaString(oldEnv, records);
}
}
const contents = const contents =
CONTENTS_PREFIX + CONTENTS_PREFIX +
Object.entries(records) Object.entries(records)
@@ -125,6 +139,13 @@ export default async function pull(
)}\n` )}\n`
); );
output.print('\n');
if (deltaString) {
output.print(deltaString);
} else if (oldEnv && exists) {
output.log('No changes found.');
}
return 0; return 0;
} }

View File

@@ -16,6 +16,7 @@ import param from '../../util/output/param';
import { emoji, prependEmoji } from '../../util/emoji'; import { emoji, prependEmoji } from '../../util/emoji';
import { isKnownError } from '../../util/env/known-error'; import { isKnownError } from '../../util/env/known-error';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import { isAPIError } from '../../util/errors-ts';
type Options = { type Options = {
'--debug': boolean; '--debug': boolean;
@@ -120,12 +121,12 @@ export default async function rm(
try { try {
output.spinner('Removing'); output.spinner('Removing');
await removeEnvRecord(output, client, project.id, env); await removeEnvRecord(output, client, project.id, env);
} catch (error) { } catch (err: unknown) {
if (isKnownError(error) && error.serverMessage) { if (isAPIError(err) && isKnownError(err)) {
output.error(error.serverMessage); output.error(err.serverMessage);
return 1; return 1;
} }
throw error; throw err;
} }
output.print( output.print(

View File

@@ -2,8 +2,9 @@ import chalk from 'chalk';
import { join } from 'path'; import { join } from 'path';
import { Org, Project } from '../../types'; import { Org, Project } from '../../types';
import Client from '../../util/client'; import Client from '../../util/client';
import { parseGitConfig, pluckRemoteUrl } from '../../util/create-git-meta'; import { parseGitConfig, pluckRemoteUrls } from '../../util/create-git-meta';
import confirm from '../../util/input/confirm'; import confirm from '../../util/input/confirm';
import list, { ListChoice } from '../../util/input/list';
import { Output } from '../../util/output'; import { Output } from '../../util/output';
import link from '../../util/output/link'; import link from '../../util/output/link';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
@@ -64,20 +65,37 @@ export default async function connect(
); );
return 1; return 1;
} }
const remoteUrl = pluckRemoteUrl(gitConfig); const remoteUrls = pluckRemoteUrls(gitConfig);
if (!remoteUrl) { if (!remoteUrls) {
output.error( output.error(
`No remote origin URL found in your Git config. Make sure you've configured a remote repo in your local Git config. Run ${chalk.cyan( `No remote URLs found in your Git config. Make sure you've configured a remote repo in your local Git config. Run ${chalk.cyan(
'`git remote --help`' '`git remote --help`'
)} for more details.` )} for more details.`
); );
return 1; return 1;
} }
output.log(`Identified Git remote "origin": ${link(remoteUrl)}`);
let remoteUrl: string;
if (Object.keys(remoteUrls).length > 1) {
output.log(`Found multiple remote URLs.`);
remoteUrl = await selectRemoteUrl(client, remoteUrls);
} else {
// If only one is found, get it — usually "origin"
remoteUrl = Object.values(remoteUrls)[0];
}
if (remoteUrl === '') {
output.log('Aborted.');
return 0;
}
output.log(`Connecting Git remote: ${link(remoteUrl)}`);
const parsedUrl = parseRepoUrl(remoteUrl); const parsedUrl = parseRepoUrl(remoteUrl);
if (!parsedUrl) { if (!parsedUrl) {
output.error( output.error(
`Failed to parse Git repo data from the following remote URL in your Git config: ${link( `Failed to parse Git repo data from the following remote URL: ${link(
remoteUrl remoteUrl
)}` )}`
); );
@@ -166,3 +184,22 @@ async function confirmRepoConnect(
} }
return shouldReplaceProject; return shouldReplaceProject;
} }
async function selectRemoteUrl(
client: Client,
remoteUrls: { [key: string]: string }
): Promise<string> {
let choices: ListChoice[] = [];
for (const [urlKey, urlValue] of Object.entries(remoteUrls)) {
choices.push({
name: `${urlValue} ${chalk.gray(`(${urlKey})`)}`,
value: urlValue,
short: urlKey,
});
}
return await list(client, {
message: 'Which remote do you want to connect?',
choices,
});
}

View File

@@ -7,6 +7,7 @@ import handleError from '../../util/handle-error';
import logo from '../../util/output/logo'; import logo from '../../util/output/logo';
import init from './init'; import init from './init';
import { getPkgName } from '../../util/pkg-name'; import { getPkgName } from '../../util/pkg-name';
import { isError } from '../../util/is-error';
const COMMAND_CONFIG = { const COMMAND_CONFIG = {
init: ['init'], init: ['init'],
@@ -70,9 +71,11 @@ export default async function main(client: Client) {
try { try {
return await init(client, argv, args); return await init(client, argv, args);
} catch (err) { } catch (err: unknown) {
output.prettyError(err); output.prettyError(err);
output.debug(err.stack); if (isError(err) && typeof err.stack === 'string') {
output.debug(err.stack);
}
return 1; return 1;
} }
} }

View File

@@ -12,6 +12,9 @@ import Client from '../util/client';
import { getDeployment } from '../util/get-deployment'; import { getDeployment } from '../util/get-deployment';
import { Deployment } from '@vercel/client'; import { Deployment } from '@vercel/client';
import { Build } from '../types'; import { Build } from '../types';
import title from 'title';
import { isErrnoException } from '../util/is-error';
import { isAPIError } from '../util/errors-ts';
const help = () => { const help = () => {
console.log(` console.log(`
@@ -75,8 +78,11 @@ export default async function main(client: Client) {
try { try {
({ contextName } = await getScope(client)); ({ contextName } = await getScope(client));
} catch (err) { } catch (err: unknown) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') { if (
isErrnoException(err) &&
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
) {
error(err.message); error(err.message);
return 1; return 1;
} }
@@ -92,28 +98,38 @@ export default async function main(client: Client) {
try { try {
deployment = await getDeployment(client, deploymentIdOrHost); deployment = await getDeployment(client, deploymentIdOrHost);
} catch (err) { } catch (err: unknown) {
if (err.status === 404) { if (isAPIError(err)) {
error( if (err.status === 404) {
`Failed to find deployment "${deploymentIdOrHost}" in ${chalk.bold( error(
contextName `Failed to find deployment "${deploymentIdOrHost}" in ${chalk.bold(
)}` contextName
); )}`
return 1; );
} return 1;
if (err.status === 403) { }
error( if (err.status === 403) {
`No permission to access deployment "${deploymentIdOrHost}" in ${chalk.bold( error(
contextName `No permission to access deployment "${deploymentIdOrHost}" in ${chalk.bold(
)}` contextName
); )}`
return 1; );
return 1;
}
} }
// unexpected // unexpected
throw err; throw err;
} }
const { id, name, url, createdAt, routes, readyState } = deployment; const {
id,
name,
url,
createdAt,
routes,
readyState,
alias: aliases,
} = deployment;
const { builds } = const { builds } =
deployment.version === 2 deployment.version === 2
@@ -121,20 +137,20 @@ export default async function main(client: Client) {
: { builds: [] }; : { builds: [] };
log( log(
`Fetched deployment "${url}" in ${chalk.bold(contextName)} ${elapsed( `Fetched deployment ${chalk.bold(url)} in ${chalk.bold(
Date.now() - depFetchStart contextName
)}` )} ${elapsed(Date.now() - depFetchStart)}`
); );
print('\n'); print('\n');
print(chalk.bold(' General\n\n')); print(chalk.bold(' General\n\n'));
print(` ${chalk.cyan('id')}\t\t${id}\n`); print(` ${chalk.cyan('id')}\t\t${id}\n`);
print(` ${chalk.cyan('name')}\t${name}\n`); print(` ${chalk.cyan('name')}\t${name}\n`);
print(` ${chalk.cyan('readyState')}\t${stateString(readyState)}\n`); print(` ${chalk.cyan('status')}\t${stateString(readyState)}\n`);
print(` ${chalk.cyan('url')}\t\t${url}\n`); print(` ${chalk.cyan('url')}\t\thttps://${url}\n`);
if (createdAt) { if (createdAt) {
print( print(
` ${chalk.cyan('createdAt')}\t${new Date(createdAt)} ${elapsed( ` ${chalk.cyan('created')}\t${new Date(createdAt)} ${elapsed(
Date.now() - createdAt, Date.now() - createdAt,
true true
)}\n` )}\n`
@@ -142,6 +158,16 @@ export default async function main(client: Client) {
} }
print('\n\n'); print('\n\n');
if (aliases.length > 0) {
print(chalk.bold(' Aliases\n\n'));
let aliasList = '';
for (const alias of aliases) {
aliasList += `${chalk.gray('╶')} https://${alias}\n`;
}
print(indent(aliasList, 4));
print('\n\n');
}
if (builds.length > 0) { if (builds.length > 0) {
const times: { [id: string]: string | null } = {}; const times: { [id: string]: string | null } = {};
@@ -165,19 +191,24 @@ export default async function main(client: Client) {
return 0; return 0;
} }
// renders the state string
function stateString(s: Deployment['readyState']) { function stateString(s: Deployment['readyState']) {
const CIRCLE = '● ';
const sTitle = s && title(s);
switch (s) { switch (s) {
case 'INITIALIZING': case 'INITIALIZING':
return chalk.yellow(s); case 'BUILDING':
case 'DEPLOYING':
case 'ANALYZING':
return chalk.yellow(CIRCLE) + sTitle;
case 'ERROR': case 'ERROR':
return chalk.red(s); return chalk.red(CIRCLE) + sTitle;
case 'READY': case 'READY':
return s; return chalk.green(CIRCLE) + sTitle;
case 'QUEUED':
return chalk.gray(CIRCLE) + sTitle;
case 'CANCELED':
return chalk.gray(CIRCLE) + sTitle;
default: default:
return chalk.gray(s || 'UNKNOWN'); return chalk.gray('UNKNOWN');
} }
} }

View File

@@ -19,6 +19,7 @@ import validatePaths from '../util/validate-paths';
import { getLinkedProject } from '../util/projects/link'; import { getLinkedProject } from '../util/projects/link';
import { ensureLink } from '../util/ensure-link'; import { ensureLink } from '../util/ensure-link';
import getScope from '../util/get-scope'; import getScope from '../util/get-scope';
import { isAPIError } from '../util/errors-ts';
const help = () => { const help = () => {
console.log(` console.log(`
@@ -152,16 +153,7 @@ export default async function main(client: Client) {
const { currentTeam } = config; const { currentTeam } = config;
try { ({ contextName } = await getScope(client));
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
error(err.message);
return 1;
}
throw err;
}
const nextTimestamp = argv['--next']; const nextTimestamp = argv['--next'];
@@ -228,8 +220,8 @@ export default async function main(client: Client) {
try { try {
await now.findDeployment(app); await now.findDeployment(app);
} catch (err) { } catch (err: unknown) {
if (err.status === 404) { if (isAPIError(err) && err.status === 404) {
debug('Ignore findDeployment 404'); debug('Ignore findDeployment 404');
} else { } else {
throw err; throw err;

View File

@@ -5,6 +5,8 @@ import { writeToConfigFile, writeToAuthConfigFile } from '../util/config/files';
import getArgs from '../util/get-args'; import getArgs from '../util/get-args';
import Client from '../util/client'; import Client from '../util/client';
import { getCommandName, getPkgName } from '../util/pkg-name'; import { getCommandName, getPkgName } from '../util/pkg-name';
import { isAPIError } from '../util/errors-ts';
import { errorToString } from '../util/is-error';
const help = () => { const help = () => {
console.log(` console.log(`
@@ -63,12 +65,14 @@ export default async function main(client: Client): Promise<number> {
method: 'DELETE', method: 'DELETE',
useCurrentTeam: false, useCurrentTeam: false,
}); });
} catch (err) { } catch (err: unknown) {
if (err.status === 403) { if (isAPIError(err)) {
output.debug('Token is invalid so it cannot be revoked'); if (err.status === 403) {
} else if (err.status !== 200) { output.debug('Token is invalid so it cannot be revoked');
output.debug(err?.message ?? ''); } else if (err.status !== 200) {
exitCode = 1; output.debug(err?.message ?? '');
exitCode = 1;
}
} }
} }
@@ -86,8 +90,8 @@ export default async function main(client: Client): Promise<number> {
writeToConfigFile(config); writeToConfigFile(config);
writeToAuthConfigFile(authConfig); writeToAuthConfigFile(authConfig);
output.debug('Configuration has been deleted'); output.debug('Configuration has been deleted');
} catch (err) { } catch (err: unknown) {
output.debug(err?.message ?? ''); output.debug(errorToString(err));
exitCode = 1; exitCode = 1;
} }

View File

@@ -8,6 +8,7 @@ import { getPkgName } from '../util/pkg-name';
import getArgs from '../util/get-args'; import getArgs from '../util/get-args';
import Client from '../util/client'; import Client from '../util/client';
import { getDeployment } from '../util/get-deployment'; import { getDeployment } from '../util/get-deployment';
import { isAPIError } from '../util/errors-ts';
const help = () => { const help = () => {
console.log(` console.log(`
@@ -125,22 +126,24 @@ export default async function main(client: Client) {
let deployment; let deployment;
try { try {
deployment = await getDeployment(client, id); deployment = await getDeployment(client, id);
} catch (err) { } catch (err: unknown) {
output.stopSpinner(); output.stopSpinner();
if (err.status === 404) { if (isAPIError(err)) {
output.error( if (err.status === 404) {
`Failed to find deployment "${id}" in ${chalk.bold(contextName)}` output.error(
); `Failed to find deployment "${id}" in ${chalk.bold(contextName)}`
return 1; );
} return 1;
if (err.status === 403) { }
output.error( if (err.status === 403) {
`No permission to access deployment "${id}" in ${chalk.bold( output.error(
contextName `No permission to access deployment "${id}" in ${chalk.bold(
)}` contextName
); )}`
return 1; );
return 1;
}
} }
// unexpected // unexpected
throw err; throw err;

View File

@@ -1,6 +1,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import ms from 'ms'; import ms from 'ms';
import Client from '../../util/client'; import Client from '../../util/client';
import { isAPIError } from '../../util/errors-ts';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
export default async function add( export default async function add(
@@ -36,12 +37,12 @@ export default async function add(
method: 'POST', method: 'POST',
body: { name }, body: { name },
}); });
} catch (error) { } catch (err: unknown) {
if (error.status === 409) { if (isAPIError(err) && err.status === 409) {
// project already exists, so we can // project already exists, so we can
// show a success message // show a success message
} else { } else {
throw error; throw err;
} }
} }
const elapsed = ms(Date.now() - start); const elapsed = ms(Date.now() - start);

View File

@@ -72,18 +72,7 @@ export default async function main(client: Client) {
subcommand = argv._[0] || 'list'; subcommand = argv._[0] || 'list';
const args = argv._.slice(1); const args = argv._.slice(1);
const { output } = client; const { output } = client;
const { contextName } = await getScope(client);
let contextName = '';
try {
({ contextName } = await getScope(client));
} catch (error) {
if (error.code === 'NOT_AUTHORIZED' || error.code === 'TEAM_DELETED') {
output.error(error.message);
return 1;
}
throw error;
}
switch (subcommand) { switch (subcommand) {
case 'ls': case 'ls':

View File

@@ -2,6 +2,7 @@ import chalk from 'chalk';
import ms from 'ms'; import ms from 'ms';
import Client from '../../util/client'; import Client from '../../util/client';
import { emoji, prependEmoji } from '../../util/emoji'; import { emoji, prependEmoji } from '../../util/emoji';
import { isAPIError } from '../../util/errors-ts';
import confirm from '../../util/input/confirm'; import confirm from '../../util/input/confirm';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
@@ -32,8 +33,8 @@ export default async function rm(client: Client, args: string[]) {
await client.fetch(`/v2/projects/${e(name)}`, { await client.fetch(`/v2/projects/${e(name)}`, {
method: 'DELETE', method: 'DELETE',
}); });
} catch (err) { } catch (err: unknown) {
if (err.status === 404) { if (isAPIError(err) && err.status === 404) {
client.output.error('No such project exists'); client.output.error('No such project exists');
return 1; return 1;
} }

View File

@@ -179,6 +179,8 @@ export default async function main(client: Client) {
return pullResultCode; return pullResultCode;
} }
client.output.print('\n');
client.output.log('Downloading project settings');
await writeProjectSettings(cwd, project, org); await writeProjectSettings(cwd, project, org);
const settingsStamp = stamp(); const settingsStamp = stamp();

View File

@@ -114,18 +114,7 @@ export default async function main(client: Client) {
return 1; return 1;
} }
let contextName: string | null = null; const { contextName } = await getScope(client);
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
output.spinner( output.spinner(
`Fetching deployment(s) ${ids `Fetching deployment(s) ${ids

View File

@@ -11,6 +11,7 @@ import { getPkgName, getCommandName } from '../../util/pkg-name';
import Client from '../../util/client'; import Client from '../../util/client';
import createTeam from '../../util/teams/create-team'; import createTeam from '../../util/teams/create-team';
import patchTeam from '../../util/teams/patch-team'; import patchTeam from '../../util/teams/patch-team';
import { errorToString, isError } from '../../util/is-error';
const validateSlugKeypress = (data: string, value: string) => const validateSlugKeypress = (data: string, value: string) =>
// TODO: the `value` here should contain the current value + the keypress // TODO: the `value` here should contain the current value + the keypress
@@ -56,8 +57,8 @@ export default async function add(client: Client): Promise<number> {
valid: team, valid: team,
forceLowerCase: true, forceLowerCase: true,
}); });
} catch (err) { } catch (err: unknown) {
if (err.message === 'USER_ABORT') { if (isError(err) && err.message === 'USER_ABORT') {
output.log('Aborted'); output.log('Aborted');
return 0; return 0;
} }
@@ -71,10 +72,10 @@ export default async function add(client: Client): Promise<number> {
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
team = await createTeam(client, { slug }); team = await createTeam(client, { slug });
} catch (err) { } catch (err: unknown) {
output.stopSpinner(); output.stopSpinner();
output.print(eraseLines(2)); output.print(eraseLines(2));
output.error(err.message); output.error(errorToString(err));
} }
} while (!team); } while (!team);
@@ -92,8 +93,8 @@ export default async function add(client: Client): Promise<number> {
label: `- ${teamNamePrefix}`, label: `- ${teamNamePrefix}`,
validateKeypress: validateNameKeypress, validateKeypress: validateNameKeypress,
}); });
} catch (err) { } catch (err: unknown) {
if (err.message === 'USER_ABORT') { if (isError(err) && err.message === 'USER_ABORT') {
console.log(info('No name specified')); console.log(info('No name specified'));
return gracefulExit(); return gracefulExit();
} }

View File

@@ -11,6 +11,8 @@ import { getCommandName } from '../../util/pkg-name';
import { email as regexEmail } from '../../util/input/regexes'; import { email as regexEmail } from '../../util/input/regexes';
import getTeams from '../../util/teams/get-teams'; import getTeams from '../../util/teams/get-teams';
import inviteUserToTeam from '../../util/teams/invite-user-to-team'; import inviteUserToTeam from '../../util/teams/invite-user-to-team';
import { isAPIError } from '../../util/errors-ts';
import { errorToString, isError } from '../../util/is-error';
const validateEmail = (data: string) => const validateEmail = (data: string) =>
regexEmail.test(data.trim()) || data.length === 0; regexEmail.test(data.trim()) || data.length === 0;
@@ -67,17 +69,7 @@ export default async function invite(
const currentTeam = teams.find(team => team.id === currentTeamId); const currentTeam = teams.find(team => team.id === currentTeamId);
output.spinner('Fetching user information'); output.spinner('Fetching user information');
let user; const user = await getUser(client);
try {
user = await getUser(client);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
domains.push(user.email.split('@')[1]); domains.push(user.email.split('@')[1]);
@@ -107,8 +99,8 @@ export default async function invite(
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const res = await inviteUserToTeam(client, currentTeam.id, email); const res = await inviteUserToTeam(client, currentTeam.id, email);
userInfo = res.username; userInfo = res.username;
} catch (err) { } catch (err: unknown) {
if (err.code === 'user_not_found') { if (isAPIError(err) && err.code === 'user_not_found') {
output.error(`No user exists with the email address "${email}".`); output.error(`No user exists with the email address "${email}".`);
return 1; return 1;
} }
@@ -141,8 +133,8 @@ export default async function invite(
validateValue: validateEmail, validateValue: validateEmail,
autoComplete: value => emailAutoComplete(value, currentTeam.slug), autoComplete: value => emailAutoComplete(value, currentTeam.slug),
}); });
} catch (err) { } catch (err: unknown) {
if (err.message !== 'USER_ABORT') { if (!isError(err) || err.message !== 'USER_ABORT') {
throw err; throw err;
} }
} }
@@ -174,7 +166,7 @@ export default async function invite(
} catch (err) { } catch (err) {
output.stopSpinner(); output.stopSpinner();
process.stderr.write(eraseLines(emails.length + 2)); process.stderr.write(eraseLines(emails.length + 2));
output.error(err.message); output.error(errorToString(err));
hasError = true; hasError = true;
for (const email of emails) { for (const email of emails) {
output.log(`${chalk.cyan(chars.tick)} ${sentEmailPrefix}${email}`); output.log(`${chalk.cyan(chars.tick)} ${sentEmailPrefix}${email}`);

View File

@@ -43,17 +43,7 @@ export default async function list(client: Client): Promise<number> {
const accountIsCurrent = !currentTeam; const accountIsCurrent = !currentTeam;
output.spinner('Fetching user information'); output.spinner('Fetching user information');
let user; const user = await getUser(client);
try {
user = await getUser(client);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (accountIsCurrent) { if (accountIsCurrent) {
currentTeam = user.id; currentTeam = user.id;

View File

@@ -41,18 +41,7 @@ export default async (client: Client): Promise<number> => {
return 2; return 2;
} }
let contextName = null; const { contextName } = await getScope(client, { getTeam: false });
try {
({ contextName } = await getScope(client, { getTeam: false }));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
if (client.stdout.isTTY) { if (client.stdout.isTTY) {
output.log(contextName); output.log(contextName);

View File

@@ -1,11 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
import { isErrnoException, isError, errorToString } from './util/is-error';
try { try {
// Test to see if cwd has been deleted before // Test to see if cwd has been deleted before
// importing 3rd party packages that might need cwd. // importing 3rd party packages that might need cwd.
process.cwd(); process.cwd();
} catch (e) { } catch (err) {
if (e && e.message && e.message.includes('uv_cwd')) { if (isError(err) && err.message.includes('uv_cwd')) {
console.error('Error! The current working directory does not exist.'); console.error('Error! The current working directory does not exist.');
process.exit(1); process.exit(1);
} }
@@ -103,7 +104,7 @@ const main = async () => {
}, },
{ permissive: true } { permissive: true }
); );
} catch (err) { } catch (err: unknown) {
handleError(err); handleError(err);
return 1; return 1;
} }
@@ -201,14 +202,11 @@ const main = async () => {
// Ensure that the Vercel global configuration directory exists // Ensure that the Vercel global configuration directory exists
try { try {
await mkdirp(VERCEL_DIR); await mkdirp(VERCEL_DIR);
} catch (err) { } catch (err: unknown) {
console.error( output.error(
error( `An unexpected error occurred while trying to create the global directory "${hp(
`${ VERCEL_DIR
'An unexpected error occurred while trying to create the ' + )}" ${errorToString(err)}`
`global directory "${hp(VERCEL_DIR)}" `
}${err.message}`
)
); );
} }
@@ -217,13 +215,13 @@ const main = async () => {
try { try {
configExists = existsSync(VERCEL_CONFIG_PATH); configExists = existsSync(VERCEL_CONFIG_PATH);
} catch (err) { } catch (err: unknown) {
console.error( console.error(
error( error(
`${ `${
'An unexpected error occurred while trying to find the ' + 'An unexpected error occurred while trying to find the ' +
`config file "${hp(VERCEL_CONFIG_PATH)}" ` `config file "${hp(VERCEL_CONFIG_PATH)}" `
}${err.message}` }${errorToString(err)}`
) )
); );
@@ -241,7 +239,7 @@ const main = async () => {
`${ `${
'An unexpected error occurred while trying to read the ' + 'An unexpected error occurred while trying to read the ' +
`config in "${hp(VERCEL_CONFIG_PATH)}" ` `config in "${hp(VERCEL_CONFIG_PATH)}" `
}${err.message}` }${errorToString(err)}`
) )
); );
@@ -272,13 +270,13 @@ const main = async () => {
try { try {
configFiles.writeToConfigFile(config); configFiles.writeToConfigFile(config);
} catch (err) { } catch (err: unknown) {
console.error( console.error(
error( error(
`${ `${
'An unexpected error occurred while trying to write the ' + 'An unexpected error occurred while trying to write the ' +
`default config to "${hp(VERCEL_CONFIG_PATH)}" ` `default config to "${hp(VERCEL_CONFIG_PATH)}" `
}${err.message}` }${errorToString(err)}`
) )
); );
@@ -290,13 +288,13 @@ const main = async () => {
try { try {
authConfigExists = existsSync(VERCEL_AUTH_CONFIG_PATH); authConfigExists = existsSync(VERCEL_AUTH_CONFIG_PATH);
} catch (err) { } catch (err: unknown) {
console.error( console.error(
error( error(
`${ `${
'An unexpected error occurred while trying to find the ' + 'An unexpected error occurred while trying to find the ' +
`auth file "${hp(VERCEL_AUTH_CONFIG_PATH)}" ` `auth file "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
}${err.message}` }${errorToString(err)}`
) )
); );
@@ -317,13 +315,13 @@ const main = async () => {
if (authConfigExists) { if (authConfigExists) {
try { try {
authConfig = configFiles.readAuthConfigFile(); authConfig = configFiles.readAuthConfigFile();
} catch (err) { } catch (err: unknown) {
console.error( console.error(
error( error(
`${ `${
'An unexpected error occurred while trying to read the ' + 'An unexpected error occurred while trying to read the ' +
`auth config in "${hp(VERCEL_AUTH_CONFIG_PATH)}" ` `auth config in "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
}${err.message}` }${errorToString(err)}`
) )
); );
@@ -345,13 +343,13 @@ const main = async () => {
try { try {
configFiles.writeToAuthConfigFile(authConfig); configFiles.writeToAuthConfigFile(authConfig);
} catch (err) { } catch (err: unknown) {
console.error( console.error(
error( error(
`${ `${
'An unexpected error occurred while trying to write the ' + 'An unexpected error occurred while trying to write the ' +
`default config to "${hp(VERCEL_AUTH_CONFIG_PATH)}" ` `default config to "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
}${err.message}` }${errorToString(err)}`
) )
); );
return 1; return 1;
@@ -543,8 +541,8 @@ const main = async () => {
try { try {
user = await getUser(client); user = await getUser(client);
} catch (err) { } catch (err: unknown) {
if (err.code === 'NOT_AUTHORIZED') { if (isErrnoException(err) && err.code === 'NOT_AUTHORIZED') {
output.prettyError({ output.prettyError({
message: `You do not have access to the specified account`, message: `You do not have access to the specified account`,
link: 'https://err.sh/vercel/scope-not-accessible', link: 'https://err.sh/vercel/scope-not-accessible',
@@ -564,8 +562,8 @@ const main = async () => {
try { try {
teams = await getTeams(client); teams = await getTeams(client);
} catch (err) { } catch (err: unknown) {
if (err.code === 'not_authorized') { if (isErrnoException(err) && err.code === 'not_authorized') {
output.prettyError({ output.prettyError({
message: `You do not have access to the specified team`, message: `You do not have access to the specified team`,
link: 'https://err.sh/vercel/scope-not-accessible', link: 'https://err.sh/vercel/scope-not-accessible',
@@ -594,8 +592,8 @@ const main = async () => {
} }
} }
const metric = metrics();
let exitCode; let exitCode;
let metric: ReturnType<typeof metrics> | undefined;
const eventCategory = 'Exit Code'; const eventCategory = 'Exit Code';
try { try {
@@ -698,13 +696,14 @@ const main = async () => {
if (shouldCollectMetrics) { if (shouldCollectMetrics) {
const category = 'Command Invocation'; const category = 'Command Invocation';
if (!metric) metric = metrics();
metric metric
.timing(category, targetCommand, end, pkg.version) .timing(category, targetCommand, end, pkg.version)
.event(category, targetCommand, pkg.version) .event(category, targetCommand, pkg.version)
.send(); .send();
} }
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOTFOUND') { if (isErrnoException(err) && err.code === 'ENOTFOUND') {
// Error message will look like the following: // Error message will look like the following:
// "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com" // "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || ''); const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
@@ -716,11 +715,16 @@ const main = async () => {
)} could not be resolved. Please verify your internet connectivity and DNS configuration.` )} could not be resolved. Please verify your internet connectivity and DNS configuration.`
); );
} }
output.debug(err.stack); if (typeof err.stack === 'string') {
output.debug(err.stack);
}
return 1; return 1;
} }
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') { if (
isErrnoException(err) &&
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
) {
output.prettyError(err); output.prettyError(err);
return 1; return 1;
} }
@@ -732,9 +736,10 @@ const main = async () => {
} }
if (shouldCollectMetrics) { if (shouldCollectMetrics) {
if (!metric) metric = metrics();
metric metric
.event(eventCategory, '1', pkg.version) .event(eventCategory, '1', pkg.version)
.exception(err.message) .exception(errorToString(err))
.send(); .send();
} }
@@ -742,23 +747,24 @@ const main = async () => {
// but instead show the message. Any error that is handled by this should // but instead show the message. Any error that is handled by this should
// actually be handled in the sub command instead. Please make sure // actually be handled in the sub command instead. Please make sure
// that happens for anything that lands here. It should NOT bubble up to here. // that happens for anything that lands here. It should NOT bubble up to here.
if (err.code) { if (isErrnoException(err)) {
output.debug(err.stack); if (typeof err.stack === 'string') {
output.debug(err.stack);
}
output.prettyError(err); output.prettyError(err);
} else { } else {
await reportError(Sentry, client, err); await reportError(Sentry, client, err);
// Otherwise it is an unexpected error and we should show the trace // Otherwise it is an unexpected error and we should show the trace
// and an unexpected error message // and an unexpected error message
output.error( output.error(`An unexpected error occurred in ${subcommand}: ${err}`);
`An unexpected error occurred in ${subcommand}: ${err.stack}`
);
} }
return 1; return 1;
} }
if (shouldCollectMetrics) { if (shouldCollectMetrics) {
if (!metric) metric = metrics();
metric.event(eventCategory, `${exitCode}`, pkg.version).send(); metric.event(eventCategory, `${exitCode}`, pkg.version).send();
} }

View File

@@ -271,6 +271,12 @@ export interface ProjectLinkData {
export interface Project extends ProjectSettings { export interface Project extends ProjectSettings {
id: string; id: string;
analytics?: {
id: string;
enabledAt?: number;
disabledAt?: number;
canceledAt?: number | null;
};
name: string; name: string;
accountId: string; accountId: string;
updatedAt: number; updatedAt: number;

View File

@@ -68,37 +68,39 @@ async function performCreateAlias(
body: { alias }, body: { alias },
} }
); );
} catch (error) { } catch (err: unknown) {
if (error.code === 'cert_missing' || error.code === 'cert_expired') { if (ERRORS.isAPIError(err)) {
return new ERRORS.CertMissing(alias); if (err.code === 'cert_missing' || err.code === 'cert_expired') {
} return new ERRORS.CertMissing(alias);
if (error.status === 409) {
return { uid: error.uid, alias: error.alias } as AliasRecord;
}
if (error.code === 'deployment_not_found') {
return new ERRORS.DeploymentNotFound({
context: contextName,
id: deployment.uid,
});
}
if (error.code === 'gone') {
return new ERRORS.DeploymentFailedAliasImpossible();
}
if (error.code === 'invalid_alias') {
return new ERRORS.InvalidAlias(alias);
}
if (error.status === 403) {
if (error.code === 'alias_in_use') {
return new ERRORS.AliasInUse(alias);
} }
if (error.code === 'forbidden') { if (err.status === 409) {
return new ERRORS.DomainPermissionDenied(alias, contextName); return { uid: err.uid, alias: err.alias } as AliasRecord;
}
if (err.code === 'deployment_not_found') {
return new ERRORS.DeploymentNotFound({
context: contextName,
id: deployment.uid,
});
}
if (err.code === 'gone') {
return new ERRORS.DeploymentFailedAliasImpossible();
}
if (err.code === 'invalid_alias') {
return new ERRORS.InvalidAlias(alias);
}
if (err.status === 403) {
if (err.code === 'alias_in_use') {
return new ERRORS.AliasInUse(alias);
}
if (err.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(alias, contextName);
}
}
if (err.status === 400) {
return new ERRORS.DeploymentNotReady({ url: deployment.url });
} }
}
if (error.status === 400) {
return new ERRORS.DeploymentNotReady({ url: deployment.url });
} }
throw error; throw err;
} }
} }

View File

@@ -1,6 +1,14 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import { basename, dirname, extname, join, relative, resolve } from 'path'; import {
basename,
dirname,
extname,
join,
relative,
resolve,
posix,
} from 'path';
import { import {
Builder, Builder,
BuildResultV2, BuildResultV2,
@@ -15,30 +23,47 @@ import {
download, download,
EdgeFunction, EdgeFunction,
BuildResultBuildOutput, BuildResultBuildOutput,
getLambdaOptionsFromFunction,
normalizePath,
} from '@vercel/build-utils'; } from '@vercel/build-utils';
import pipe from 'promisepipe'; import pipe from 'promisepipe';
import { unzip } from './unzip'; import { unzip } from './unzip';
import { VERCEL_DIR } from '../projects/link'; import { VERCEL_DIR } from '../projects/link';
import { VercelConfig } from '@vercel/client';
const { normalize } = posix;
export const OUTPUT_DIR = join(VERCEL_DIR, 'output'); export const OUTPUT_DIR = join(VERCEL_DIR, 'output');
/**
* An entry in the "functions" object in `vercel.json`.
*/
interface FunctionConfiguration {
memory?: number;
maxDuration?: number;
}
export async function writeBuildResult( export async function writeBuildResult(
outputDir: string, outputDir: string,
buildResult: BuildResultV2 | BuildResultV3, buildResult: BuildResultV2 | BuildResultV3,
build: Builder, build: Builder,
builder: BuilderV2 | BuilderV3, builder: BuilderV2 | BuilderV3,
builderPkg: PackageJson, builderPkg: PackageJson,
cleanUrls?: boolean vercelConfig: VercelConfig | null
) { ) {
const { version } = builder; const { version } = builder;
if (typeof version !== 'number' || version === 2) { if (typeof version !== 'number' || version === 2) {
return writeBuildResultV2( return writeBuildResultV2(
outputDir, outputDir,
buildResult as BuildResultV2, buildResult as BuildResultV2,
cleanUrls vercelConfig
); );
} else if (version === 3) { } else if (version === 3) {
return writeBuildResultV3(outputDir, buildResult as BuildResultV3, build); return writeBuildResultV3(
outputDir,
buildResult as BuildResultV3,
build,
vercelConfig
);
} }
throw new Error( throw new Error(
`Unsupported Builder version \`${version}\` from "${builderPkg.name}"` `Unsupported Builder version \`${version}\` from "${builderPkg.name}"`
@@ -67,6 +92,13 @@ export interface PathOverride {
path?: string; path?: string;
} }
/**
* Remove duplicate slashes as well as leading/trailing slashes.
*/
function stripDuplicateSlashes(path: string): string {
return normalize(path).replace(/(^\/|\/$)/g, '');
}
/** /**
* Writes the output from the `build()` return value of a v2 Builder to * Writes the output from the `build()` return value of a v2 Builder to
* the filesystem. * the filesystem.
@@ -74,7 +106,7 @@ export interface PathOverride {
async function writeBuildResultV2( async function writeBuildResultV2(
outputDir: string, outputDir: string,
buildResult: BuildResultV2, buildResult: BuildResultV2,
cleanUrls?: boolean vercelConfig: VercelConfig | null
) { ) {
if ('buildOutputPath' in buildResult) { if ('buildOutputPath' in buildResult) {
await mergeBuilderOutput(outputDir, buildResult); await mergeBuilderOutput(outputDir, buildResult);
@@ -84,16 +116,23 @@ async function writeBuildResultV2(
const lambdas = new Map<Lambda, string>(); const lambdas = new Map<Lambda, string>();
const overrides: Record<string, PathOverride> = {}; const overrides: Record<string, PathOverride> = {};
for (const [path, output] of Object.entries(buildResult.output)) { for (const [path, output] of Object.entries(buildResult.output)) {
const normalizedPath = stripDuplicateSlashes(path);
if (isLambda(output)) { if (isLambda(output)) {
await writeLambda(outputDir, output, path, lambdas); await writeLambda(outputDir, output, normalizedPath, undefined, lambdas);
} else if (isPrerender(output)) { } else if (isPrerender(output)) {
await writeLambda(outputDir, output.lambda, path, lambdas); await writeLambda(
outputDir,
output.lambda,
normalizedPath,
undefined,
lambdas
);
// Write the fallback file alongside the Lambda directory // Write the fallback file alongside the Lambda directory
let fallback = output.fallback; let fallback = output.fallback;
if (fallback) { if (fallback) {
const ext = getFileExtension(fallback); const ext = getFileExtension(fallback);
const fallbackName = `${path}.prerender-fallback${ext}`; const fallbackName = `${normalizedPath}.prerender-fallback${ext}`;
const fallbackPath = join(outputDir, 'functions', fallbackName); const fallbackPath = join(outputDir, 'functions', fallbackName);
const stream = fallback.toStream(); const stream = fallback.toStream();
await pipe( await pipe(
@@ -109,7 +148,7 @@ async function writeBuildResultV2(
const prerenderConfigPath = join( const prerenderConfigPath = join(
outputDir, outputDir,
'functions', 'functions',
`${path}.prerender-config.json` `${normalizedPath}.prerender-config.json`
); );
const prerenderConfig = { const prerenderConfig = {
...output, ...output,
@@ -118,12 +157,20 @@ async function writeBuildResultV2(
}; };
await fs.writeJSON(prerenderConfigPath, prerenderConfig, { spaces: 2 }); await fs.writeJSON(prerenderConfigPath, prerenderConfig, { spaces: 2 });
} else if (isFile(output)) { } else if (isFile(output)) {
await writeStaticFile(outputDir, output, path, overrides, cleanUrls); await writeStaticFile(
outputDir,
output,
normalizedPath,
overrides,
vercelConfig?.cleanUrls
);
} else if (isEdgeFunction(output)) { } else if (isEdgeFunction(output)) {
await writeEdgeFunction(outputDir, output, path); await writeEdgeFunction(outputDir, output, normalizedPath);
} else { } else {
throw new Error( throw new Error(
`Unsupported output type: "${(output as any).type}" for ${path}` `Unsupported output type: "${
(output as any).type
}" for ${normalizedPath}`
); );
} }
} }
@@ -137,19 +184,28 @@ async function writeBuildResultV2(
async function writeBuildResultV3( async function writeBuildResultV3(
outputDir: string, outputDir: string,
buildResult: BuildResultV3, buildResult: BuildResultV3,
build: Builder build: Builder,
vercelConfig: VercelConfig | null
) { ) {
const { output } = buildResult; const { output } = buildResult;
const src = build.src; const src = build.src;
if (typeof src !== 'string') { if (typeof src !== 'string') {
throw new Error(`Expected "build.src" to be a string`); throw new Error(`Expected "build.src" to be a string`);
} }
const functionConfiguration = vercelConfig
? await getLambdaOptionsFromFunction({
sourceFile: src,
config: vercelConfig,
})
: {};
const ext = extname(src); const ext = extname(src);
const path = build.config?.zeroConfig const path = stripDuplicateSlashes(
? src.substring(0, src.length - ext.length) build.config?.zeroConfig ? src.substring(0, src.length - ext.length) : src
: src; );
if (isLambda(output)) { if (isLambda(output)) {
await writeLambda(outputDir, output, path); await writeLambda(outputDir, output, path, functionConfiguration);
} else if (isEdgeFunction(output)) { } else if (isEdgeFunction(output)) {
await writeEdgeFunction(outputDir, output, path); await writeEdgeFunction(outputDir, output, path);
} else { } else {
@@ -235,6 +291,7 @@ async function writeEdgeFunction(
const config = { const config = {
runtime: 'edge', runtime: 'edge',
...edgeFunction, ...edgeFunction,
entrypoint: normalizePath(edgeFunction.entrypoint),
files: undefined, files: undefined,
type: undefined, type: undefined,
}; };
@@ -258,6 +315,7 @@ async function writeLambda(
outputDir: string, outputDir: string,
lambda: Lambda, lambda: Lambda,
path: string, path: string,
functionConfiguration?: FunctionConfiguration,
lambdas?: Map<Lambda, string> lambdas?: Map<Lambda, string>
) { ) {
const dest = join(outputDir, 'functions', `${path}.func`); const dest = join(outputDir, 'functions', `${path}.func`);
@@ -292,8 +350,14 @@ async function writeLambda(
throw new Error('Malformed `Lambda` - no "files" present'); throw new Error('Malformed `Lambda` - no "files" present');
} }
const memory = functionConfiguration?.memory ?? lambda.memory;
const maxDuration = functionConfiguration?.maxDuration ?? lambda.maxDuration;
const config = { const config = {
...lambda, ...lambda,
handler: normalizePath(lambda.handler),
memory,
maxDuration,
type: undefined, type: undefined,
files: undefined, files: undefined,
zipBuffer: undefined, zipBuffer: undefined,

View File

@@ -15,17 +15,19 @@ export default async function createCertForCns(
try { try {
const certificate = await issueCert(client, cns); const certificate = await issueCert(client, cns);
return certificate; return certificate;
} catch (error) { } catch (err: unknown) {
if (error.code === 'forbidden') { if (ERRORS.isAPIError(err)) {
return new ERRORS.DomainPermissionDenied(error.domain, context); if (err.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(err.domain, context);
}
const mappedError = mapCertError(err, cns);
if (mappedError) {
return mappedError;
}
} }
const mappedError = mapCertError(error, cns); throw err;
if (mappedError) {
return mappedError;
}
throw error;
} finally { } finally {
output.stopSpinner(); output.stopSpinner();
} }

View File

@@ -2,6 +2,8 @@ import { readFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import Client from '../client'; import Client from '../client';
import { Cert } from '../../types'; import { Cert } from '../../types';
import { isErrnoException } from '../is-error';
import { isAPIError } from '../errors-ts';
export default async function createCertFromFile( export default async function createCertFromFile(
client: Client, client: Client,
@@ -25,15 +27,15 @@ export default async function createCertFromFile(
}, },
}); });
return certificate; return certificate;
} catch (error) { } catch (err: unknown) {
if (error.code === 'ENOENT') { if (isErrnoException(err) && err.code === 'ENOENT') {
return new Error(`The specified file "${error.path}" doesn't exist.`); return new Error(`The specified file "${err.path}" doesn't exist.`);
} }
if (error.status < 500) { if (isAPIError(err) && err.status < 500) {
return error; return err;
} }
throw error; throw err;
} }
} }

View File

@@ -22,16 +22,18 @@ export default async function startCertOrder(
}, },
}); });
return cert; return cert;
} catch (error) { } catch (err: unknown) {
if (error.code === 'cert_order_not_found') { if (ERRORS.isAPIError(err)) {
return new ERRORS.CertOrderNotFound(cns); if (err.code === 'cert_order_not_found') {
return new ERRORS.CertOrderNotFound(cns);
}
const mappedError = mapCertError(err, cns);
if (mappedError) {
return mappedError;
}
} }
const mappedError = mapCertError(error, cns); throw err;
if (mappedError) {
return mappedError;
}
throw error;
} }
} }

View File

@@ -5,10 +5,10 @@ import * as ERRORS from '../errors-ts';
export default async function getCertById(client: Client, id: string) { export default async function getCertById(client: Client, id: string) {
try { try {
return await client.fetch<Cert>(`/v6/now/certs/${id}`); return await client.fetch<Cert>(`/v6/now/certs/${id}`);
} catch (error) { } catch (err: unknown) {
if (error.code === 'cert_not_found') { if (ERRORS.isAPIError(err) && err.code === 'cert_not_found') {
return new ERRORS.CertNotFound(id); return new ERRORS.CertNotFound(id);
} }
throw error; throw err;
} }
} }

View File

@@ -17,10 +17,10 @@ export async function getCustomCertsForDomain(
`/v5/now/certs?${stringify({ domain, custom: true })}` `/v5/now/certs?${stringify({ domain, custom: true })}`
); );
return certs; return certs;
} catch (error) { } catch (err: unknown) {
if (error.code === 'forbidden') { if (ERRORS.isAPIError(err) && err.code === 'forbidden') {
return new ERRORS.CertsPermissionDenied(context, domain); return new ERRORS.CertsPermissionDenied(context, domain);
} }
throw error; throw err;
} }
} }

View File

@@ -1,6 +1,8 @@
import retry from 'async-retry'; import retry from 'async-retry';
import { Cert } from '../../types'; import { Cert } from '../../types';
import Client from '../client'; import Client from '../client';
import { isAPIError } from '../errors-ts';
import { isError } from '../is-error';
// When it's a configuration error we should retry because of the DNS propagation // When it's a configuration error we should retry because of the DNS propagation
// otherwise we bail to handle the error in the upper level // otherwise we bail to handle the error in the upper level
@@ -10,13 +12,15 @@ export default async function issueCert(client: Client, cns: string[]) {
try { try {
return await client.fetch<Cert>('/v3/now/certs', { return await client.fetch<Cert>('/v3/now/certs', {
method: 'POST', method: 'POST',
body: { domains: cns } body: { domains: cns },
}); });
} catch (error) { } catch (err: unknown) {
if (error.code === 'configuration_error') { if (isAPIError(err) && err.code === 'configuration_error') {
throw error; throw err;
} else if (isError(err)) {
bail(err);
} else { } else {
bail(error); throw err;
} }
} }
}, },

View File

@@ -1,9 +1,11 @@
import * as ERRORS from '../errors-ts'; import * as ERRORS from '../errors-ts';
export default function mapCertError(error: any, cns?: string[]) { export default function mapCertError(error: ERRORS.APIError, cns?: string[]) {
const errorCode: string = error.code; const errorCode: string = error.code;
if (errorCode === 'too_many_requests') { if (errorCode === 'too_many_requests') {
return new ERRORS.TooManyRequests('certificates', error.retryAfter); const retryAfter =
typeof error.retryAfter === 'number' ? error.retryAfter : 0;
return new ERRORS.TooManyRequests('certificates', retryAfter);
} }
if (errorCode === 'not_found') { if (errorCode === 'not_found') {
return new ERRORS.DomainNotFound(error.domain); return new ERRORS.DomainNotFound(error.domain);

View File

@@ -10,6 +10,7 @@ import error from '../output/error';
import highlight from '../output/highlight'; import highlight from '../output/highlight';
import { VercelConfig } from '../dev/types'; import { VercelConfig } from '../dev/types';
import { AuthConfig, GlobalConfig } from '../../types'; import { AuthConfig, GlobalConfig } from '../../types';
import { isErrnoException, isError } from '../is-error';
const VERCEL_DIR = getGlobalPathConfig(); const VERCEL_DIR = getGlobalPathConfig();
const CONFIG_FILE_PATH = join(VERCEL_DIR, 'config.json'); const CONFIG_FILE_PATH = join(VERCEL_DIR, 'config.json');
@@ -25,25 +26,27 @@ export const readConfigFile = (): GlobalConfig => {
export const writeToConfigFile = (stuff: GlobalConfig): void => { export const writeToConfigFile = (stuff: GlobalConfig): void => {
try { try {
return writeJSON.sync(CONFIG_FILE_PATH, stuff, { indent: 2 }); return writeJSON.sync(CONFIG_FILE_PATH, stuff, { indent: 2 });
} catch (err) { } catch (err: unknown) {
if (err.code === 'EPERM') { if (isErrnoException(err)) {
console.error( if (isErrnoException(err) && err.code === 'EPERM') {
error( console.error(
`Not able to create ${highlight( error(
CONFIG_FILE_PATH `Not able to create ${highlight(
)} (operation not permitted).` CONFIG_FILE_PATH
) )} (operation not permitted).`
); )
process.exit(1); );
} else if (err.code === 'EBADF') { process.exit(1);
console.error( } else if (err.code === 'EBADF') {
error( console.error(
`Not able to create ${highlight( error(
CONFIG_FILE_PATH `Not able to create ${highlight(
)} (bad file descriptor).` CONFIG_FILE_PATH
) )} (bad file descriptor).`
); )
process.exit(1); );
process.exit(1);
}
} }
throw err; throw err;
@@ -65,25 +68,27 @@ export const writeToAuthConfigFile = (authConfig: AuthConfig) => {
indent: 2, indent: 2,
mode: 0o600, mode: 0o600,
}); });
} catch (err) { } catch (err: unknown) {
if (err.code === 'EPERM') { if (isErrnoException(err)) {
console.error( if (err.code === 'EPERM') {
error( console.error(
`Not able to create ${highlight( error(
AUTH_CONFIG_FILE_PATH `Not able to create ${highlight(
)} (operation not permitted).` AUTH_CONFIG_FILE_PATH
) )} (operation not permitted).`
); )
process.exit(1); );
} else if (err.code === 'EBADF') { process.exit(1);
console.error( } else if (err.code === 'EBADF') {
error( console.error(
`Not able to create ${highlight( error(
AUTH_CONFIG_FILE_PATH `Not able to create ${highlight(
)} (bad file descriptor).` AUTH_CONFIG_FILE_PATH
) )} (bad file descriptor).`
); )
process.exit(1); );
process.exit(1);
}
} }
throw err; throw err;
@@ -123,12 +128,14 @@ export function readLocalConfig(
if (existsSync(target)) { if (existsSync(target)) {
config = loadJSON.sync(target); config = loadJSON.sync(target);
} }
} catch (err) { } catch (err: unknown) {
if (err.name === 'JSONError') { if (isError(err) && err.name === 'JSONError') {
console.error(error(err.message)); console.error(error(err.message));
} else { } else if (isErrnoException(err)) {
const code = err.code ? ` (${err.code})` : ''; const code = err.code ? ` (${err.code})` : '';
console.error(error(`Failed to read config file: ${target}${code}`)); console.error(error(`Failed to read config file: ${target}${code}`));
} else {
console.error(err);
} }
process.exit(1); process.exit(1);
} }

View File

@@ -3,76 +3,43 @@ import { join } from 'path';
import ini from 'ini'; import ini from 'ini';
import git from 'git-last-commit'; import git from 'git-last-commit';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { GitMetadata } from '../types'; import { GitMetadata, Project } from '../types';
import { Output } from './output'; import { Output } from './output';
import { errorToString } from './is-error';
export function isDirty(directory: string, output: Output): Promise<boolean> {
return new Promise(resolve => {
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
let debugMessage = `Failed to determine if Git repo has been modified:`;
if (err || stderr) {
if (err) debugMessage += `\n${err}`;
if (stderr) debugMessage += `\n${stderr.trim()}`;
output.debug(debugMessage);
return resolve(false);
}
resolve(stdout.trim().length > 0);
});
});
}
function getLastCommit(directory: string): Promise<git.Commit> {
return new Promise((resolve, reject) => {
git.getLastCommit(
(err, commit) => {
if (err) return reject(err);
resolve(commit);
},
{ dst: directory }
);
});
}
export async function parseGitConfig(configPath: string, output: Output) {
try {
return ini.parse(await fs.readFile(configPath, 'utf-8'));
} catch (error) {
output.debug(`Error while parsing repo data: ${error.message}`);
}
}
export function pluckRemoteUrl(gitConfig: {
[key: string]: any;
}): string | undefined {
// Assuming "origin" is the remote url that the user would want to use
return gitConfig['remote "origin"']?.url;
}
export async function getRemoteUrl(
configPath: string,
output: Output
): Promise<string | null> {
let gitConfig = await parseGitConfig(configPath, output);
if (!gitConfig) {
return null;
}
const originUrl = pluckRemoteUrl(gitConfig);
if (originUrl) {
return originUrl;
}
return null;
}
export async function createGitMeta( export async function createGitMeta(
directory: string, directory: string,
output: Output output: Output,
project?: Project | null
): Promise<GitMetadata | undefined> { ): Promise<GitMetadata | undefined> {
const remoteUrl = await getRemoteUrl(join(directory, '.git/config'), output); // If a Git repository is already connected via `vc git`, use that remote url
let remoteUrl;
if (project?.link) {
// in the form of org/repo
const { repo } = project.link;
const remoteUrls = await getRemoteUrls(
join(directory, '.git/config'),
output
);
if (remoteUrls) {
for (const urlValue of Object.values(remoteUrls)) {
if (urlValue.includes(repo)) {
remoteUrl = urlValue;
}
}
}
}
// If we couldn't get a remote url from the connected repo, default to the origin url
if (!remoteUrl) {
remoteUrl = await getOriginUrl(join(directory, '.git/config'), output);
}
// If we can't get the repo URL, then don't return any metadata // If we can't get the repo URL, then don't return any metadata
if (!remoteUrl) { if (!remoteUrl) {
return; return;
} }
const [commit, dirty] = await Promise.all([ const [commit, dirty] = await Promise.all([
getLastCommit(directory).catch(err => { getLastCommit(directory).catch(err => {
output.debug( output.debug(
@@ -96,3 +63,97 @@ export async function createGitMeta(
dirty, dirty,
}; };
} }
function getLastCommit(directory: string): Promise<git.Commit> {
return new Promise((resolve, reject) => {
git.getLastCommit(
(err, commit) => {
if (err) return reject(err);
resolve(commit);
},
{ dst: directory }
);
});
}
export function isDirty(directory: string, output: Output): Promise<boolean> {
return new Promise(resolve => {
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
let debugMessage = `Failed to determine if Git repo has been modified:`;
if (err || stderr) {
if (err) debugMessage += `\n${err}`;
if (stderr) debugMessage += `\n${stderr.trim()}`;
output.debug(debugMessage);
return resolve(false);
}
resolve(stdout.trim().length > 0);
});
});
}
export async function parseGitConfig(configPath: string, output: Output) {
try {
return ini.parse(await fs.readFile(configPath, 'utf-8'));
} catch (err: unknown) {
output.debug(`Error while parsing repo data: ${errorToString(err)}`);
}
}
export function pluckRemoteUrls(gitConfig: {
[key: string]: any;
}): { [key: string]: string } | undefined {
let remoteUrls: { [key: string]: string } = {};
for (const key of Object.keys(gitConfig)) {
if (key.includes('remote')) {
// ex. remote "origin" — matches origin
const remoteName = key.match(/(?<=").*(?=")/g)?.[0];
const remoteUrl = gitConfig[key]?.url;
if (remoteName && remoteUrl) {
remoteUrls[remoteName] = remoteUrl;
}
}
}
if (Object.keys(remoteUrls).length === 0) {
return;
}
return remoteUrls;
}
export async function getRemoteUrls(
configPath: string,
output: Output
): Promise<{ [key: string]: string } | undefined> {
const config = await parseGitConfig(configPath, output);
if (!config) {
return;
}
const remoteUrls = pluckRemoteUrls(config);
return remoteUrls;
}
export function pluckOriginUrl(gitConfig: {
[key: string]: any;
}): string | undefined {
// Assuming "origin" is the remote url that the user would want to use
return gitConfig['remote "origin"']?.url;
}
export async function getOriginUrl(
configPath: string,
output: Output
): Promise<string | null> {
let gitConfig = await parseGitConfig(configPath, output);
if (!gitConfig) {
return null;
}
const originUrl = pluckOriginUrl(gitConfig);
if (originUrl) {
return originUrl;
}
return null;
}

View File

@@ -20,103 +20,105 @@ export default async function createDeploy(
): Promise<any | DeploymentError> { ): Promise<any | DeploymentError> {
try { try {
return await now.create(paths, createArgs, org, isSettingUpProject, cwd); return await now.create(paths, createArgs, org, isSettingUpProject, cwd);
} catch (error) { } catch (err: unknown) {
if (error.code === 'rate_limited') { if (ERRORS_TS.isAPIError(err)) {
throw new ERRORS_TS.DeploymentsRateLimited(error.message); if (err.code === 'rate_limited') {
} throw new ERRORS_TS.DeploymentsRateLimited(err.message);
// Means that the domain used as a suffix no longer exists
if (error.code === 'domain_missing') {
throw new ERRORS_TS.DomainNotFound(error.value);
}
if (error.code === 'domain_not_found' && error.domain) {
throw new ERRORS_TS.DomainNotFound(error.domain);
}
// This error occures when a domain used in the `alias`
// is not yet verified
if (error.code === 'domain_not_verified' && error.domain) {
throw new ERRORS_TS.DomainNotVerified(error.domain);
}
// If the domain used as a suffix is not verified, we fail
if (error.code === 'domain_not_verified' && error.value) {
throw new ERRORS_TS.DomainVerificationFailed(error.value);
}
// If the domain isn't owned by the user
if (error.code === 'not_domain_owner') {
throw new ERRORS_TS.NotDomainOwner(error.message);
}
if (error.code === 'builds_rate_limited') {
throw new ERRORS_TS.BuildsRateLimited(error.message);
}
// If the user doesn't have permissions over the domain used as a suffix we fail
if (error.code === 'forbidden') {
throw new ERRORS_TS.DomainPermissionDenied(error.value, contextName);
}
if (error.code === 'bad_request' && error.keyword) {
throw new ERRORS.SchemaValidationFailed(
error.message,
error.keyword,
error.dataPath,
error.params
);
}
if (error.code === 'domain_configured') {
throw new ERRORS_TS.AliasDomainConfigured(error);
}
if (error.code === 'missing_build_script') {
throw new ERRORS_TS.MissingBuildScript(error);
}
if (error.code === 'conflicting_file_path') {
throw new ERRORS_TS.ConflictingFilePath(error);
}
if (error.code === 'conflicting_path_segment') {
throw new ERRORS_TS.ConflictingPathSegment(error);
}
// If the cert is missing we try to generate a new one and the retry
if (error.code === 'cert_missing') {
const result = await generateCertForDeploy(
client,
contextName,
error.value
);
if (result instanceof NowError) {
return result;
} }
return createDeploy( // Means that the domain used as a suffix no longer exists
client, if (err.code === 'domain_missing') {
now, throw new ERRORS_TS.DomainNotFound(err.value);
contextName, }
paths,
createArgs,
org,
isSettingUpProject
);
}
if (error.code === 'not_found') { if (err.code === 'domain_not_found' && err.domain) {
throw new ERRORS_TS.DeploymentNotFound({ context: contextName }); throw new ERRORS_TS.DomainNotFound(err.domain);
} }
const certError = mapCertError(error); // This error occures when a domain used in the `alias`
if (certError) { // is not yet verified
return certError; if (err.code === 'domain_not_verified' && err.domain) {
throw new ERRORS_TS.DomainNotVerified(err.domain);
}
// If the domain used as a suffix is not verified, we fail
if (err.code === 'domain_not_verified' && err.value) {
throw new ERRORS_TS.DomainVerificationFailed(err.value);
}
// If the domain isn't owned by the user
if (err.code === 'not_domain_owner') {
throw new ERRORS_TS.NotDomainOwner(err.message);
}
if (err.code === 'builds_rate_limited') {
throw new ERRORS_TS.BuildsRateLimited(err.message);
}
// If the user doesn't have permissions over the domain used as a suffix we fail
if (err.code === 'forbidden') {
throw new ERRORS_TS.DomainPermissionDenied(err.value, contextName);
}
if (err.code === 'bad_request' && err.keyword) {
throw new ERRORS.SchemaValidationFailed(
err.message,
err.keyword,
err.dataPath,
err.params
);
}
if (err.code === 'domain_configured') {
throw new ERRORS_TS.AliasDomainConfigured(err);
}
if (err.code === 'missing_build_script') {
throw new ERRORS_TS.MissingBuildScript(err);
}
if (err.code === 'conflicting_file_path') {
throw new ERRORS_TS.ConflictingFilePath(err);
}
if (err.code === 'conflicting_path_segment') {
throw new ERRORS_TS.ConflictingPathSegment(err);
}
// If the cert is missing we try to generate a new one and the retry
if (err.code === 'cert_missing') {
const result = await generateCertForDeploy(
client,
contextName,
err.value
);
if (result instanceof NowError) {
return result;
}
return createDeploy(
client,
now,
contextName,
paths,
createArgs,
org,
isSettingUpProject
);
}
if (err.code === 'not_found') {
throw new ERRORS_TS.DeploymentNotFound({ context: contextName });
}
const certError = mapCertError(err);
if (certError) {
return certError;
}
} }
// If the error is unknown, we just throw // If the error is unknown, we just throw
throw error; throw err;
} }
} }

View File

@@ -5,6 +5,7 @@ import {
DeploymentNotFound, DeploymentNotFound,
DeploymentPermissionDenied, DeploymentPermissionDenied,
InvalidDeploymentId, InvalidDeploymentId,
isAPIError,
} from '../errors-ts'; } from '../errors-ts';
import mapCertError from '../certs/map-cert-error'; import mapCertError from '../certs/map-cert-error';
@@ -26,23 +27,25 @@ export default async function getDeploymentByIdOrHost(
) )
: await getDeploymentById(client, idOrHost, apiVersion); : await getDeploymentById(client, idOrHost, apiVersion);
return deployment; return deployment;
} catch (error) { } catch (err: unknown) {
if (error.status === 404) { if (isAPIError(err)) {
return new DeploymentNotFound({ id: idOrHost, context: contextName }); if (err.status === 404) {
} return new DeploymentNotFound({ id: idOrHost, context: contextName });
if (error.status === 403) { }
return new DeploymentPermissionDenied(idOrHost, contextName); if (err.status === 403) {
} return new DeploymentPermissionDenied(idOrHost, contextName);
if (error.status === 400 && error.message.includes('`id`')) { }
return new InvalidDeploymentId(idOrHost); if (err.status === 400 && err.message.includes('`id`')) {
return new InvalidDeploymentId(idOrHost);
}
const certError = mapCertError(err);
if (certError) {
return certError;
}
} }
const certError = mapCertError(error); throw err;
if (certError) {
return certError;
}
throw error;
} }
} }

View File

@@ -1,7 +1,10 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import { join } from 'path'; import { join } from 'path';
import { BuildsManifest } from '../../commands/build';
export default async function getPrebuiltJson(directory: string) { export default async function getPrebuiltJson(
directory: string
): Promise<BuildsManifest | null> {
try { try {
return await fs.readJSON(join(directory, '.vercel/output/builds.json')); return await fs.readJSON(join(directory, '.vercel/output/builds.json'));
} catch (error) { } catch (error) {

View File

@@ -11,13 +11,11 @@ import cliPkg from '../pkg';
import cmd from '../output/cmd'; import cmd from '../output/cmd';
import { Output } from '../output'; import { Output } from '../output';
import { getDistTag } from '../get-dist-tag';
import { NoBuilderCacheError } from '../errors-ts'; import { NoBuilderCacheError } from '../errors-ts';
import * as staticBuilder from './static-builder'; import * as staticBuilder from './static-builder';
import { BuilderWithPackage } from './types'; import { BuilderWithPackage } from './types';
import { isErrnoException } from '../is-error';
type CliPackageJson = typeof cliPkg;
const require_: typeof require = eval('require'); const require_: typeof require = eval('require');
@@ -37,8 +35,6 @@ const localBuilders: { [key: string]: BuilderWithPackage } = {
'@vercel/static': createStaticBuilder('vercel'), '@vercel/static': createStaticBuilder('vercel'),
}; };
const distTag = getDistTag(cliPkg.version);
export const cacheDirPromise = prepareCacheDir(); export const cacheDirPromise = prepareCacheDir();
export const builderDirPromise = prepareBuilderDir(); export const builderDirPromise = prepareBuilderDir();
@@ -65,8 +61,8 @@ export async function prepareBuilderDir() {
try { try {
const buildersPkg = join(builderDir, 'package.json'); const buildersPkg = join(builderDir, 'package.json');
await writeJSON(buildersPkg, { private: true }, { flag: 'wx' }); await writeJSON(buildersPkg, { private: true }, { flag: 'wx' });
} catch (err) { } catch (err: unknown) {
if (err.code !== 'EEXIST') { if (!isErrnoException(err) || err.code !== 'EEXIST') {
throw err; throw err;
} }
} }
@@ -102,9 +98,8 @@ function parseVersionSafe(rawSpec: string) {
export function filterPackage( export function filterPackage(
builderSpec: string, builderSpec: string,
distTag: string,
buildersPkg: PackageJson, buildersPkg: PackageJson,
cliPkg: Partial<CliPackageJson> cliPkg: Partial<PackageJson>
) { ) {
if (builderSpec in localBuilders) return false; if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec); const parsed = npa(builderSpec);
@@ -126,31 +121,6 @@ export function filterPackage(
return false; return false;
} }
// Skip install of already installed Runtime with tag compatible match
if (
parsed.name &&
parsed.type === 'tag' &&
parsed.fetchSpec === distTag &&
buildersPkg.dependencies
) {
const parsedInstalled = npa(
`${parsed.name}@${buildersPkg.dependencies[parsed.name]}`
);
if (parsedInstalled.type !== 'version') {
return true;
}
const semverInstalled = semver.parse(parsedInstalled.rawSpec);
if (!semverInstalled) {
return true;
}
if (semverInstalled.prerelease.length > 0) {
return semverInstalled.prerelease[0] !== distTag;
}
if (distTag === 'latest') {
return false;
}
}
return true; return true;
} }
@@ -183,7 +153,7 @@ export async function installBuilders(
// Filter out any packages that come packaged with Vercel CLI // Filter out any packages that come packaged with Vercel CLI
const packagesToInstall = packages.filter(p => const packagesToInstall = packages.filter(p =>
filterPackage(p, distTag, buildersPkgBefore, cliPkg) filterPackage(p, buildersPkgBefore, cliPkg)
); );
if (packagesToInstall.length === 0) { if (packagesToInstall.length === 0) {
@@ -362,8 +332,12 @@ export async function getBuilder(
builder: Object.freeze(mod), builder: Object.freeze(mod),
package: Object.freeze(pkg), package: Object.freeze(pkg),
}; };
} catch (err) { } catch (err: unknown) {
if (err.code === 'MODULE_NOT_FOUND' && !isRetry) { if (
isErrnoException(err) &&
err.code === 'MODULE_NOT_FOUND' &&
!isRetry
) {
output.debug( output.debug(
`Attempted to require ${requirePath}, but it is not installed` `Attempted to require ${requirePath}, but it is not installed`
); );
@@ -392,20 +366,13 @@ export function isBundledBuilder(
return false; return false;
} }
const bundledVersion = dependencies[parsed.name]; const inCliDependencyList = !!dependencies[parsed.name];
if (bundledVersion) { const inScope = parsed.scope === '@vercel';
if (parsed.type === 'tag') { const isVersionedReference = ['tag', 'version', 'range'].includes(
if (parsed.fetchSpec === 'canary') { parsed.type
return bundledVersion.includes('canary'); );
} else if (parsed.fetchSpec === 'latest') {
return !bundledVersion.includes('canary');
}
} else if (parsed.type === 'version') {
return parsed.fetchSpec === bundledVersion;
}
}
return false; return inCliDependencyList && inScope && isVersionedReference;
} }
function getPackageName( function getPackageName(

View File

@@ -4,7 +4,7 @@ import ms from 'ms';
import bytes from 'bytes'; import bytes from 'bytes';
import { delimiter, dirname, join } from 'path'; import { delimiter, dirname, join } from 'path';
import { fork, ChildProcess } from 'child_process'; import { fork, ChildProcess } from 'child_process';
import { createFunction } from '@zeit/fun'; import { createFunction } from '@vercel/fun';
import { import {
Builder, Builder,
BuildOptions, BuildOptions,

View File

@@ -94,6 +94,12 @@ import { ProjectEnvVariable, ProjectSettings } from '../../types';
import exposeSystemEnvs from './expose-system-envs'; import exposeSystemEnvs from './expose-system-envs';
import { treeKill } from '../tree-kill'; import { treeKill } from '../tree-kill';
import { nodeHeadersToFetchHeaders } from './headers'; import { nodeHeadersToFetchHeaders } from './headers';
import {
errorToString,
isErrnoException,
isError,
isSpawnError,
} from '../is-error';
const frontendRuntimeSet = new Set( const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build') frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
@@ -340,8 +346,8 @@ export default class DevServer {
} }
fileChanged(name, changed, removed); fileChanged(name, changed, removed);
this.output.debug(`File created: ${name}`); this.output.debug(`File created: ${name}`);
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOENT') { if (isErrnoException(err) && err.code === 'ENOENT') {
this.output.debug(`File created, but has since been deleted: ${name}`); this.output.debug(`File created, but has since been deleted: ${name}`);
fileRemoved(name, this.files, changed, removed); fileRemoved(name, this.files, changed, removed);
} else { } else {
@@ -375,8 +381,8 @@ export default class DevServer {
this.files[name] = await FileFsRef.fromFsPath({ fsPath }); this.files[name] = await FileFsRef.fromFsPath({ fsPath });
fileChanged(name, changed, removed); fileChanged(name, changed, removed);
this.output.debug(`File modified: ${name}`); this.output.debug(`File modified: ${name}`);
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOENT') { if (isErrnoException(err) && err.code === 'ENOENT') {
this.output.debug(`File modified, but has since been deleted: ${name}`); this.output.debug(`File modified, but has since been deleted: ${name}`);
fileRemoved(name, this.files, changed, removed); fileRemoved(name, this.files, changed, removed);
} else { } else {
@@ -507,8 +513,8 @@ export default class DevServer {
this.output.debug(`Using local env: ${filePath}`); this.output.debug(`Using local env: ${filePath}`);
env = parseDotenv(dotenv); env = parseDotenv(dotenv);
env = this.injectSystemValuesInDotenv(env); env = this.injectSystemValuesInDotenv(env);
} catch (err) { } catch (err: unknown) {
if (err.code !== 'ENOENT') { if (!isErrnoException(err) || err.code !== 'ENOENT') {
throw err; throw err;
} }
} }
@@ -558,9 +564,8 @@ export default class DevServer {
]); ]);
await this.validateVercelConfig(vercelConfig); await this.validateVercelConfig(vercelConfig);
const { error: routeError, routes: maybeRoutes } = getTransformedRoutes({ const { error: routeError, routes: maybeRoutes } =
nowConfig: vercelConfig, getTransformedRoutes(vercelConfig);
});
if (routeError) { if (routeError) {
this.output.prettyError(routeError); this.output.prettyError(routeError);
await this.exit(); await this.exit();
@@ -720,13 +725,15 @@ export default class DevServer {
const parsed: WithFileNameSymbol<T> = JSON.parse(raw); const parsed: WithFileNameSymbol<T> = JSON.parse(raw);
parsed[fileNameSymbol] = rel; parsed[fileNameSymbol] = rel;
return parsed; return parsed;
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOENT') { if (isError(err)) {
this.output.debug(`No \`${rel}\` file present`); if (isErrnoException(err) && err.code === 'ENOENT') {
} else if (err.name === 'SyntaxError') { this.output.debug(`No \`${rel}\` file present`);
this.output.warn( } else if (err.name === 'SyntaxError') {
`There is a syntax error in the \`${rel}\` file: ${err.message}` this.output.warn(
); `There is a syntax error in the \`${rel}\` file: ${err.message}`
);
}
} else { } else {
throw err; throw err;
} }
@@ -852,22 +859,26 @@ export default class DevServer {
while (typeof address !== 'string') { while (typeof address !== 'string') {
try { try {
address = await listen(this.server, ...listenSpec); address = await listen(this.server, ...listenSpec);
} catch (err) { } catch (err: unknown) {
this.output.debug(`Got listen error: ${err.code}`); if (isErrnoException(err)) {
if (err.code === 'EADDRINUSE') { this.output.debug(`Got listen error: ${err.code}`);
if (typeof listenSpec[0] === 'number') { if (err.code === 'EADDRINUSE') {
// Increase port and try again if (typeof listenSpec[0] === 'number') {
this.output.note( // Increase port and try again
`Requested port ${chalk.yellow( this.output.note(
String(listenSpec[0]) `Requested port ${chalk.yellow(
)} is already in use` String(listenSpec[0])
); )} is already in use`
listenSpec[0]++; );
} else { listenSpec[0]++;
this.output.error( } else {
`Requested socket ${chalk.cyan(listenSpec[0])} is already in use` this.output.error(
); `Requested socket ${chalk.cyan(
process.exit(1); listenSpec[0]
)} is already in use`
);
process.exit(1);
}
} }
} else { } else {
throw err; throw err;
@@ -1029,12 +1040,8 @@ export default class DevServer {
try { try {
await Promise.all(ops); await Promise.all(ops);
} catch (err) { } catch (err: unknown) {
// Node 8 doesn't have a code for that error if (isErrnoException(err) && err.code === 'ERR_SERVER_NOT_RUNNING') {
if (
err.code === 'ERR_SERVER_NOT_RUNNING' ||
err.message === 'Not running'
) {
process.exit(exitCode || 0); process.exit(exitCode || 0);
} else { } else {
throw err; throw err;
@@ -1304,13 +1311,16 @@ export default class DevServer {
try { try {
const vercelConfig = await this.getVercelConfig(); const vercelConfig = await this.getVercelConfig();
await this.serveProjectAsNowV2(req, res, requestId, vercelConfig); await this.serveProjectAsNowV2(req, res, requestId, vercelConfig);
} catch (err) { } catch (err: unknown) {
console.error(err); console.error(err);
this.output.debug(err.stack);
if (isError(err) && typeof err.stack === 'string') {
this.output.debug(err.stack);
}
if (!res.finished) { if (!res.finished) {
res.statusCode = 500; res.statusCode = 500;
res.end(err.message); res.end(errorToString(err));
} }
} }
}; };
@@ -1534,16 +1544,16 @@ export default class DevServer {
); );
} }
} }
} catch (err) { } catch (err: unknown) {
// `startDevServer()` threw an error. Most likely this means the dev // `startDevServer()` threw an error. Most likely this means the dev
// server process exited before sending the port information message // server process exited before sending the port information message
// (missing dependency at runtime, for example). // (missing dependency at runtime, for example).
if (err.code === 'ENOENT') { if (isSpawnError(err) && err.code === 'ENOENT') {
err.message = `Command not found: ${chalk.cyan( err.message = `Command not found: ${chalk.cyan(
err.path, err.path,
...err.spawnargs ...err.spawnargs
)}\nPlease ensure that ${cmd(err.path)} is properly installed`; )}\nPlease ensure that ${cmd(err.path!)} is properly installed`;
err.link = 'https://vercel.link/command-not-found'; (err as any).link = 'https://vercel.link/command-not-found';
} }
await this.sendError( await this.sendError(
@@ -1846,16 +1856,16 @@ export default class DevServer {
buildEnv: { ...envConfigs.buildEnv }, buildEnv: { ...envConfigs.buildEnv },
}, },
}); });
} catch (err) { } catch (err: unknown) {
// `startDevServer()` threw an error. Most likely this means the dev // `startDevServer()` threw an error. Most likely this means the dev
// server process exited before sending the port information message // server process exited before sending the port information message
// (missing dependency at runtime, for example). // (missing dependency at runtime, for example).
if (err.code === 'ENOENT') { if (isSpawnError(err) && err.code === 'ENOENT') {
err.message = `Command not found: ${chalk.cyan( err.message = `Command not found: ${chalk.cyan(
err.path, err.path,
...err.spawnargs ...err.spawnargs
)}\nPlease ensure that ${cmd(err.path)} is properly installed`; )}\nPlease ensure that ${cmd(err.path!)} is properly installed`;
err.link = 'https://vercel.link/command-not-found'; (err as any).link = 'https://vercel.link/command-not-found';
} }
this.output.prettyError(err); this.output.prettyError(err);

View File

@@ -1,6 +1,6 @@
import http from 'http'; import http from 'http';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { Lambda as FunLambda } from '@zeit/fun'; import { Lambda as FunLambda } from '@vercel/fun';
import { import {
Builder as BuildConfig, Builder as BuildConfig,
BuildOptions, BuildOptions,

View File

@@ -5,6 +5,7 @@ import {
DNSInvalidPort, DNSInvalidPort,
DNSInvalidType, DNSInvalidType,
DNSConflictingRecord, DNSConflictingRecord,
isAPIError,
} from '../errors-ts'; } from '../errors-ts';
import { DNSRecordData } from '../../types'; import { DNSRecordData } from '../../types';
@@ -26,32 +27,34 @@ export default async function addDNSRecord(
} }
); );
return record; return record;
} catch (error) { } catch (err: unknown) {
if (error.status === 400 && error.code === 'invalid_type') { if (isAPIError(err)) {
return new DNSInvalidType(recordData.type); if (err.status === 400 && err.code === 'invalid_type') {
return new DNSInvalidType(recordData.type);
}
if (err.status === 400 && err.message.includes('port')) {
return new DNSInvalidPort();
}
if (err.status === 400) {
return err;
}
if (err.status === 403) {
return new DNSPermissionDenied(domain);
}
if (err.status === 404) {
return new DomainNotFound(domain);
}
if (err.status === 409) {
const { oldId = '' } = err;
return new DNSConflictingRecord(oldId);
}
} }
if (error.status === 400 && error.message.includes('port')) { throw err;
return new DNSInvalidPort();
}
if (error.status === 400) {
return error;
}
if (error.status === 403) {
return new DNSPermissionDenied(domain);
}
if (error.status === 404) {
return new DomainNotFound(domain);
}
if (error.status === 409) {
const { oldId = '' } = error;
return new DNSConflictingRecord(oldId);
}
throw error;
} }
} }

View File

@@ -1,5 +1,5 @@
import { DNSRecord, PaginationOptions } from '../../types'; import { DNSRecord, PaginationOptions } from '../../types';
import { DomainNotFound } from '../errors-ts'; import { DomainNotFound, isAPIError } from '../errors-ts';
import { Output } from '../output'; import { Output } from '../output';
import Client from '../client'; import Client from '../client';
@@ -27,10 +27,10 @@ export default async function getDomainDNSRecords(
const data = await client.fetch<Response>(url); const data = await client.fetch<Response>(url);
return data; return data;
} catch (error) { } catch (err: unknown) {
if (error.code === 'not_found') { if (isAPIError(err) && err.code === 'not_found') {
return new DomainNotFound(domain); return new DomainNotFound(domain);
} }
throw error; throw err;
} }
} }

View File

@@ -2,7 +2,7 @@ import chalk from 'chalk';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import { Response } from 'node-fetch'; import { Response } from 'node-fetch';
import { DomainNotFound, InvalidDomain } from '../errors-ts'; import { DomainNotFound, InvalidDomain, isAPIError } from '../errors-ts';
import Client from '../client'; import Client from '../client';
type JSONResponse = { type JSONResponse = {
@@ -33,15 +33,17 @@ export default async function importZonefile(
const { recordIds } = (await res.json()) as JSONResponse; const { recordIds } = (await res.json()) as JSONResponse;
return recordIds; return recordIds;
} catch (error) { } catch (err: unknown) {
if (error.code === 'not_found') { if (isAPIError(err)) {
return new DomainNotFound(domain, contextName); if (err.code === 'not_found') {
return new DomainNotFound(domain, contextName);
}
if (err.code === 'invalid_domain') {
return new InvalidDomain(domain);
}
} }
if (error.code === 'invalid_domain') { throw err;
return new InvalidDomain(domain);
}
throw error;
} }
} }

View File

@@ -1,6 +1,6 @@
import chalk from 'chalk'; import chalk from 'chalk';
import retry from 'async-retry'; import retry from 'async-retry';
import { DomainAlreadyExists, InvalidDomain } from '../errors-ts'; import { DomainAlreadyExists, InvalidDomain, isAPIError } from '../errors-ts';
import { Domain } from '../../types'; import { Domain } from '../../types';
import Client from '../client'; import Client from '../client';
@@ -29,16 +29,18 @@ async function performAddRequest(client: Client, domainName: string) {
method: 'POST', method: 'POST',
}); });
return domain; return domain;
} catch (error) { } catch (err: unknown) {
if (error.code === 'invalid_name') { if (isAPIError(err)) {
return new InvalidDomain(domainName); if (err.code === 'invalid_name') {
return new InvalidDomain(domainName);
}
if (err.code === 'domain_already_exists') {
return new DomainAlreadyExists(domainName);
}
} }
if (error.code === 'domain_already_exists') { throw err;
return new DomainAlreadyExists(domainName);
}
throw error;
} }
}, },
{ retries: 5, maxTimeout: 8000 } { retries: 5, maxTimeout: 8000 }

View File

@@ -1,7 +1,11 @@
import chalk from 'chalk'; import chalk from 'chalk';
import Client from '../client'; import Client from '../client';
import { Domain } from '../../types'; import { Domain } from '../../types';
import { DomainPermissionDenied, DomainNotFound } from '../errors-ts'; import {
DomainPermissionDenied,
DomainNotFound,
isAPIError,
} from '../errors-ts';
type Response = { type Response = {
domain: Domain; domain: Domain;
@@ -25,15 +29,17 @@ export default async function getDomainByName(
`/v4/domains/${encodeURIComponent(domainName)}` `/v4/domains/${encodeURIComponent(domainName)}`
); );
return domain; return domain;
} catch (error) { } catch (err: unknown) {
if (error.status === 404) { if (isAPIError(err)) {
return new DomainNotFound(domainName, contextName); if (err.status === 404) {
return new DomainNotFound(domainName, contextName);
}
if (err.status === 403) {
return new DomainPermissionDenied(domainName, contextName);
}
} }
if (error.status === 403) { throw err;
return new DomainPermissionDenied(domainName, contextName);
}
throw error;
} }
} }

View File

@@ -1,5 +1,6 @@
import Client from '../client'; import Client from '../client';
import { DomainConfig } from '../../types'; import { DomainConfig } from '../../types';
import { isAPIError } from '../errors-ts';
export async function getDomainConfig(client: Client, domainName: string) { export async function getDomainConfig(client: Client, domainName: string) {
try { try {
@@ -8,11 +9,11 @@ export async function getDomainConfig(client: Client, domainName: string) {
); );
return config; return config;
} catch (error) { } catch (err: unknown) {
if (error.status < 500) { if (isAPIError(err) && err.status < 500) {
return error; return err;
} }
throw error; throw err;
} }
} }

View File

@@ -1,5 +1,5 @@
import { stringify } from 'querystring'; import { stringify } from 'querystring';
import { UnsupportedTLD } from '../errors-ts'; import { isAPIError, UnsupportedTLD } from '../errors-ts';
import Client from '../client'; import Client from '../client';
type Response = { type Response = {
@@ -15,15 +15,17 @@ export default async function getDomainPrice(
try { try {
const querystr = type ? stringify({ name, type }) : stringify({ name }); const querystr = type ? stringify({ name, type }) : stringify({ name });
return await client.fetch<Response>(`/v3/domains/price?${querystr}`); return await client.fetch<Response>(`/v3/domains/price?${querystr}`);
} catch (error) { } catch (err: unknown) {
if (error.code === 'unsupported_tld') { if (isAPIError(err)) {
return new UnsupportedTLD(name); if (err.code === 'unsupported_tld') {
return new UnsupportedTLD(name);
}
if (err.status < 500) {
return err;
}
} }
if (error.status < 500) { throw err;
return error;
}
throw error;
} }
} }

View File

@@ -1,6 +1,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import Client from '../client'; import Client from '../client';
import { Domain } from '../../types'; import { Domain } from '../../types';
import { isAPIError } from '../errors-ts';
type Response = { type Response = {
domain: Domain; domain: Domain;
@@ -20,11 +21,11 @@ export async function getDomain(
); );
return domain; return domain;
} catch (error) { } catch (err: unknown) {
if (error.status < 500) { if (isAPIError(err) && err.status < 500) {
return error; return err;
} }
throw error; throw err;
} }
} }

View File

@@ -20,25 +20,27 @@ export default async function moveOutDomain(
method: 'PATCH', method: 'PATCH',
} }
); );
} catch (error) { } catch (err: unknown) {
if (error.code === 'forbidden') { if (ERRORS.isAPIError(err)) {
return new ERRORS.DomainPermissionDenied(name, contextName); if (err.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(name, contextName);
}
if (err.code === 'not_found') {
return new ERRORS.DomainNotFound(name);
}
if (err.code === 'invalid_move_destination') {
return new ERRORS.InvalidMoveDestination(destination);
}
if (err.code === 'domain_move_conflict') {
const { pendingAsyncPurchase, resolvable, suffix, message } = err;
return new ERRORS.DomainMoveConflict({
message,
pendingAsyncPurchase,
resolvable,
suffix,
});
}
} }
if (error.code === 'not_found') { throw err;
return new ERRORS.DomainNotFound(name);
}
if (error.code === 'invalid_move_destination') {
return new ERRORS.InvalidMoveDestination(destination);
}
if (error.code === 'domain_move_conflict') {
const { pendingAsyncPurchase, resolvable, suffix, message } = error;
return new ERRORS.DomainMoveConflict({
message,
pendingAsyncPurchase,
resolvable,
suffix,
});
}
throw error;
} }
} }

View File

@@ -20,28 +20,30 @@ export default async function purchaseDomain(
body: { name, expectedPrice, renew }, body: { name, expectedPrice, renew },
method: 'POST', method: 'POST',
}); });
} catch (error) { } catch (err: unknown) {
if (error.code === 'invalid_domain') { if (ERRORS.isAPIError(err)) {
return new ERRORS.InvalidDomain(name); if (err.code === 'invalid_domain') {
return new ERRORS.InvalidDomain(name);
}
if (err.code === 'not_available') {
return new ERRORS.DomainNotAvailable(name);
}
if (err.code === 'service_unavailabe') {
return new ERRORS.DomainServiceNotAvailable(name);
}
if (err.code === 'unexpected_error') {
return new ERRORS.UnexpectedDomainPurchaseError(name);
}
if (err.code === 'source_not_found') {
return new ERRORS.SourceNotFound();
}
if (err.code === 'payment_error') {
return new ERRORS.DomainPaymentError();
}
if (err.code === 'unsupported_tld') {
return new ERRORS.UnsupportedTLD(name);
}
} }
if (error.code === 'not_available') { throw err;
return new ERRORS.DomainNotAvailable(name);
}
if (error.code === 'service_unavailabe') {
return new ERRORS.DomainServiceNotAvailable(name);
}
if (error.code === 'unexpected_error') {
return new ERRORS.UnexpectedDomainPurchaseError(name);
}
if (error.code === 'source_not_found') {
return new ERRORS.SourceNotFound();
}
if (error.code === 'payment_error') {
return new ERRORS.DomainPaymentError();
}
if (error.code === 'unsupported_tld') {
return new ERRORS.UnsupportedTLD(name);
}
throw error;
} }
} }

View File

@@ -10,24 +10,26 @@ export default async function removeDomainByName(
return await now.fetch(`/v3/domains/${encodeURIComponent(domain)}`, { return await now.fetch(`/v3/domains/${encodeURIComponent(domain)}`, {
method: 'DELETE', method: 'DELETE',
}); });
} catch (error) { } catch (err: unknown) {
if (error.code === 'not_found') { if (ERRORS.isAPIError(err)) {
return new ERRORS.DomainNotFound(domain); if (err.code === 'not_found') {
return new ERRORS.DomainNotFound(domain);
}
if (err.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(domain, contextName);
}
if (err.code === 'domain_removal_conflict') {
return new ERRORS.DomainRemovalConflict({
aliases: err.aliases,
certs: err.certs,
message: err.message,
pendingAsyncPurchase: err.pendingAsyncPurchase,
resolvable: err.resolvable,
suffix: err.suffix,
transferring: err.transferring,
});
}
} }
if (error.code === 'forbidden') { throw err;
return new ERRORS.DomainPermissionDenied(domain, contextName);
}
if (error.code === 'domain_removal_conflict') {
return new ERRORS.DomainRemovalConflict({
aliases: error.aliases,
certs: error.certs,
message: error.message,
pendingAsyncPurchase: error.pendingAsyncPurchase,
resolvable: error.resolvable,
suffix: error.suffix,
transferring: error.transferring,
});
}
throw error;
} }
} }

View File

@@ -14,25 +14,27 @@ export default async function setCustomSuffix(
suffix, suffix,
}, },
}); });
} catch (error) { } catch (err: unknown) {
if (error.code === 'forbidden') { if (ERRORS.isAPIError(err)) {
return new ERRORS.DomainPermissionDenied(domain, contextName); if (err.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(domain, contextName);
}
if (err.code === 'domain_external') {
return new ERRORS.DomainExternal(domain);
}
if (err.code === 'domain_invalid') {
return new ERRORS.InvalidDomain(domain);
}
if (err.code === 'domain_not_found') {
return new ERRORS.DomainNotFound(domain);
}
if (err.code === 'domain_not_verified') {
return new ERRORS.DomainNotVerified(domain);
}
if (err.code === 'domain_permission_denied') {
return new ERRORS.DomainPermissionDenied(domain, contextName);
}
} }
if (error.code === 'domain_external') { throw err;
return new ERRORS.DomainExternal(domain);
}
if (error.code === 'domain_invalid') {
return new ERRORS.InvalidDomain(domain);
}
if (error.code === 'domain_not_found') {
return new ERRORS.DomainNotFound(domain);
}
if (error.code === 'domain_not_verified') {
return new ERRORS.DomainNotVerified(domain);
}
if (error.code === 'domain_permission_denied') {
return new ERRORS.DomainPermissionDenied(domain, contextName);
}
throw error;
} }
} }

View File

@@ -17,25 +17,27 @@ export default async function transferInDomain(
body: { method: 'transfer-in', name, authCode, expectedPrice }, body: { method: 'transfer-in', name, authCode, expectedPrice },
method: 'POST', method: 'POST',
}); });
} catch (error) { } catch (err: unknown) {
if (error.code === 'invalid_name') { if (ERRORS.isAPIError(err)) {
return new ERRORS.InvalidDomain(name); if (err.code === 'invalid_name') {
return new ERRORS.InvalidDomain(name);
}
if (err.code === 'domain_already_exists') {
return new ERRORS.DomainNotAvailable(name);
}
if (err.code === 'not_transferable') {
return new ERRORS.DomainNotTransferable(name);
}
if (err.code === 'invalid_auth_code') {
return new ERRORS.InvalidTransferAuthCode(name, authCode);
}
if (err.code === 'source_not_found') {
return new ERRORS.SourceNotFound();
}
if (err.code === 'registration_failed') {
return new ERRORS.DomainRegistrationFailed(name, err.message);
}
} }
if (error.code === 'domain_already_exists') { throw err;
return new ERRORS.DomainNotAvailable(name);
}
if (error.code === 'not_transferable') {
return new ERRORS.DomainNotTransferable(name);
}
if (error.code === 'invalid_auth_code') {
return new ERRORS.InvalidTransferAuthCode(name, authCode);
}
if (error.code === 'source_not_found') {
return new ERRORS.SourceNotFound();
}
if (error.code === 'registration_failed') {
return new ERRORS.DomainRegistrationFailed(name, error.message);
}
throw error;
} }
} }

View File

@@ -29,6 +29,6 @@ export default async function addEnvRecord(
const url = `/v8/projects/${projectId}/env`; const url = `/v8/projects/${projectId}/env`;
await client.fetch(url, { await client.fetch(url, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body,
}); });
} }

View File

@@ -0,0 +1,80 @@
import { Output } from '../output';
import { Dictionary } from '@vercel/client';
import { readFile } from 'fs-extra';
import { parseEnv } from '../parse-env';
import chalk from 'chalk';
export async function createEnvObject(
envPath: string,
output: Output
): Promise<Dictionary<string | undefined> | undefined> {
// Originally authored by Tyler Waters under MIT License: https://github.com/tswaters/env-file-parser/blob/f17c009b39da599380e069ee72728d1cafdb56b8/lib/parse.js
// https://github.com/tswaters/env-file-parser/blob/f17c009b39da599380e069ee72728d1cafdb56b8/LICENSE
const envArr = (await readFile(envPath, 'utf-8'))
// remove double quotes
.replace(/"/g, '')
// split on new line
.split(/\r?\n|\r/)
// filter comments
.filter(line => /^[^#]/.test(line))
// needs equal sign
.filter(line => /=/i.test(line));
const parsedEnv = parseEnv(envArr);
if (Object.keys(parsedEnv).length === 0) {
output.debug('Failed to parse env file.');
return;
}
return parsedEnv;
}
function findChanges(
oldEnv: Dictionary<string | undefined>,
newEnv: Dictionary<string | undefined>
): {
added: string[];
changed: string[];
removed: string[];
} {
const added = [];
const changed = [];
for (const key of Object.keys(newEnv)) {
if (oldEnv[key] === undefined) {
added.push(key);
} else if (oldEnv[key] !== newEnv[key]) {
changed.push(key);
}
delete oldEnv[key];
}
const removed = Object.keys(oldEnv);
return {
added,
changed,
removed,
};
}
export function buildDeltaString(
oldEnv: Dictionary<string | undefined>,
newEnv: Dictionary<string | undefined>
): string {
const { added, changed, removed } = findChanges(oldEnv, newEnv);
let deltaString = '';
deltaString += chalk.green(addDeltaSection('+', added));
deltaString += chalk.yellow(addDeltaSection('~', changed));
deltaString += chalk.red(addDeltaSection('-', removed));
return deltaString ? chalk.gray('Changes:\n') + deltaString : deltaString;
}
function addDeltaSection(prefix: string, arr: string[]): string {
return (
arr
.sort()
.map(item => `${prefix} ${item}`)
.join('\n') + '\n'
);
}

View File

@@ -1,3 +1,5 @@
import { isErrnoException } from '../is-error';
const knownErrorsCodes = new Set([ const knownErrorsCodes = new Set([
'PAYMENT_REQUIRED', 'PAYMENT_REQUIRED',
'BAD_REQUEST', 'BAD_REQUEST',
@@ -7,7 +9,8 @@ const knownErrorsCodes = new Set([
'ENV_SHOULD_BE_A_SECRET', 'ENV_SHOULD_BE_A_SECRET',
]); ]);
export function isKnownError(error: { code?: string }) { export function isKnownError(error: unknown) {
const code = error && typeof error.code === 'string' ? error.code : ''; const code = isErrnoException(error) ? error.code : null;
if (!code) return false;
return knownErrorsCodes.has(code.toUpperCase()); return knownErrorsCodes.has(code.toUpperCase());
} }

View File

@@ -87,3 +87,18 @@ export async function responseErrorMessage(
return `${message} (${res.status})`; return `${message} (${res.status})`;
} }
/**
* Returns a new Object with enumberable properties that match
* the provided `err` instance, for use with `JSON.stringify()`.
*/
export function toEnumerableError<E extends Partial<Error>>(err: E) {
const enumerable: {
[K in keyof E]?: E[K];
} = {};
enumerable.name = err.name;
for (const key of Object.getOwnPropertyNames(err) as (keyof E)[]) {
enumerable[key] = err[key];
}
return enumerable;
}

View File

@@ -5,6 +5,7 @@ import { NowError } from './now-error';
import code from './output/code'; import code from './output/code';
import { getCommandName } from './pkg-name'; import { getCommandName } from './pkg-name';
import chalk from 'chalk'; import chalk from 'chalk';
import { isError } from './is-error';
/** /**
* This error is thrown when there is an API error with a payload. The error * This error is thrown when there is an API error with a payload. The error
@@ -45,6 +46,10 @@ export class APIError extends Error {
} }
} }
export function isAPIError(v: unknown): v is APIError {
return isError(v) && 'status' in v;
}
/** /**
* When you're fetching information for the current team but the client can't * When you're fetching information for the current team but the client can't
* retrieve information. This means that the team was probably deleted or the * retrieve information. This means that the team was probably deleted or the

View File

@@ -81,21 +81,21 @@ async function printEvents(
return; return;
} }
poller = startPoller(); poller = startPoller();
} catch (error) { } catch (err: unknown) {
stream.end(); stream.end();
finish(error); finish(err);
} }
}, 5000); }, 5000);
})(); })();
} }
let finishCalled = false; let finishCalled = false;
function finish(error?: Error) { function finish(err?: unknown) {
if (finishCalled) return; if (finishCalled) return;
finishCalled = true; finishCalled = true;
clearTimeout(poller); clearTimeout(poller);
if (error) { if (err) {
reject(error); reject(err);
} else { } else {
resolve(); resolve();
} }

View File

@@ -10,6 +10,7 @@ import humanizePath from './humanize-path';
import readJSONFile from './read-json-file'; import readJSONFile from './read-json-file';
import { VercelConfig } from './dev/types'; import { VercelConfig } from './dev/types';
import { Output } from './output'; import { Output } from './output';
import { isErrnoException } from './is-error';
let config: VercelConfig; let config: VercelConfig;
@@ -25,8 +26,8 @@ export default async function getConfig(
let localPath: string; let localPath: string;
try { try {
localPath = process.cwd(); localPath = process.cwd();
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOENT') { if (isErrnoException(err) && err.code === 'ENOENT') {
return new WorkingDirectoryDoesNotExist(); return new WorkingDirectoryDoesNotExist();
} }
throw err; throw err;

View File

@@ -7,6 +7,7 @@ import {
Secret, Secret,
} from '../types'; } from '../types';
import getEnvRecords, { EnvRecordsSource } from './env/get-env-records'; import getEnvRecords, { EnvRecordsSource } from './env/get-env-records';
import { isAPIError } from './errors-ts';
export default async function getDecryptedEnvRecords( export default async function getDecryptedEnvRecords(
output: Output, output: Output,
@@ -39,12 +40,12 @@ export default async function getDecryptedEnvRecords(
); );
return { id, type, key, value: secret.value, found: true }; return { id, type, key, value: secret.value, found: true };
} catch (error) { } catch (err: unknown) {
if (error && error.status === 404) { if (isAPIError(err) && err.status === 404) {
return { id, type, key, value: '', found: false }; return { id, type, key, value: '', found: false };
} }
throw error; throw err;
} }
} }

View File

@@ -4,10 +4,7 @@ import errorOutput from './output/error';
import { APIError } from './errors-ts'; import { APIError } from './errors-ts';
import { getCommandName } from './pkg-name'; import { getCommandName } from './pkg-name';
export default function handleError( export default function handleError(error: unknown, { debug = false } = {}) {
error: string | Error | APIError,
{ debug = false } = {}
) {
// Coerce Strings to Error instances // Coerce Strings to Error instances
if (typeof error === 'string') { if (typeof error === 'string') {
error = new Error(error); error = new Error(error);

View File

@@ -14,7 +14,7 @@ interface ListSeparator {
separator: string; separator: string;
} }
type ListChoice = ListEntry | ListSeparator | typeof inquirer.Separator; export type ListChoice = ListEntry | ListSeparator | typeof inquirer.Separator;
interface ListOptions { interface ListOptions {
message: string; message: string;

View File

@@ -0,0 +1,82 @@
export interface SpawnError extends NodeJS.ErrnoException {
spawnargs: string[];
}
/**
* A simple type guard for objects.
*
* @param obj - A possible object
*/
export const isObject = (obj: unknown): obj is Record<string, unknown> =>
typeof obj === 'object' && obj !== null;
/**
* A type guard for `try...catch` errors.
*
* This function is based on:
* https://github.com/stdlib-js/assert-is-error
*/
export const isError = (error: unknown): error is Error => {
if (!isObject(error)) return false;
// Check for `Error` objects instantiated within the current global context.
if (error instanceof Error) return true;
// Walk the prototype tree until we find a matching object.
while (error) {
if (Object.prototype.toString.call(error) === '[object Error]') return true;
// eslint-disable-next-line no-param-reassign -- TODO: Fix eslint error following @vercel/style-guide migration
error = Object.getPrototypeOf(error);
}
return false;
};
export const isErrnoException = (
error: unknown
): error is NodeJS.ErrnoException => {
return isError(error) && 'code' in error;
};
interface ErrorLike {
message: string;
name?: string;
stack?: string;
}
/**
* A type guard for error-like objects.
*/
export const isErrorLike = (error: unknown): error is ErrorLike =>
isObject(error) && 'message' in error;
/**
* Parses errors to string, useful for getting the error message in a
* `try...catch` statement.
*/
export const errorToString = (error: unknown, fallback?: string): string => {
if (isError(error) || isErrorLike(error)) return error.message;
if (typeof error === 'string') return error;
return fallback ?? 'An unknown error has ocurred.';
};
/**
* Normalizes unknown errors to the Error type, useful for working with errors
* in a `try...catch` statement.
*/
export const normalizeError = (error: unknown): Error => {
if (isError(error)) return error;
const errorMessage = errorToString(error);
// Copy over additional properties if the object is error-like.
return isErrorLike(error)
? Object.assign(new Error(errorMessage), error)
: new Error(errorMessage);
};
export function isSpawnError(v: unknown): v is SpawnError {
return isErrnoException(v) && 'spawnargs' in v;
}

View File

@@ -27,6 +27,7 @@ import stamp from '../output/stamp';
import { EmojiLabel } from '../emoji'; import { EmojiLabel } from '../emoji';
import createDeploy from '../deploy/create-deploy'; import createDeploy from '../deploy/create-deploy';
import Now, { CreateOptions } from '../index'; import Now, { CreateOptions } from '../index';
import { isAPIError } from '../errors-ts';
export interface SetupAndLinkOptions { export interface SetupAndLinkOptions {
forceDelete?: boolean; forceDelete?: boolean;
@@ -96,15 +97,17 @@ export default async function setupAndLink(
'Which scope should contain your project?', 'Which scope should contain your project?',
autoConfirm autoConfirm
); );
} catch (err) { } catch (err: unknown) {
if (err.code === 'NOT_AUTHORIZED') { if (isAPIError(err)) {
output.prettyError(err); if (err.code === 'NOT_AUTHORIZED') {
return { status: 'error', exitCode: 1, reason: 'NOT_AUTHORIZED' }; output.prettyError(err);
} return { status: 'error', exitCode: 1, reason: 'NOT_AUTHORIZED' };
}
if (err.code === 'TEAM_DELETED') { if (err.code === 'TEAM_DELETED') {
output.prettyError(err); output.prettyError(err);
return { status: 'error', exitCode: 1, reason: 'TEAM_DELETED' }; return { status: 'error', exitCode: 1, reason: 'TEAM_DELETED' };
}
} }
throw err; throw err;

View File

@@ -6,6 +6,8 @@ import verify from './verify';
import executeLogin from './login'; import executeLogin from './login';
import Client from '../client'; import Client from '../client';
import { LoginResult } from './types'; import { LoginResult } from './types';
import { isAPIError } from '../errors-ts';
import { errorToString } from '../is-error';
export default async function doEmailLogin( export default async function doEmailLogin(
client: Client, client: Client,
@@ -22,8 +24,8 @@ export default async function doEmailLogin(
const data = await executeLogin(client, email); const data = await executeLogin(client, email);
verificationToken = data.token; verificationToken = data.token;
securityCode = data.securityCode; securityCode = data.securityCode;
} catch (err) { } catch (err: unknown) {
output.error(err.message); output.error(errorToString(err));
return 1; return 1;
} }
@@ -51,9 +53,9 @@ export default async function doEmailLogin(
'Email', 'Email',
ssoUserId ssoUserId
); );
} catch (err) { } catch (err: unknown) {
if (err.serverMessage !== 'Confirmation incomplete') { if (!isAPIError(err) || err.serverMessage !== 'Confirmation incomplete') {
output.error(err.message); output.error(errorToString(err));
return 1; return 1;
} }
} }

View File

@@ -1,5 +1,6 @@
import Client from '../client'; import Client from '../client';
import { InvalidEmail, AccountNotFound } from '../errors-ts'; import { InvalidEmail, AccountNotFound, isAPIError } from '../errors-ts';
import { errorToString } from '../is-error';
import { LoginData } from './types'; import { LoginData } from './types';
export default async function login( export default async function login(
@@ -11,18 +12,20 @@ export default async function login(
method: 'POST', method: 'POST',
body: { email }, body: { email },
}); });
} catch (err) { } catch (err: unknown) {
if (err.code === 'not_exists') { if (isAPIError(err)) {
throw new AccountNotFound( if (err.code === 'not_exists') {
email, throw new AccountNotFound(
`Please sign up: https://vercel.com/signup` email,
); `Please sign up: https://vercel.com/signup`
);
}
if (err.code === 'invalid_email') {
throw new InvalidEmail(email, err.message);
}
} }
if (err.code === 'invalid_email') { throw new Error(`Unexpected error: ${errorToString(err)}`);
throw new InvalidEmail(email, err.message);
}
throw new Error(`Unexpected error: ${err.message}`);
} }
} }

View File

@@ -68,7 +68,7 @@ export async function readInput(
message, message,
}); });
input = val; input = val;
} catch (err) { } catch (err: any) {
console.log(); // \n console.log(); // \n
if (err.isTtyError) { if (err.isTtyError) {

View File

@@ -1,5 +1,4 @@
import chalk from 'chalk'; import chalk from 'chalk';
import title from 'title';
import bytes from 'bytes'; import bytes from 'bytes';
import { isReady, isFailed } from '../build-state'; import { isReady, isFailed } from '../build-state';
import { Build, BuildOutput } from '../../types'; import { Build, BuildOutput } from '../../types';
@@ -8,9 +7,6 @@ export interface Times {
[id: string]: string | null; [id: string]: string | null;
} }
// That's how long the word "Initializing" is
const longestState = 12;
// That's the spacing between the source, state and time // That's the spacing between the source, state and time
const padding = 8; const padding = 8;
@@ -18,8 +14,6 @@ const padding = 8;
const MAX_BUILD_GROUPS = 5; const MAX_BUILD_GROUPS = 5;
const MAX_OUTPUTS_PER_GROUP = 5; const MAX_OUTPUTS_PER_GROUP = 5;
const prepareState = (state: string) => title(state.replace('_', ' '));
const hasOutput = (b: Build) => Array.isArray(b.output) && b.output.length > 0; const hasOutput = (b: Build) => Array.isArray(b.output) && b.output.length > 0;
// Get the common path out of multiple builds // Get the common path out of multiple builds
@@ -47,24 +41,19 @@ const getCommonPath = (buildGroup: Build[]) => {
}; };
const styleBuild = (build: Build, times: Times, longestSource: number) => { const styleBuild = (build: Build, times: Times, longestSource: number) => {
const { entrypoint, readyState, id } = build; const { entrypoint, id } = build;
const state = prepareState(readyState).padEnd(longestState + padding);
const time = typeof times[id] === 'string' ? times[id] : ''; const time = typeof times[id] === 'string' ? times[id] : '';
let stateColor = chalk.grey;
let pathColor = chalk.cyan; let pathColor = chalk.cyan;
if (isReady(build)) { if (isFailed(build)) {
stateColor = chalk;
} else if (isFailed(build)) {
stateColor = chalk.red;
pathColor = chalk.red; pathColor = chalk.red;
} }
const entry = entrypoint.padEnd(longestSource + padding); const entry = entrypoint.padEnd(longestSource + padding);
const prefix = hasOutput(build) ? '┌' : '╶'; const prefix = hasOutput(build) ? '┌' : '╶';
return `${chalk.grey(prefix)} ${pathColor(entry)}${stateColor(state)}${time}`; return `${chalk.grey(prefix)} ${pathColor(entry)}${time}`;
}; };
const styleHiddenBuilds = ( const styleHiddenBuilds = (
@@ -79,43 +68,9 @@ const styleHiddenBuilds = (
const time = typeof times[id] === 'string' ? times[id] : ''; const time = typeof times[id] === 'string' ? times[id] : '';
const prefix = isHidden === false && buildGroup.some(hasOutput) ? '┌' : '╶'; const prefix = isHidden === false && buildGroup.some(hasOutput) ? '┌' : '╶';
// Set the defaults so that they will be sorted
const stateMap: { [readyState: string]: number } = {
READY: 0,
ERROR: 0,
BUILDING: 0,
};
for (const { readyState } of buildGroup) {
stateMap[readyState]++;
}
let state = Object.keys(stateMap)
.map(readyState => {
const counter = stateMap[readyState];
const name = prepareState(readyState);
if (!counter) {
return null;
}
return `${counter > 9 ? '9+' : counter} ${name}`;
})
.filter(s => s)
.join(', ');
// Since the longestState might still be shorter
// than multiple states we still want to ensure
// a space between the states and the time
state = `${state} `.padEnd(longestState + padding);
let pathColor = chalk.cyan; let pathColor = chalk.cyan;
let stateColor = chalk.grey;
if (buildGroup.every(isReady)) { if (buildGroup.every(isFailed)) {
stateColor = chalk;
} else if (buildGroup.every(isFailed)) {
stateColor = chalk.red;
pathColor = chalk.red; pathColor = chalk.red;
} }
@@ -123,7 +78,7 @@ const styleHiddenBuilds = (
pathColor = chalk.grey; pathColor = chalk.grey;
} }
return `${chalk.grey(prefix)} ${pathColor(entry)}${stateColor(state)}${time}`; return `${chalk.grey(prefix)} ${pathColor(entry)}${time}`;
}; };
const styleOutput = ( const styleOutput = (

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