Compare commits

..

97 Commits

Author SHA1 Message Date
Steven
041e8cc601 Publish Canary
- @vercel/build-utils@2.4.3-canary.0
 - vercel@20.0.0-canary.2
 - @vercel/client@8.2.2-canary.1
 - @vercel/static-build@0.17.7-canary.1
 - @vercel/redwood@0.0.2-canary.0
2020-07-23 16:28:44 -04:00
Steven
b118e461b1 [redwoood] Add runtime @vercel/redwood (#4676)
### Changes
- Add new Runtime: `@vercel/redwood`
- Update `run.js` script to sort packages properly

### Related
- Related to #4539 
- Related to #4535
- Depends on https://github.com/apollographql/apollo-server/pull/4311
2020-07-23 16:22:48 -04:00
Andy Bitz
e519d49d7b Publish Canary
- vercel@20.0.0-canary.1
2020-07-23 15:42:46 +02:00
Andy Bitz
27683818ba Bump vercel to 20.0.0-canary.0 2020-07-23 15:42:30 +02:00
Andy
e016e38229 [cli] (Major) Change vc domains add workflow to match the dashboard (#3325)
* [now-cli] Change `now domains add` workflow to match dashboard

* Fix error throwing and add --force

* Remove `now domains verify`

* Count projects when removing a domain

* Include projects for `now inspect`

* Updated `now domains ls`

* Fix `now domains ls`

* Use Finally

* Add tests

* Use --force for domain tests

* Adjust tests

* Adjust test and fix output

* Fix padding

* Adjust more tests

* Ensure the project exists

* Log the deployment

* Fix tests again

* Remove project after test

* Fix command

* Fix test

* Change user after a couple of tests

* Add check

* Use random name for domain

* More logging

* Add more logging

* Remove logging

* Remove .now/project.json every time

* Skip test

* Add test to change user

* More logging

* Use now login

* Remove duplicated test

* Consider linked project for `domains add`

* Fix linting

* Remove unused docs

* Undo changes

* Undo more changes

* Fix typo

* Do not sort projects when there is only one record

* Fix loop

* Deploy with `-V 2`

* Remove verification

* Remove outdated link

* Update packages/now-cli/src/commands/domains/add.ts

Co-authored-by: Steven <steven@ceriously.com>

* Update packages/now-cli/src/commands/domains/add.ts

Co-authored-by: Steven <steven@ceriously.com>

* Use utils for public suffix and service type

* Update packages/now-cli/src/commands/domains/ls.ts

Co-authored-by: Steven <steven@ceriously.com>

* Update packages/now-cli/src/commands/domains/add.ts

Co-authored-by: Steven <steven@ceriously.com>

* Change link

* Undo test changes

* Fix type issues

* Update packages/now-cli/src/commands/domains/add.ts

Co-authored-by: Steven <steven@ceriously.com>

* Use domain config to print info

* Check apex domain when removing a domain

* Remove psl

* List projects when removing a domain

* Do not list projects

* Change user earlier

* Update packages/now-cli/src/commands/domains/inspect.ts

Co-authored-by: Steven <steven@ceriously.com>

* Apply suggestions from code review

Co-authored-by: Steven <steven@ceriously.com>

* Apply suggestions from code review

Co-authored-by: Steven <steven@ceriously.com>

Co-authored-by: Mark Glagola <mark.glagola@gmail.com>
Co-authored-by: Steven <steven@ceriously.com>
2020-07-23 15:41:25 +02:00
Max Leiter
5db1c5e610 [tests] Skip try to purchase a domain CLI test if a VERCEL_TOKEN is provided (#4909)
I placed the check inside the test because otherwise `test.only` and other test functions break with this test.
2020-07-23 01:39:05 +00:00
Steven
24c228569f Publish Canary
- vercel@19.2.1-canary.2
 - @vercel/next@2.6.14-canary.1
2020-07-22 16:09:26 -04:00
Steven
963de9b64f [cli] Update "set up" message (#4907)
Follow up to #4897 

* `vc link` - Update message to say `Set up ~/proj`
* `vc dev` - Update message  to say `Set up and develop ~/proj`
* `vc link --help` - Updated example message to include `vc link ~/proj` usage
2020-07-22 19:13:41 +00:00
JJ Kasper
ab7fd52305 [next] Don't trace pages for lambdas when not needed (#4841)
When pages are fully static and don't leverage preview mode or revalidation we can skip tracing the pages for generating the lambdas since lambdas aren't needed in this case
2020-07-22 18:14:47 +00:00
Nathan Rajlich
0fdb0dac91 [cli] Send the Vercel platform proxy request headers to frontend dev server (#4874)
This is a follow up to #4850 that does the same thing when
proxying to the frontend dev server.
2020-07-22 14:36:15 +00:00
Andy Bitz
bb0b632dcf Publish Canary
- vercel@19.2.1-canary.1
 - @vercel/go@1.1.5-canary.0
 - @vercel/next@2.6.14-canary.0
 - @vercel/node@1.7.4-canary.0
 - @vercel/static-build@0.17.7-canary.0
2020-07-22 13:34:40 +02:00
Andy
ced9495143 Revert "Revert "[go] Implement startDevServer() function (#4662)" (#4899)" (#4906)
This reverts commit 8d1afc026f.
2020-07-22 13:33:36 +02:00
Nathan Rajlich
fadc3f2588 [next][node][static-build] Execute "vercel-build" script if defined (#4863)
* [build-utils] Make `runPackageJsonScript()` run the `vercel-` or `now-` if defined in `package.json`

* [build-utils] Export `getScriptName()`

* [next] Use `getScriptName()` and `remove()`

* [node] Update for `vercel-`

* [static-build] Update for `vercel-`

* Remove debug

* Add `getScriptName()` unit tests

* Test for `vercel-build` in e2e

* Make platform name behavior be opt-in

So that it's not a breaking behavior change.

* Check for only "vercel-build" or "now-build", but not "build"

* Simplify `getScriptName()` to return the first existing script in a possible set

* Revert change

* Fix test

Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-07-21 20:04:24 -07:00
Steven
a1d548dfef Publish Canary
- vercel@19.2.1-canary.0
 - @vercel/client@8.2.2-canary.0
2020-07-21 18:56:21 -04:00
Steven
754090a8ab [cli] Add vercel link command (#4897)
- [x] Add `vercel link` command
- [x] Update `vercel dev` to automatically link 
- [x] Updated `vercel env` error message to mention `vercel link`
- [x] Test `vercel link`
- [x] Test `vercel dev`
- [x] Add `--confirm` flag

### Related

- Fixes #3767
- Fixes #4889 
- Fixes #3991 
- Fixes #4534 
- Related to #3989
2020-07-21 18:52:46 -04:00
dependabot[bot]
8269a48ee0 Bump codecov from 3.6.5 to 3.7.1 (#4900)
Bumps [codecov](https://github.com/codecov/codecov-node) from 3.6.5 to 3.7.1.
<details>
<summary>Commits</summary>
<ul>
<li><a href="29dd5b6b03"><code>29dd5b6</code></a> 3.7.1</li>
<li><a href="c0711c6566"><code>c0711c6</code></a> Switch from execSync to execFileSync (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/180">#180</a>)</li>
<li><a href="5f6cc62d4f"><code>5f6cc62</code></a> Bump lodash from 4.17.15 to 4.17.19 (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/183">#183</a>)</li>
<li><a href="0c4d7f3c6f"><code>0c4d7f3</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/182">#182</a> from codecov/update-readme-badges</li>
<li><a href="cc5e121842"><code>cc5e121</code></a> Update depstat image and urls</li>
<li><a href="b44b44e1f8"><code>b44b44e</code></a> Update readme with 400 error info (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/181">#181</a>)</li>
<li><a href="bb79335719"><code>bb79335</code></a> V3.7.0 (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/179">#179</a>)</li>
<li><a href="0d7b9b01cb"><code>0d7b9b0</code></a> Remove <code>'x-amz-acl': 'public-read'</code> header (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/178">#178</a>)</li>
<li><a href="eeff4e1953"><code>eeff4e1</code></a> Bump acorn from 5.7.3 to 5.7.4 (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/174">#174</a>)</li>
<li><a href="eb8a527470"><code>eb8a527</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/172">#172</a> from RoboCafaz/bugfix/codebuild-pr-parser</li>
<li>Additional commits viewable in <a href="https://github.com/codecov/codecov-node/compare/v3.6.5...v3.7.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov&package-manager=npm_and_yarn&previous-version=3.6.5&new-version=3.7.1)](https://help.github.com/articles/configuring-automated-security-fixes)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/vercel/vercel/network/alerts).

</details>
2020-07-21 17:25:56 +00:00
Andy Bitz
9f05a1865c Publish Stable
- @vercel/frameworks@0.0.17
 - @vercel/build-utils@2.4.2
 - vercel@19.2.0
 - @vercel/client@8.2.1
 - @vercel/go@1.1.4
 - @vercel/next@2.6.13
 - @vercel/node@1.7.3
 - @vercel/static-build@0.17.6
2020-07-21 14:58:27 +02:00
Andy
8d1afc026f Revert "[go] Implement startDevServer() function (#4662)" (#4899)
This reverts commit 04bea1e3cd.
2020-07-21 14:55:59 +02:00
Nathan Rajlich
130f36aad6 Publish Canary
- vercel@19.1.3-canary.6
2020-07-20 23:11:46 -07:00
Nathan Rajlich
dd87c9b0c6 Publish Canary
- vercel@19.1.3-canary.5
 - @vercel/next@2.6.13-canary.2
2020-07-20 19:51:15 -07:00
Max Leiter
f813b3340b [cli] Fetch environment variables from Project Settings if .env file is not present (#4562)
Previously, users would have to run `vc env pull` to fetch cloud environment
variables into `.env`. After this PR, if no `.env` or `.build.env` file is present,
environment variables will be pulled by `vc dev` from your Vercel Environment
Variables settings, no file necessary.
2020-07-20 19:47:13 -07:00
Joe Haddad
976b02e895 [next] Add tests for trailing slash behavior (#4894) 2020-07-20 22:29:09 -04:00
Nathan Rajlich
843be9658c [cli] Use update-notifier instead of update-check (#4896)
This makes the updating logic be asynchronous instead of synchronous, and as such will make boot-up of CLI be faster.

The actual update notification display is identical to previous, we are not using `update-notifier`'s default boxen rendering.
2020-07-20 22:31:15 +00:00
dependabot[bot]
ad501a4cd0 Bump lodash from 4.17.15 to 4.17.19 (#4877)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-20 14:00:32 -04:00
Steven
727ae587db Publish Canary
- @vercel/frameworks@0.0.17-canary.2
 - @vercel/build-utils@2.4.2-canary.1
 - vercel@19.1.3-canary.4
 - @vercel/client@8.2.1-canary.1
 - @vercel/next@2.6.13-canary.1
 - @vercel/node@1.7.3-canary.0
2020-07-20 10:27:10 -04:00
Steven
536b15079b [node][next] Bump node-file-trace to 0.8.0 (#4814)
Bump `node-file-trace` to version [0.7.0](https://github.com/vercel/node-file-trace/releases/tag/0.7.0) and then [0.8.0](https://github.com/vercel/node-file-trace/releases/tag/0.8.0)

- Add support for `exports`, dynamic import from CJS support, package boundary emission
- Fix trailing slash in `require()`
- Import named instead of default
2020-07-20 10:24:05 -04:00
Nathan Rajlich
887882697b [cli] Remove "npm pack" logic from output-is-source dev test (#4870)
Now that `@vercel/static-build` is being symlinked to CLI due to the
yarn workspace, `vc dev` will always use the latest code for
zero-config, so this "npm pack" logic is no longer necessary.

The `vercel.json` also no longer gets created in this test so that
zero-config is used.
2020-07-16 16:44:28 -07:00
Nathan Rajlich
e2db7c7734 [build-utils] Add getScriptName() function (#4869)
And allow a set of possible script names to use in `runPackageJsonScript()`.

These are only the changes to `@vercel/build-utils` from #4863.
2020-07-16 16:42:55 -07:00
Steven
022504787c [frameworks][examples] Update blitz new command (#4868)
Updated description per @leo
2020-07-16 22:16:03 +00:00
Nathan Rajlich
0f424de406 [cli] Set server: Vercel header and remove x-now response headers in vercel dev (#4867)
This matches production.
2020-07-16 20:23:44 +00:00
Nathan Rajlich
4819c3ac61 Publish Canary
- vercel@19.1.3-canary.3
2020-07-15 11:04:16 -07:00
Nathan Rajlich
c28ca7ef2d [cli] Set Vercel platform env vars for startDevServer() invocations (#4855)
Ensures that `VERCEL_REGION` is set for API calls when the runtime
implements `startDevServer()`.

Also adds `VERCEL_URL` which was previously not defined.
2020-07-15 10:34:27 -07:00
Nathan Rajlich
068ea00615 [cli] Set Vercel proxy request headers in startDevServer() (#4850)
The Vercel proxy request headers (`x-vercel-deployment-url` for example)
were not being sent to the dev server when the Runtime defines
`startDevServer()`. This is now fixed.

Also updates the header names to match production:

 - `now` -> `vercel`
 - 'x-now-trace', 'x-now-id', and 'x-now-log-id' are removed

Fixes #4729.
2020-07-14 14:48:46 -07:00
Paco
7f8145ab40 [cli] Minor text changes to wrong config error message (#4848)
CH-3242

From review:
- Capitalize "Project"
- Highlight ".vercel" in code snippet style

Thanks to @styfle for pointing me to output/code.
2020-07-14 13:53:23 +00:00
Steven
1a12715096 Publish Canary
- @vercel/build-utils@2.4.2-canary.0
 - vercel@19.1.3-canary.2
 - @vercel/client@8.2.1-canary.0
 - @vercel/next@2.6.13-canary.0
2020-07-13 18:57:39 -04:00
Steven
5b6d565360 [build-utils] Update error message for unmatched function pattern (#4849)
- [x] Depends on https://github.com/vercel/docs/pull/1985
2020-07-13 22:55:15 +00:00
JJ Kasper
0794158906 Publish Stable
- @vercel/next@2.6.12
2020-07-12 10:30:51 -05:00
JJ Kasper
02cdb88d3b Publish Canary
- vercel@19.1.3-canary.1
 - @vercel/next@2.6.12-canary.0
2020-07-12 10:25:27 -05:00
Paco
fa58855114 [cli] Show helpful error when using unauthorized .vercel config (#4807)
CH-3242

When a user has project configuration in `.vercel` that corresponds to a team that they are not a part of, we currently throw a 403 error. This adds special handling so that we can show a specific message when this happens.

I didn't want to update the return type of `getProjectByIdOrName` because we only want to handle this 403 in a special case, so I `throw` it instead.

Other changes are linter.
2020-07-11 03:52:02 +00:00
JJ Kasper
1fac11792f [next] Fix nested public files being renamed incorrectly (#4817)
This corrects the file rename for public files to not incorrectly rename nested `public` files

x-ref: https://github.com/vercel-support/next-nested-public-dir
2020-07-10 23:52:54 +00:00
Nathan Rajlich
05ffc9ce2b Publish Canary
- @vercel/frameworks@0.0.17-canary.1
 - vercel@19.1.3-canary.0
 - @vercel/go@1.1.4-canary.0
 - @vercel/static-build@0.17.6-canary.1
2020-07-10 15:51:19 -07:00
Nathan Rajlich
f848551043 [cli] Pin @vercel/static-build to "0.17.6-canary.0" (#4837)
It got de-synced in c68e83f972.
2020-07-10 22:49:35 +00:00
Steven
9712abc5bf [frameworks][api] Move dependency to devdependency (#4836)
Follow up to #4829
2020-07-10 16:38:19 -04:00
Nathan Rajlich
a9ef3cc726 [go] Add filesystem write fallback for startDevServer() (#4830)
This is for `@vercel/go` to work on Windows, which currently fails with
this error:

```
panic: write pipe: The handle is invalid.

goroutine 1 [running]:
main.main()
	C:/Users/Nathan/Code/casca/vercel-go-test/.vercel/cache/go/2flefmhra8o/api/vercel-dev-server-main.go:25 +0x1ec
exit status 2
```

So this fallback writes the port number to a temp file that
`startDevServer()` polls for in order to figure out the port number.
2020-07-10 19:55:53 +00:00
Steven
e5cc1d643a [frameworks][api] Add sort order (#4829)
This PR adds an optional property called `sort` to each framework so that we can change the order returned in the API. 
The reason this is necessary is because the order of the original array determines the precedence of framework detection. So we need another way to indicate the order of templates/examples returned from the API.

In particular, we need "Next.js" to be first and "Other" to be last.

I also updated the deprecated `@now/node` usage to `@vercel/node` in the API.
2020-07-10 18:09:14 +00:00
Andy Bitz
c68e83f972 Publish Canary
- @vercel/frameworks@0.0.17-canary.0
 - @vercel/static-build@0.17.6-canary.0
2020-07-10 18:45:47 +02:00
Sébastien Chopin
80118de040 [build-utils] Add Nuxt cache and default routes (#4755)
* [@vercel/build-utils] Improve Nuxt build

Need review from @pi0 and @danielroe

* Update packages/now-static-build/src/frameworks.ts

Co-authored-by: Daniel Roe <daniel@roe.dev>

Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
2020-07-10 18:41:02 +02:00
Sébastien Chopin
c69da18e9c fix: Also handle nuxt-edge detection (#4756)
Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
2020-07-10 17:55:29 +02:00
Steven
e19446f89c Publish Stable
- @vercel/frameworks@0.0.16
 - @vercel/build-utils@2.4.1
 - vercel@19.1.2
 - @vercel/client@8.2.0
 - @vercel/go@1.1.3
 - @vercel/next@2.6.11
 - @vercel/node@1.7.2
 - @vercel/ruby@1.2.3
 - @vercel/static-build@0.17.5
2020-07-10 08:20:44 -07:00
JJ Kasper
ea3233502d Publish Stable
- @vercel/next@2.6.10
2020-07-09 21:30:31 -05:00
JJ Kasper
0a8810b64f Publish Canary
- vercel@19.1.2-canary.22
 - @vercel/next@2.6.10-canary.2
2020-07-09 21:29:18 -05:00
JJ Kasper
f6b373f0f4 [next] Fix preview mode with dynamic routes in shared lambdas (#4812)
This fixes dynamic routes missing inside of shared lambdas for non-revalidate pages that leverage preview mode which causes them to not be able to render. Additional tests have also been added to ensure preview mode is working properly
2020-07-10 02:22:17 +00:00
Steven
7ad2a99cd7 Publish Canary
- @vercel/frameworks@0.0.16-canary.2
 - @vercel/build-utils@2.4.1-canary.3
 - vercel@19.1.2-canary.21
 - @vercel/client@8.1.1-canary.3
 - @vercel/next@2.6.10-canary.1
 - @vercel/static-build@0.17.5-canary.1
2020-07-09 16:42:50 -07:00
JJ Kasper
0349eea494 [next] Add pages with invalid route regexes (#4813)
This adds failing page names to our tests which should be resolved once the correct regexes is being generated for the routes-manifest in Next.js

Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
Co-authored-by: Steven <steven@ceriously.com>
2020-07-09 19:37:13 -04:00
Luc
ed4d006fb7 replace "more details" with "learn more" (#4828)
Follow up to PR #4825. Replace more "More details" with "Learn More".
2020-07-09 19:31:04 -04:00
Steven
12a9d203e9 [cli] Handle 400 errors as user errors (#4827)
When a user performs an action such as trying to add a domain they don't own, the API returns a 400-499 response code.

Previously, we would save this error in Sentry but this PR changes the behavior so that the error is printed locally for the user but not saved in Sentry.
2020-07-09 21:57:18 +00:00
Luc
ac1f4cf789 [cli] Handle API Error action property (#4825)
* handle error action

* fix test
2020-07-09 22:14:30 +02:00
Steven
b284ca350a Revert "[frameworks] Remove blitz demo (#4816)" (#4826)
This reverts commit f11eb32b2c.

Reverts PR #4816

This PR adds back the Blitz.js demo now that we confirmed the example works and is deployed to the correct account.
2020-07-09 13:28:38 -04:00
Steven
a43bf6c912 [cli][static-build] Use titlecase for "Project Settings" (#4820)
"project settings" should be "Project Settings"
2020-07-09 11:56:11 -04:00
Nathan Rajlich
62410806bb Publish Canary
- vercel@19.1.2-canary.20
 - @vercel/next@2.6.10-canary.0
2020-07-08 16:24:18 -07:00
Steven
47e3111cab [all] Change prettier config arrowParens: avoid (#4819)
In PR #4760, Prettier was upgraded to 2.0 which changed the default configuration for `arrowParens` from `avoid` to `always`. This PR changes it back to `avoid` so we don't have PRs changing unnecessary lines during in the commit hook.

See the [Prettier 2.0 Announcement](https://prettier.io/blog/2020/03/21/2.0.0.html#change-default-value-for-arrowparens-to-always-7430httpsgithubcomprettierprettierpull7430-by-kachkaevhttpsgithubcomkachkaev) for more.
2020-07-08 18:46:48 -04:00
Nathan Rajlich
135f35002f [cli] Remove caching of vercel.json config in vc dev (#4808)
This is a re-introduction of #4697, which was reverted for performance
resons. But now that #4760 and #4793 have landed, we can enable this
behavior again.
2020-07-08 22:37:55 +00:00
Steven
ee40052cee Publish Stable
- @vercel/next@2.6.9
2020-07-08 13:53:18 -07:00
JJ Kasper
86b730c1cd Publish Canary
- vercel@19.1.2-canary.19
 - @vercel/next@2.6.9-canary.1
2020-07-08 15:08:29 -05:00
JJ Kasper
b440249c26 [next] Don't use potentially invalid routeKeys/regexes (#4803)
The first version of `routeKeys` which was Array based could contain invalid named regexes which would break the build during route validation so this disables using them in that version of the manifest instead of trying to leverage them still. It also adds an additional test fixture with this version of the manifest to ensure it is still working properly
2020-07-08 20:05:00 +00:00
Steven
5380c12569 Publish Canary
- @vercel/frameworks@0.0.16-canary.1
 - @vercel/build-utils@2.4.1-canary.2
 - vercel@19.1.2-canary.18
 - @vercel/client@8.1.1-canary.2
 - @vercel/next@2.6.9-canary.0
2020-07-08 11:27:30 -07:00
Steven
f11eb32b2c [frameworks] Remove blitz demo (#4816)
This is not ready yet. We need to confirm Blitz is working before we deploy the demo.
2020-07-08 14:19:55 -04:00
Brandon Bayer
3d40e343ac [frameworks][examples] Add Blitz.js (#4465)
This adds [Blitz.js](https://github.com/blitz-js/blitz) to the zero-config frameworks.

Blitz compiles to a Next.js app, so it uses the same `@vercel/next` builder.

The change in `packages/now-build-utils/src/detect-builders.ts` was made according to @styfle's suggestion in Slack.

---

**This is a rerun of #4219 which had to be reverted because of this:**

> It looks like the @vercel/next package still requires next in the dependencies.
>
> When I've deployed the example I've got only a file listing, but when I added next to the dependencies it worked. I'll revert for now.

This new PR fixes that problem with a small refactor to `@vercel/next`.

`@vercel/next` was getting the Next version two different ways:

1. By checking project root package.json for `next` in deps or devDeps
2. By trying to resolve the installed next version and get `version` from its package.json

My refactor consolidates those two approaches by changing the existing `getNextVersion()` function to first try and resolve the real next version. If that fails, fall back to checking the project root.

Blitz bundles Next, so the real next package version should always be resolved.
2020-07-08 13:18:01 -04:00
Nathan Rajlich
80f525796f [cli] Make filesystem globbing faster in vercel dev (#4793)
The current code uses `getAllProjectFiles()` which globs for every file
in the project, not considering the `.vercelignore` file and default
ignore list. For example, every file in the `node_modules` directory is
selected, just for `vercel dev` to manually ignore them afterwards,
which is very slow and wasteful. For a simple Next.js project, this
globbing was taking over 3 seconds on my machine 🤯!

Solution is to use the `staticFiles()` function which is the same as
`vercel dev` uses at boot-up time. This function properly considers the
ignore list and thus is much faster, and the manual filtering is no
longer necessary by `vercel dev`. For the same simple Next.js project,
this function takes on average 10 milliseconds.

Also did a bit of cleanup and removed the `getAllProjectFiles()`
function since it is no longer used anywhere.
2020-07-07 20:30:21 +00:00
Nathan Rajlich
af4ad358f2 [cli] Print a warning if both "functions" and "builds" are defined in vercel.json in vercel dev (#4795)
This matches the error message that is used in production when using the
`vercel dev` command.
2020-07-07 19:41:29 +00:00
Nathan Rajlich
e6033d7a2d [client] Add @vercel/build-utils as a "dependency" (#4804)
Fixes: https://github.com/philcockfield/sample.vercel.client-error
2020-07-07 18:56:31 +00:00
JJ Kasper
d3148dffaa Publish Stable
- @vercel/next@2.6.8
2020-07-06 08:40:44 -05:00
JJ Kasper
30048cf4ff Publish Canary
- vercel@19.1.2-canary.17
 - @vercel/next@2.6.8-canary.7
2020-07-06 08:31:35 -05:00
JJ Kasper
07c65fa5c8 [next] Use correct data route regex (#4778)
For `getServerSideProps` routes the non-named data route was being used for routes causing dynamic route params that were being trusted from the proxy to be incorrect. Our tests weren't failing after trusting the values from the proxy was landed in Next.js on canary due to the `21-server-props` test suite not running against the latest canary which is updated in this PR. 

Note: the below Next.js PR is not needed or can be reverted if this PR is landed

x-ref: https://github.com/vercel/next.js/pull/14829
2020-07-05 21:13:01 +00:00
Nathan Rajlich
411ec64986 [cli] Reduce number of times getNowConfig() is invoked in vercel dev (#4760)
* [cli] Reduce number of times `getNowConfig()` is invoked in `vercel dev`

When using the `vercel dev` command, the `getNowConfig()` function was
being invoked many times per HTTP-request. It should only be read _once_
per HTTP request as it may be an expensive function depending on the
project size.

This change reduces the number of times the function is called to once
per HTTP request, and passes around the resulting `nowConfig` object to
the functions that require it.

* Fix default values

* Remove unnecessary type narrowing

* Remove one more `getNowConfig()` invocation

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-07-02 17:25:07 -07:00
Joe Haddad
e4d2cc704c Add Lockfile to Fix Gatsby Example (#4771)
This adds the Lockfile from Gatsby's official starter to fix deployments.

```
❯ npx gatsby-cli new ~/Desktop/scratch/test-default
❯ cp ~/Desktop/scratch/test-default/yarn.lock examples/gatsby
❯ cd examples/gatsby
❯ yarn
❯ git add examples/gatsby/yarn.lock
```
2020-07-02 21:06:44 +00:00
Joe Haddad
38db720586 Regenerate yarn.lock file (#4769) 2020-07-02 11:20:35 -07:00
Joe Haddad
c26c7886be [next] Skip Lambda Deployment for Static Sites (#4748) 2020-07-02 11:51:02 -04:00
JJ Kasper
b0e5d308ca [next] Add additiontal test cases for basePath support (#4747)
This adds some additional test cases for `basePath` support in Next.js to ensure it is working properly with `@vercel/next`
2020-07-02 09:40:15 +00:00
JJ Kasper
609b98cc73 Publish Canary
- vercel@19.1.2-canary.16
 - @vercel/next@2.6.8-canary.6
2020-07-01 14:46:28 -05:00
JJ Kasper
e0ec6c792b [next] Re-enable shared lambdas by default (#4757)
This re-enables the shared lambdas optimization by default and also adds additional tests to ensure that when a `now.json` or `vercel.json` contains either `routes` or `functions` configs the shared lambda optimization is disabled. 

Additional test deployments done:

- minimal `now.json` with `builds` config [deploy](https://shared-lambdas-tests-d646fsqju.vercel.app)
- `now.json` with `functions` config [deploy](https://shared-lambdas-tests-ahnuosp4s.vercel.app)
- `now.json` with `routes` config [deploy](https://shared-lambdas-tests-gulam3jda.vercel.app)
- minimal `vercel.json` with `builds` config [deploy](https://shared-lambdas-tests-7ic7wzirs.vercel.app)
- `vercel.json` with `functions` config [deploy](https://shared-lambdas-tests-7ic7wzirs.vercel.app)
- `vercel.json` with `routes` config [deploy](https://shared-lambdas-tests-rja2391tq.vercel.app)
2020-07-01 19:44:09 +00:00
Nathan Rajlich
b29db2fd1d [cli] Pin @vercel/static-build to v0.17.5-canary.0 (#4751)
It got de-synced in fa0f1b90b4.
2020-07-01 10:59:22 -07:00
JJ Kasper
b604ced99d Publish Canary
- vercel@19.1.2-canary.15
 - @vercel/next@2.6.8-canary.5
2020-07-01 10:53:18 -05:00
JJ Kasper
b7fd69517e [next] Fix dynamic route mapping with shared lambdas (#4740)
This fixes mapping dynamic routes to their shared lambdas failing from the `dynamicPageLambdaRoute` not being injected directly after the dynamic route due to dynamic routes now containing a `query` which caused the route lookup to fail. 

This is resolved by making sure to parse the pathname while injecting the `dynamicPageLambdaRoute` so the route can be looked up correctly. This also adds additional test cases to prevent regressing on this along with updating existing fixtures for custom routes being de-experimentalized
2020-07-01 07:30:41 +00:00
Nathan Rajlich
4c821a6fb5 Publish Canary
- vercel@19.1.2-canary.14
 - @vercel/node@1.7.2-canary.1
 - @vercel/static-build@0.17.5-canary.0
2020-06-30 16:27:46 -07:00
Nathan Rajlich
5c4fb319af [cli] Revert "Remove caching of vercel.json config in vc dev" (#4749)
This reverts commit ffb98781f1 (#4697),
because it was realized that `vc dev` reads this configuration file many
times per HTTP request, causing the server to feel extremely slow.

Reverting this optimization for now until the over-reading issue is
addressed.
2020-06-30 16:26:56 -07:00
Nathan Rajlich
fbe9ea0750 [node] Set the TypeScript "target" in vc dev (#4728)
Fixes: https://github.com/vercel/vercel/discussions/4724
2020-06-30 16:07:04 -07:00
Andy Bitz
fa0f1b90b4 Publish Stable 2020-06-30 23:33:47 +02:00
Andy Bitz
2daa0e28d3 Publish Canary
- @vercel/frameworks@0.0.16-canary.0
 - vercel@19.1.2-canary.13
 - @vercel/static-build@0.17.4-canary.1
2020-06-30 13:21:02 +02:00
Andy
48358b4986 [@vercel/build-utils] Cache .cache for Eleventy (#4743) 2020-06-30 12:54:44 +02:00
Steven
c59f44a63b [frameworks] Fix build command placeholder nuxt generate (#4737)
The Nuxt placeholder was wrong, it should be `nuxt generate` like the build command here: ca27864201/packages/now-static-build/src/frameworks.ts (L516)
2020-06-29 11:59:34 -04:00
Joe Haddad
ca27864201 Update "description" in package.json for Vercel (#4735) 2020-06-28 22:05:30 -07:00
Steven
6c81a87338 Publish Canary
- vercel@19.1.2-canary.12
 - @vercel/static-build@0.17.4-canary.0
2020-06-27 01:02:10 -04:00
Steven
a7bef9387b [static-build] Fix error handling for unexpected output directory (#4727)
This PR improves the error handling when a zero config framework has an unexpected output directory.

Previously, setting a Docusaurus 2.0 build command to `docusaurus build && mv build foo` would fail with the following:

```
Error: ENOENT: no such file or directory, scandir '/vercel/514ce14b/build'
```

With this PR, the error message will show the expected:

```
Error: No Output Directory named "build" found after the Build completed. You can configure the Output Directory in your project settings. Learn more: https://vercel.com/docs/v2/platform/frequently-asked-questions#missing-public-directory
```

I also changed the usage of [`promisify(fs)`](https://nodejs.org/docs/latest-v10.x/api/util.html#util_util_promisify_original) to [`fs.promises`](https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_promises_api) which is available in Node 10 or newer.

Lastly, I updated the test suite to check if the correct error message is returned for builds we expect to fail.
2020-06-26 23:56:45 -04:00
Nathan Rajlich
65d6c5e1f4 [cli] Use the configured "Output Directory" from project's settings in vc dev (#4715)
Previously, when a project has a specific "Output Directory" configured
in the project settings page, it was not being correctly considered when
using `vercel dev`.

The main fix here is passing in the full project settings object
(including the `outputDirectory`) to the `detectBuilders()` function.

Also cleaned up a few types and updated the error message that was
previously being rendered to use a short link.
2020-06-26 16:07:07 -07:00
Steven
c09355fdb3 Publish Canary
- vercel@19.1.2-canary.11
 - @vercel/client@8.1.1-canary.1
2020-06-26 13:11:05 -04:00
Steven
d9ac4c45e1 [cli] Fix vc dev serving pure static dot files (#4714)
Fixes ch1233 and discussion #4475
2020-06-26 17:10:11 +00:00
319 changed files with 42817 additions and 2173 deletions

View File

@@ -34,3 +34,6 @@ packages/now-node-bridge/bridge.*
# now-static-build
packages/now-static-build/test/fixtures
# redwood
packages/redwood/test/fixtures

View File

@@ -1,4 +1,4 @@
import { NowRequest, NowResponse } from '@now/node';
import { NowRequest, NowResponse } from '@vercel/node';
import { errorHandler } from './error-handler';
type Handler = (req: NowRequest, res: NowResponse) => Promise<any>;

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
// @ts-ignore
import tar from 'tar-fs';
import { extract } from '../../_lib/examples/extract';
import { NowRequest, NowResponse } from '@now/node';
import { NowRequest, NowResponse } from '@vercel/node';
import { withApiHandler } from '../../_lib/util/with-api-handler';
const TMP_DIR = '/tmp';
@@ -15,8 +15,8 @@ function notFound(res: NowResponse, message: string) {
return res.status(404).send({
error: {
code: 'not_found',
message
}
message,
},
});
}
@@ -35,7 +35,10 @@ function streamToBuffer(stream: any) {
});
}
export default withApiHandler(async function(req: NowRequest, res: NowResponse) {
export default withApiHandler(async function (
req: NowRequest,
res: NowResponse
) {
const ext = '.tar.gz';
const { segment = '' } = req.query;

View File

@@ -3,12 +3,12 @@
// @ts-ignore
import parseGitUrl from 'parse-github-url';
import { NowRequest, NowResponse } from '@now/node';
import { NowRequest, NowResponse } from '@vercel/node';
import { withApiHandler } from '../_lib/util/with-api-handler';
import { getGitHubRepoInfo } from '../_lib/examples/github-repo-info';
import { getGitLabRepoInfo } from '../_lib/examples/gitlab-repo-info';
export default withApiHandler(async function(
export default withApiHandler(async function (
req: NowRequest,
res: NowResponse
) {

View File

@@ -1,8 +1,8 @@
import { NowRequest, NowResponse } from '@now/node';
import { NowRequest, NowResponse } from '@vercel/node';
import { getExampleList } from '../_lib/examples/example-list';
import { withApiHandler } from '../_lib/util/with-api-handler';
export default withApiHandler(async function(
export default withApiHandler(async function (
req: NowRequest,
res: NowResponse
) {

View File

@@ -1,10 +1,10 @@
import { extract } from '../_lib/examples/extract';
import { summary } from '../_lib/examples/summary';
import { NowRequest, NowResponse } from '@now/node';
import { NowRequest, NowResponse } from '@vercel/node';
import { mapOldToNew } from '../_lib/examples/map-old-to-new';
import { withApiHandler } from '../_lib/util/with-api-handler';
export default withApiHandler(async function(
export default withApiHandler(async function (
req: NowRequest,
res: NowResponse
) {

View File

@@ -1,21 +1,27 @@
import { NowRequest, NowResponse } from '@now/node';
import { NowRequest, NowResponse } from '@vercel/node';
import { withApiHandler } from './_lib/util/with-api-handler';
import frameworkList, { Framework } from '../packages/frameworks';
import _frameworks, { Framework } from '../packages/frameworks';
const frameworks = (frameworkList as Framework[]).map(frameworkItem => {
const framework = {
...frameworkItem,
detectors: undefined,
};
const frameworks = (_frameworks as Framework[])
.sort(
(a, b) =>
(a.sort || Number.MAX_SAFE_INTEGER) - (b.sort || Number.MAX_SAFE_INTEGER)
)
.map(frameworkItem => {
const framework = {
...frameworkItem,
detectors: undefined,
sort: undefined,
};
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
return framework;
});
return framework;
});
export default withApiHandler(async function(
export default withApiHandler(async function (
req: NowRequest,
res: NowResponse
) {

View File

@@ -13,12 +13,12 @@
"node-fetch": "2.6.0",
"parse-github-url": "1.0.2",
"tar-fs": "2.0.0",
"typescript": "3.7.4",
"unzip-stream": "0.3.0"
},
"devDependencies": {
"@now/node": "1.3.3",
"@types/node": "13.1.4",
"@types/node-fetch": "2.5.4"
"@types/node-fetch": "2.5.4",
"@vercel/node": "1.7.2",
"typescript": "3.9.6"
}
}

View File

@@ -2,13 +2,6 @@
# yarn lockfile v1
"@now/node@1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@now/node/-/node-1.3.3.tgz#5407cb6a730d4dd9b6b6b0bc4a316f29086c9feb"
integrity sha512-s1qajtQttWhhSs1k6FX0/6eTFYFUplzultrQeKfOPMoYzzz6OxDq5qrQ3elpsGlZlDVmO+x+JOJ7yad+3yBgpg==
dependencies:
"@types/node" "*"
"@sentry/apm@5.11.1":
version "5.11.1"
resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.11.1.tgz#cc89fa4150056fbf009f92eca94fccc3980db34e"
@@ -141,11 +134,25 @@
dependencies:
"@types/node" "*"
"@vercel/node@1.7.2":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@vercel/node/-/node-1.7.2.tgz#85cb8aac661c02dfef6fe752740f5b162e90767b"
integrity sha512-XV5lrLC+K/cxsaFj8H2OoGu1zliOqnxcrOnPInI8HmQjR/Tztt+0nzgpt+7sx8wXcrib0Nu7lK303jP7VjSETw==
dependencies:
"@types/node" "*"
ts-node "8.9.1"
typescript "3.9.3"
agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
binary@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
@@ -161,6 +168,11 @@ bl@^3.0.0:
dependencies:
readable-stream "^3.0.1"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
buffers@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
@@ -229,6 +241,11 @@ defer-to-connect@^1.1.1:
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.1.tgz#88ae694b93f67b81815a2c8c769aef6574ac8f2f"
integrity sha512-J7thop4u3mRTkYRQ+Vpfwy2G5Ehoy82I14+14W4YMDLKdWloI9gSzRbV30s/NckQGVJtPkWNcW4oMAUigTdqiQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@@ -313,6 +330,11 @@ lru_map@^0.3.3:
resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
mimic-response@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
@@ -396,6 +418,19 @@ safe-buffer@~5.2.0:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
source-map-support@^0.5.17:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -434,6 +469,17 @@ to-readable-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=
ts-node@8.9.1:
version "8.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.1.tgz#2f857f46c47e91dcd28a14e052482eb14cfd65a5"
integrity sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
@@ -444,10 +490,15 @@ type-fest@^0.8.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
typescript@3.7.4:
version "3.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19"
integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==
typescript@3.9.3:
version "3.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a"
integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==
typescript@3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
unzip-stream@0.3.0:
version "0.3.0"
@@ -466,3 +517,8 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

View File

@@ -2,16 +2,14 @@
#### Why This Error Occurred
The domain you supplied cannot be verified using either the intended set of nameservers or the given verification TXT record.
The domain you supplied cannot be verified using the intended nameservers.
#### Possible Ways to Fix It
#### Possible Way to Fix It
Apply the intended set of nameservers to your domain or add the given TXT verification record through your domain provider.
Apply the intended set of nameservers to your domain.
You can retrieve both the intended nameservers and TXT verification record for the domain you wish to verify by running `vercel domains inspect <domain>`.
When you have added either verification method to your domain, you can run `vercel domains verify <domain>` again to complete verification for your domain.
Vercel will also automatically check periodically that your domain has been verified and automatically mark it as such if we detect either verification method on the domain.
If you would not like to verify your domain, you can remove it from your account using `vercel domains rm <domain>`.

View File

@@ -0,0 +1,4 @@
module.exports = {
presets: ["next/babel"],
plugins: [],
}

View File

@@ -0,0 +1,10 @@
module.exports = {
extends: ["react-app", "plugin:jsx-a11y/recommended"],
plugins: ["jsx-a11y"],
rules: {
"import/no-anonymous-default-export": "error",
"import/no-webpack-loader-syntax": "off",
"react/react-in-jsx-scope": "off", // React is always in scope with Blitz
"jsx-a11y/anchor-is-valid": "off", //Doesn't play well with Blitz/Next <Link> usage
},
}

56
examples/blitzjs/.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# dependencies
node_modules
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
.npm
web_modules/
# blitz
/.blitz/
/.next/
*.sqlite
.now
.vercel
.blitz-console-history
blitz-log.log
# misc
.DS_Store
# local env files
.env
.envrc
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Testing
coverage
*.lcov
.nyc_output
lib-cov
# Caches
*.tsbuildinfo
.eslintcache
.node_repl_history
.yarn-integrity
# Serverless directories
.serverless/
# Stores VSCode versions used for testing VSCode extensions
.vscode-test

1
examples/blitzjs/.npmrc Normal file
View File

@@ -0,0 +1 @@
save-exact=true

View File

@@ -0,0 +1,6 @@
.gitkeep
.env
*.ico
*.lock
db/migrations

View File

@@ -0,0 +1,21 @@
![Blitz Logo](https://github.com/vercel/vercel/blob/master/packages/frameworks/logos/blitz.svg)
This is a [Blitz.js](https://blitzjs.com/) project bootstrapped with `npx blitz new`.
## Getting Started
First, run the development server:
```bash
npx blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Learn More
To learn more about Blitz.js, view [Blitzjs.com](https://blitzjs.com)
## Deploy on Vercel
View the [documentation on deploying to Vercel](https://blitzjs.com/docs/deploy-vercel)

View File

View File

@@ -0,0 +1,21 @@
import React from "react"
export default class ErrorBoundary extends React.Component<{
fallback: (error: any) => React.ReactNode
}> {
state = { hasError: false, error: null }
static getDerivedStateFromError(error: any) {
return {
hasError: true,
error,
}
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error)
}
return this.props.children
}
}

View File

View File

@@ -0,0 +1,5 @@
import { AppProps } from "blitz"
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

View File

@@ -0,0 +1,23 @@
import { Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/ } from "blitz"
class MyDocument extends Document {
// Only uncomment if you need to customize this behaviour
// static async getInitialProps(ctx: DocumentContext) {
// const initialProps = await Document.getInitialProps(ctx)
// return {...initialProps}
// }
render() {
return (
<Html lang="en">
<DocumentHead />
<body>
<Main />
<BlitzScript />
</body>
</Html>
)
}
}
export default MyDocument

View File

@@ -0,0 +1,197 @@
import { Head, Link } from "blitz"
const Home = () => (
<div className="container">
<Head>
<title>blitzjs</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<div className="logo">
<img src="/logo.png" alt="blitz.js" />
</div>
<p>1. Run this command in your terminal:</p>
<pre>
<code>blitz generate all project name:string</code>
</pre>
<p>2. Then run this command:</p>
<pre>
<code>blitz db migrate</code>
</pre>
<p>
3. Go to{" "}
<Link href="/projects">
<a>/projects</a>
</Link>
</p>
<div className="buttons">
<a
className="button"
href="https://github.com/blitz-js/blitz/blob/master/USER_GUIDE.md?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<a
className="button-outline"
href="https://github.com/blitz-js/blitz"
target="_blank"
rel="noopener noreferrer"
>
Github Repo
</a>
<a
className="button-outline"
href="https://slack.blitzjs.com"
target="_blank"
rel="noopener noreferrer"
>
Slack Community
</a>
</div>
</main>
<footer>
<a
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
>
Powered by Blitz.js
</a>
</footer>
<style jsx>{`
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
footer {
width: 100%;
height: 60px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
background-color: #45009d;
}
footer a {
display: flex;
justify-content: center;
align-items: center;
}
footer a {
color: #f4f4f4;
text-decoration: none;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
width: 300px;
}
.buttons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 0.5rem;
margin-top: 6rem;
}
a.button {
background-color: #6700eb;
padding: 1rem 2rem;
color: #f4f4f4;
text-align: center;
}
a.button:hover {
background-color: #45009d;
}
a.button-outline {
border: 2px solid #6700eb;
padding: 1rem 2rem;
color: #6700eb;
text-align: center;
}
a.button-outline:hover {
border-color: #45009d;
color: #45009d;
}
pre {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
}
code {
font-size: 0.9rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
`}</style>
<style jsx global>{`
@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;700&display=swap");
html,
body {
padding: 0;
margin: 0;
font-family: "Libre Franklin", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
`}</style>
</div>
)
export default Home

View File

@@ -0,0 +1,15 @@
module.exports = {
/*
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
// Important: return the modified config
return config
},
webpackDevMiddleware: (config) => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config
},
*/
}

View File

@@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client"
export * from "@prisma/client"
let prisma: PrismaClient
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient()
} else {
// Ensure the prisma instance is re-used during hot-reloading
// Otherwise, a new client will be created on every reload
global["prisma"] = global["prisma"] || new PrismaClient()
prisma = global["prisma"]
}
export default prisma

View File

View File

@@ -0,0 +1,27 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource sqlite {
provider = "sqlite"
url = "file:./db.sqlite"
}
// SQLite is easy to start with, but if you use Postgres in production
// you should also use it in development with the following:
//datasource postgresql {
// provider = "postgresql"
// url = env("DATABASE_URL")
//}
generator client {
provider = "prisma-client-js"
}
// --------------------------------------
//model Project {
// id Int @default(autoincrement()) @id
// name String
//}

View File

View File

@@ -0,0 +1,55 @@
{
"name": "blitzjs",
"version": "1.0.0",
"scripts": {
"start": "blitz start",
"studio": "blitz db studio",
"build": "blitz build",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"test": "echo \"No tests yet\""
},
"browserslist": [
"defaults"
],
"prettier": {
"semi": false,
"printWidth": 100
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && pretty-quick --staged",
"pre-push": "blitz test"
}
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix"
]
},
"dependencies": {
"@prisma/cli": "latest",
"@prisma/client": "latest",
"blitz": "latest",
"react": "experimental",
"react-dom": "experimental"
},
"devDependencies": {
"@types/react": "16.9.36",
"@typescript-eslint/eslint-plugin": "2.x",
"@typescript-eslint/parser": "2.x",
"babel-eslint": "10.1.0",
"eslint": "6.8.0",
"eslint-config-react-app": "5.2.1",
"eslint-plugin-flowtype": "4.7.0",
"eslint-plugin-import": "2.21.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.20.0",
"eslint-plugin-react-hooks": "3.0.0",
"husky": "4.2.5",
"lint-staged": "10.2.10",
"prettier": "2.0.5",
"pretty-quick": "2.0.1",
"typescript": "3.9.5"
},
"private": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": "./",
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View File

11913
examples/gatsby/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@
"lint-staged": "9.2.5",
"node-fetch": "2.6.0",
"npm-package-arg": "6.1.0",
"prettier": "1.18.2"
"prettier": "2.0.5"
},
"scripts": {
"lerna": "lerna",
@@ -64,7 +64,8 @@
},
"prettier": {
"trailingComma": "es5",
"singleQuote": true
"singleQuote": true,
"arrowParens": "avoid"
},
"eslintConfig": {
"root": true,

View File

@@ -1,12 +1,41 @@
[
{
"name": "Blitz.js",
"slug": "blitzjs",
"demo": "https://blitzjs.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/blitz.svg",
"tagline": "Blitz.js: The Fullstack React Framework",
"description": "A brand new Blitz.js app - the result of running `npx blitz new`.",
"website": "https://blitzjs.com",
"detectors": {
"every": [
{
"path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"blitz\":\\s*\".+?\"[^}]*}"
}
]
},
"settings": {
"buildCommand": {
"placeholder": "`npm run build` or `blitz build`"
},
"devCommand": {
"value": "blitz start"
},
"outputDirectory": {
"placeholder": "Next.js default"
}
}
},
{
"name": "Next.js",
"slug": "nextjs",
"demo": "https://nextjs.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/next.svg",
"tagline": "Next.js makes you productive with React instantly — whether you want to build static or dynamic sites. ",
"tagline": "Next.js makes you productive with React instantly — whether you want to build static or dynamic sites.",
"description": "A Next.js app and a Serverless Function API.",
"website": "https://nextjs.org",
"sort": 1,
"detectors": {
"every": [
{
@@ -35,6 +64,7 @@
"tagline": "Gatsby helps developers build blazing fast websites and apps with React.",
"description": "A Gatsby app, using the default starter theme and a Serverless Function API.",
"website": "https://gatsbyjs.org",
"sort": 2,
"detectors": {
"every": [
{
@@ -62,7 +92,8 @@
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/hexo.svg",
"tagline": "Hexo is a fast, simple & powerful blog framework powered by Node.js.",
"description": "A Hexo site, created with the Hexo CLI.",
"website": "https://hexo.io/",
"website": "https://hexo.io",
"sort": 3,
"detectors": {
"every": [
{
@@ -90,7 +121,8 @@
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/eleventy.svg",
"tagline": "11ty is a simpler static site generator written in JavaScript, created to be an alternative to Jekyll.",
"description": "An Eleventy site, created with npm init.",
"website": "https://www.11ty.dev/",
"website": "https://www.11ty.dev",
"sort": 4,
"detectors": {
"every": [
{
@@ -634,13 +666,13 @@
"every": [
{
"path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"nuxt\":\\s*\".+?\"[^}]*}"
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"nuxt(-edge)?\":\\s*\".+?\"[^}]*}"
}
]
},
"settings": {
"buildCommand": {
"placeholder": "`npm run build` or `nuxt build`"
"placeholder": "`npm run build` or `nuxt generate`"
},
"devCommand": {
"value": "nuxt"
@@ -658,6 +690,7 @@
"tagline": "Hugo is the worlds fastest framework for building websites, written in Go.",
"description": "A Hugo site, created with the Hugo CLI.",
"website": "https://gohugo.io",
"sort": 5,
"detectors": {
"some": [
{

View File

@@ -19,6 +19,7 @@ export interface Framework {
tagline?: string;
website?: string;
description: string;
sort?: number;
detectors?: {
every?: FrameworkDetectionItem[];
some?: FrameworkDetectionItem[];

View File

@@ -0,0 +1,30 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M95.4242 249.857H173.991C203.89 249.857 232.049 263.909 250.026 287.799L327.526 390.789C328.991 392.736 329.212 395.349 328.095 397.513L283.421 484.069C281.278 488.221 275.532 488.71 272.719 484.978L95.4242 249.857Z" fill="url(#paint0_linear)"/>
<g filter="url(#filter0_d)">
<path d="M404.558 249.991H325.991C296.093 249.991 267.933 235.939 249.956 212.049L172.456 109.059C170.991 107.112 170.771 104.499 171.888 102.335L216.561 15.7794C218.705 11.6267 224.45 11.1382 227.264 14.8695L404.558 249.991Z" fill="url(#paint1_linear)"/>
</g>
</g>
<defs>
<filter id="filter0_d" x="71.1812" y="-39.6553" width="433.377" height="437.646" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="48"/>
<feGaussianBlur stdDeviation="50"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.270588 0 0 0 0 0 0 0 0 0 0.615686 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<linearGradient id="paint0_linear" x1="163.936" y1="392.775" x2="316.429" y2="155.244" gradientUnits="userSpaceOnUse">
<stop stop-color="#6700EB"/>
<stop offset="1" stop-color="#45009D"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="336.047" y1="107.073" x2="183.554" y2="344.604" gradientUnits="userSpaceOnUse">
<stop stop-color="#6700EB"/>
<stop offset="1" stop-color="#45009D"/>
</linearGradient>
<clipPath id="clip0">
<rect width="500" height="500" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.0.15",
"version": "0.0.17",
"main": "frameworks.json",
"license": "UNLICENSED",
"scripts": {

View File

@@ -1,11 +1,9 @@
import Ajv from 'ajv';
import path from 'path';
import { join } from 'path';
import { existsSync } from 'fs';
import { isString } from 'util';
import { Framework } from '../';
function isString(arg: any): arg is string {
return typeof arg === 'string';
}
const frameworkList = require('../frameworks.json') as Framework[];
const SchemaFrameworkDetectionItem = {
type: 'array',
@@ -60,6 +58,7 @@ const Schema = {
properties: {
name: { type: 'string' },
slug: { type: ['string', 'null'] },
sort: { type: 'number' },
logo: { type: 'string' },
demo: { type: 'string' },
tagline: { type: 'string' },
@@ -89,12 +88,10 @@ const Schema = {
describe('frameworks', () => {
it('ensure there is an example for every framework', async () => {
const root = path.join(__dirname, '..', '..', '..');
const getExample = (name: string) => path.join(root, 'examples', name);
const root = join(__dirname, '..', '..', '..');
const getExample = (name: string) => join(root, 'examples', name);
const frameworks = require('../frameworks.json') as Framework[];
const result = frameworks
const result = frameworkList
.map(f => f.slug)
.filter(isString)
.filter(f => existsSync(getExample(f)) === false);
@@ -103,10 +100,8 @@ describe('frameworks', () => {
});
it('ensure schema', async () => {
const frameworks = require('../frameworks.json') as Framework[];
const ajv = new Ajv();
const result = ajv.validate(Schema, frameworks);
const result = ajv.validate(Schema, frameworkList);
if (ajv.errors) {
console.error(ajv.errors);
@@ -116,17 +111,26 @@ describe('frameworks', () => {
});
it('ensure logo', async () => {
const frameworks = require('../frameworks.json') as Framework[];
const missing = frameworks
const missing = frameworkList
.map(f => f.logo)
.filter(url => {
const prefix =
'https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/';
const name = url.replace(prefix, '');
return existsSync(path.join(__dirname, '..', 'logos', name)) === false;
return existsSync(join(__dirname, '..', 'logos', name)) === false;
});
expect(missing).toEqual([]);
});
it('ensure unique sort number', async () => {
const sortNumToSlug = new Map<number, string | null>();
frameworkList.forEach(f => {
if (f.sort) {
const duplicateSlug = sortNumToSlug.get(f.sort);
expect(duplicateSlug).toStrictEqual(undefined);
sortNumToSlug.set(f.sort, f.slug);
}
});
});
});

View File

@@ -1,5 +1,6 @@
#!/bin/sh
set -eu
#!/bin/bash
set -euo pipefail
out="dist"
rm -rf "$out"

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.4.1-canary.1",
"version": "2.4.3-canary.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -8,6 +8,8 @@ import { isOfficialRuntime } from './';
interface ErrorResponse {
code: string;
message: string;
action?: string;
link?: string;
}
interface Options {
@@ -462,7 +464,7 @@ function detectFrontBuilder(
});
}
if (framework === 'nextjs') {
if (framework === 'nextjs' || framework === 'blitzjs') {
return { src: 'package.json', use: `@vercel/next${withTag}`, config };
}
@@ -496,7 +498,7 @@ function getMissingBuildScriptError() {
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `scripts` property.' +
'\nMore details: https://vercel.com/docs/v2/platform/frequently-asked-questions#missing-build-script',
'\nLearn More: https://vercel.com/docs/v2/platform/frequently-asked-questions#missing-build-script',
};
}
@@ -608,20 +610,22 @@ function checkUnusedFunctions(
} else {
return {
code: 'unused_function',
message: `The function for ${fnKey} can't be handled by any builder`,
message: `The pattern "${fnKey}" defined in \`functions\` doesn't match any Serverless Functions.`,
action: 'Learn More',
link: 'https://vercel.link/unmatched-function-pattern',
};
}
}
}
if (unusedFunctions.size) {
const [unusedFunction] = Array.from(unusedFunctions);
const [fnKey] = Array.from(unusedFunctions);
return {
code: 'unused_function',
message:
`The function for ${unusedFunction} can't be handled by any builder. ` +
`Make sure it is inside the api/ directory.`,
message: `The pattern "${fnKey}" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
action: 'Learn More',
link: 'https://vercel.link/unmatched-function-pattern',
};
}

View File

@@ -34,7 +34,7 @@ interface Props {
*/
link?: string;
/**
* Optional "action" to display before the `link`, such as "More details".
* Optional "action" to display before the `link`, such as "Learn More".
*/
action?: string;
}

View File

@@ -15,7 +15,9 @@ async function readFileOrNull(file: string) {
return null;
}
export async function readConfigFile<T>(files: string | string[]) {
export async function readConfigFile<T>(
files: string | string[]
): Promise<T | null> {
files = Array.isArray(files) ? files : [files];
for (const name of files) {
@@ -24,11 +26,11 @@ export async function readConfigFile<T>(files: string | string[]) {
if (data) {
const str = data.toString('utf8');
if (name.endsWith('.json')) {
return JSON.parse(str);
return JSON.parse(str) as T;
} else if (name.endsWith('.toml')) {
return (toml.parse(str) as unknown) as T;
} else if (name.endsWith('.yaml') || name.endsWith('.yml')) {
return yaml.safeLoad(str, { filename: name });
return yaml.safeLoad(str, { filename: name }) as T;
}
}
}

View File

@@ -349,20 +349,35 @@ export async function runPipInstall(
);
}
export function getScriptName(
pkg: Pick<PackageJson, 'scripts'> | null | undefined,
possibleNames: Iterable<string>
): string | null {
if (pkg && pkg.scripts) {
for (const name of possibleNames) {
if (name in pkg.scripts) {
return name;
}
}
}
return null;
}
export async function runPackageJsonScript(
destPath: string,
scriptName: string,
scriptNames: string | Iterable<string>,
spawnOpts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
const { packageJson, cliType } = await scanParentDirs(destPath, true);
const hasScript = Boolean(
packageJson &&
packageJson.scripts &&
scriptName &&
packageJson.scripts[scriptName]
const scriptName = getScriptName(
packageJson,
typeof scriptNames === 'string' ? [scriptNames] : scriptNames
);
if (!hasScript) return false;
if (!scriptName) return false;
debug('Running user script...');
const runScriptTime = Date.now();
if (cliType === 'npm') {
const prettyCommand = `npm run ${scriptName}`;
@@ -382,6 +397,7 @@ export async function runPackageJsonScript(
});
}
debug(`Script complete [${Date.now() - runScriptTime}ms]`);
return true;
}

View File

@@ -13,6 +13,7 @@ import {
execCommand,
spawnCommand,
walkParentDirs,
getScriptName,
installDependencies,
runPackageJsonScript,
runNpmInstall,
@@ -47,6 +48,7 @@ export {
rename,
execAsync,
spawnAsync,
getScriptName,
installDependencies,
runPackageJsonScript,
execCommand,

View File

@@ -775,8 +775,9 @@ describe('Test `detectBuilders`', () => {
expect(errors).toEqual([
{
code: 'unused_function',
message:
"The function for server/**/*.ts can't be handled by any builder. Make sure it is inside the api/ directory.",
message: `The pattern "server/**/*.ts" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
action: 'Learn More',
link: 'https://vercel.link/unmatched-function-pattern',
},
]);
});
@@ -1878,8 +1879,9 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(errors).toEqual([
{
code: 'unused_function',
message:
"The function for server/**/*.ts can't be handled by any builder. Make sure it is inside the api/ directory.",
message: `The pattern "server/**/*.ts" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
action: 'Learn More',
link: 'https://vercel.link/unmatched-function-pattern',
},
]);
});

View File

@@ -0,0 +1,72 @@
import assert from 'assert';
import { getScriptName } from '../src';
describe('Test `getScriptName()`', () => {
it('should return "vercel-*"', () => {
const pkg = {
scripts: {
'vercel-dev': '',
'vercel-build': '',
dev: '',
build: '',
},
};
assert.equal(
getScriptName(pkg, ['vercel-dev', 'now-dev', 'dev']),
'vercel-dev'
);
assert.equal(
getScriptName(pkg, ['vercel-build', 'now-build', 'build']),
'vercel-build'
);
assert.equal(getScriptName(pkg, ['dev']), 'dev');
assert.equal(getScriptName(pkg, ['build']), 'build');
});
it('should return "now-*"', () => {
const pkg = {
scripts: {
'now-dev': '',
'now-build': '',
dev: '',
build: '',
},
};
assert.equal(
getScriptName(pkg, ['vercel-dev', 'now-dev', 'dev']),
'now-dev'
);
assert.equal(
getScriptName(pkg, ['vercel-build', 'now-build', 'build']),
'now-build'
);
assert.equal(getScriptName(pkg, ['dev']), 'dev');
assert.equal(getScriptName(pkg, ['build']), 'build');
});
it('should return base script name', () => {
const pkg = {
scripts: {
dev: '',
build: '',
},
};
assert.equal(getScriptName(pkg, ['dev']), 'dev');
assert.equal(getScriptName(pkg, ['build']), 'build');
});
it('should return `null`', () => {
assert.equal(getScriptName(undefined, ['build']), null);
assert.equal(getScriptName({}, ['build']), null);
assert.equal(getScriptName({ scripts: {} }, ['build']), null);
const pkg = {
scripts: {
dev: '',
build: '',
},
};
assert.equal(getScriptName(pkg, ['vercel-dev', 'now-dev']), null);
assert.equal(getScriptName(pkg, ['vercel-build', 'now-build']), null);
});
});

View File

@@ -1,9 +1,9 @@
{
"name": "vercel",
"version": "19.1.2-canary.10",
"version": "20.0.0-canary.2",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
"description": "The command-line interface for Vercel",
"homepage": "https://vercel.com",
"repository": {
"type": "git",
@@ -62,13 +62,15 @@
"node": ">= 10"
},
"dependencies": {
"@vercel/build-utils": "2.4.1-canary.1",
"@vercel/go": "1.1.3-canary.1",
"@vercel/next": "2.6.8-canary.4",
"@vercel/node": "1.7.2-canary.0",
"@vercel/build-utils": "2.4.3-canary.0",
"@vercel/go": "1.1.5-canary.0",
"@vercel/next": "2.6.14-canary.1",
"@vercel/node": "1.7.4-canary.0",
"@vercel/python": "1.2.2",
"@vercel/ruby": "1.2.3-canary.0",
"@vercel/static-build": "0.17.3"
"@vercel/redwood": "0.0.2-canary.0",
"@vercel/ruby": "1.2.3",
"@vercel/static-build": "0.17.7-canary.1",
"update-notifier": "4.1.0"
},
"devDependencies": {
"@sentry/node": "5.5.0",
@@ -119,7 +121,7 @@
"chalk": "2.4.2",
"chokidar": "3.3.1",
"clipboardy": "2.1.0",
"codecov": "3.6.5",
"codecov": "3.7.1",
"cpy": "7.2.0",
"credit-card": "3.0.1",
"date-fns": "1.29.0",
@@ -185,7 +187,6 @@
"ts-node": "8.3.0",
"typescript": "3.9.3",
"universal-analytics": "0.4.20",
"update-check": "1.5.3",
"utility-types": "2.1.0",
"which": "2.0.2",
"which-promise": "1.0.0",

View File

@@ -49,7 +49,13 @@ async function main() {
// Do the initial `ncc` build
console.log();
const src = join(dirRoot, 'src');
const args = ['@zeit/ncc', 'build', '--source-map'];
const args = [
'@zeit/ncc',
'build',
'--source-map',
'--external',
'update-notifier',
];
if (!isDev) {
args.push('--minify');
}
@@ -86,7 +92,7 @@ async function main() {
// A bunch of source `.ts` files from CLI's `util` directory
await remove(join(dirRoot, 'dist', 'util'));
console.log('Finished building `now-cli`');
console.log('Finished building Vercel CLI');
}
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {

View File

@@ -354,7 +354,7 @@ export default async function main(
path,
sourcePath,
project
? `To change your project settings, go to https://vercel.com/${org.slug}/${project.name}/settings`
? `To change your Project Settings, go to https://vercel.com/${org.slug}/${project.name}/settings`
: ''
)) === false
) {

View File

@@ -8,11 +8,15 @@ import Client from '../../util/client';
import { getLinkedProject } from '../../util/projects/link';
import { getFrameworks } from '../../util/get-frameworks';
import { isSettingValue } from '../../util/is-setting-value';
import { getCommandName } from '../../util/pkg-name';
import { ProjectSettings, ProjectEnvTarget } from '../../types';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
import { Env } from '@vercel/build-utils';
import setupAndLink from '../../util/link/setup-and-link';
type Options = {
'--debug'?: boolean;
'--listen'?: string;
'--confirm': boolean;
};
export default async function dev(
@@ -34,37 +38,56 @@ export default async function dev(
});
// retrieve dev command
const [link, frameworks] = await Promise.all([
let [link, frameworks] = await Promise.all([
getLinkedProject(output, client, cwd),
getFrameworks(client),
]);
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
const autoConfirm = opts['--confirm'];
const forceDelete = false;
link = await setupAndLink(
ctx,
output,
cwd,
forceDelete,
autoConfirm,
'link',
'Set up and develop'
);
if (link.status === 'not_linked') {
// User aborted project linking questions
return 0;
}
}
if (link.status === 'error') {
return link.exitCode;
}
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
output.error(
`Your codebase isnt linked to a project on Vercel. Run ${getCommandName()} to link it.`
);
return 1;
}
let devCommand: undefined | string;
let frameworkSlug: null | string = null;
let devCommand: string | undefined;
let frameworkSlug: string | undefined;
let projectSettings: ProjectSettings | undefined;
let environmentVars: Env | undefined;
if (link.status === 'linked') {
const { project, org } = link;
client.currentTeam = org.type === 'team' ? org.id : undefined;
projectSettings = project;
if (project.devCommand) {
devCommand = project.devCommand;
} else if (project.framework) {
const framework = frameworks.find(f => f.slug === project.framework);
if (framework) {
frameworkSlug = framework.slug;
const defaults = framework.settings.devCommand;
if (framework.slug) {
frameworkSlug = framework.slug;
}
const defaults = framework.settings.devCommand;
if (isSettingValue(defaults)) {
devCommand = defaults.value;
}
@@ -74,6 +97,13 @@ export default async function dev(
if (project.rootDirectory) {
cwd = join(cwd, project.rootDirectory);
}
environmentVars = await getDecryptedEnvRecords(
output,
client,
project,
ProjectEnvTarget.Development
);
}
const devServer = new DevServer(cwd, {
@@ -81,6 +111,8 @@ export default async function dev(
debug,
devCommand,
frameworkSlug,
projectSettings,
environmentVars,
});
process.once('SIGINT', () => devServer.stop());

View File

@@ -32,6 +32,7 @@ const help = () => {
-d, --debug Debug mode [off]
-l, --listen [uri] Specify a URI endpoint on which to listen [0.0.0.0:3000]
-t, --token [token] Specify an Authorization Token
--confirm Skip questions and use defaults when setting up a new project
${chalk.dim('Examples:')}
@@ -56,6 +57,7 @@ export default async function main(ctx: NowContext) {
argv = getArgs(ctx.argv.slice(2), {
'--listen': String,
'-l': '--listen',
'--confirm': Boolean,
// Deprecated
'--port': Number,
@@ -97,7 +99,7 @@ export default async function main(ctx: NowContext) {
'package.json'
)} must not contain ${cmd('now dev')}`
);
output.error(`More details: http://err.sh/now/now-dev-as-dev-script`);
output.error(`Learn More: http://err.sh/now/now-dev-as-dev-script`);
return 1;
}
if (scripts && scripts.dev && /\bvercel\b\W+\bdev\b/.test(scripts.dev)) {
@@ -106,7 +108,7 @@ export default async function main(ctx: NowContext) {
'package.json'
)} must not contain ${cmd('vercel dev')}`
);
output.error(`More details: http://err.sh/now/now-dev-as-dev-script`);
output.error(`Learn More: http://err.sh/now/now-dev-as-dev-script`);
return 1;
}
}

View File

@@ -1,23 +1,24 @@
import chalk from 'chalk';
import psl from 'psl';
import { NowContext } from '../../types';
import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import addDomain from '../../util/domains/add-domain';
import Client from '../../util/client';
import cmd from '../../util/output/cmd';
import formatDnsTable from '../../util/format-dns-table';
import formatNSTable from '../../util/format-ns-table';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import param from '../../util/output/param';
import { getCommandName, getTitleName } from '../../util/pkg-name';
import { getCommandName } from '../../util/pkg-name';
import { getDomain } from '../../util/domains/get-domain';
import { getLinkedProject } from '../../util/projects/link';
import { isPublicSuffix } from '../../util/domains/is-public-suffix';
import { getDomainConfig } from '../../util/domains/get-domain-config';
import { addDomainToProject } from '../../util/projects/add-domain-to-project';
import { removeDomainFromProject } from '../../util/projects/remove-domain-from-project';
import code from '../../util/output/code';
type Options = {
'--cdn': boolean;
'--debug': boolean;
'--no-cdn': boolean;
'--force': boolean;
};
export default async function add(
@@ -33,6 +34,7 @@ export default async function add(
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const force = opts['--force'];
const client = new Client({ apiUrl, token, currentTeam, debug });
let contextName = null;
@@ -47,105 +49,116 @@ export default async function add(
throw err;
}
if (opts['--cdn'] !== undefined || opts['--no-cdn'] !== undefined) {
output.error(`Toggling CF from ${getTitleName()} CLI is deprecated.`);
return 1;
}
const project = await getLinkedProject(output, client).then(result => {
if (result.status === 'linked') {
return result.project;
}
if (args.length !== 1) {
return null;
});
if (project && args.length !== 1) {
output.error(
`${getCommandName('domains add <domain>')} expects one argument`
`${getCommandName('domains add <domain>')} expects one argument.`
);
return 1;
} else if (!project && args.length !== 2) {
output.error(
`${getCommandName(
'domains add <domain> <project>'
)} expects two arguments.`
);
return 1;
}
const domainName = String(args[0]);
const parsedDomain = psl.parse(domainName);
if (parsedDomain.error) {
output.error(`The provided domain name ${param(domainName)} is invalid`);
return 1;
}
const { domain, subdomain } = parsedDomain;
if (!domain) {
output.error(`The provided domain '${param(domainName)}' is not valid.`);
return 1;
}
if (subdomain) {
output.error(
`You are adding '${domainName}' as a domain name containing a subdomain part '${subdomain}'\n` +
` This feature is deprecated, please add just the root domain: ${chalk.cyan(
`${getCommandName(`domain add ${domain}`)}`
)}`
);
return 1;
}
const projectName = project ? project.name : String(args[1]);
const addStamp = stamp();
const addedDomain = await addDomain(client, domainName, contextName);
if (addedDomain instanceof ERRORS.InvalidDomain) {
output.error(
`The provided domain name "${addedDomain.meta.domain}" is invalid`
);
return 1;
}
let aliasTarget = await addDomainToProject(client, projectName, domainName);
if (addedDomain instanceof ERRORS.DomainAlreadyExists) {
output.error(
`The domain ${chalk.underline(
addedDomain.meta.domain
)} is already registered by a different account.\n` +
` If this seems like a mistake, please contact us at support@vercel.com`
);
return 1;
if (aliasTarget instanceof Error) {
if (
aliasTarget instanceof ERRORS.APIError &&
aliasTarget.code === 'ALIAS_DOMAIN_EXIST' &&
aliasTarget.project &&
aliasTarget.project.id
) {
if (force) {
const removeResponse = await removeDomainFromProject(
client,
aliasTarget.project.id,
domainName
);
if (removeResponse instanceof Error) {
output.prettyError(removeResponse);
return 1;
}
aliasTarget = await addDomainToProject(client, projectName, domainName);
}
}
if (aliasTarget instanceof Error) {
output.prettyError(aliasTarget);
return 1;
}
}
// We can cast the information because we've just added the domain and it should be there
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
addedDomain.name
)} added correctly. ${addStamp()}\n`
domainName
)} added to project ${chalk.bold(projectName)}. ${addStamp()}`
);
if (!addedDomain.verified) {
if (isPublicSuffix(domainName)) {
output.log(
`The domain will automatically get assigned to your latest production deployment.`
);
return 0;
}
const domainResponse = await getDomain(client, contextName, domainName);
if (domainResponse instanceof Error) {
output.prettyError(domainResponse);
return 1;
}
const domainConfig = await getDomainConfig(client, contextName, domainName);
if (domainConfig.misconfigured) {
output.warn(
`The domain was added but it is not verified. To verify it, you should either:`
`This domain is not configured properly. To configure it you should either:`
);
output.print(
` ${chalk.gray(
'a)'
)} Change your domain nameservers to the following intended set: ${chalk.gray(
'[recommended]'
)}\n`
` ${chalk.grey('a)')} ` +
`Set the following record on your DNS provider to continue: ` +
`${code(`A ${domainName} 76.76.21.21`)} ` +
`${chalk.grey('[recommended]')}\n`
);
output.print(
` ${chalk.grey('b)')} ` +
`Change your domain nameservers to the intended set`
);
output.print(
`\n${formatNSTable(
addedDomain.intendedNameservers,
addedDomain.nameservers,
domainResponse.intendedNameservers,
domainResponse.nameservers,
{ extraSpace: ' ' }
)}\n\n`
);
output.print(
` ${chalk.gray(
'b)'
)} Add a DNS TXT record with the name and value shown below.\n`
);
output.print(
`\n${formatDnsTable([['_now', 'TXT', addedDomain.verificationRecord]], {
extraSpace: ' ',
})}\n\n`
);
output.print(
` We will run a verification for you and you will receive an email upon completion.\n`
);
output.print(
` If you want to force running a verification, you can run ${cmd(
`${getCommandName('domains verify <domain>')}`
)}\n`
output.print(' Read more: https://vercel.link/domain-configuration\n\n');
} else {
output.log(
`The domain will automatically get assigned to your latest production deployment.`
);
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
}
return 0;

View File

@@ -13,7 +13,6 @@ import transferIn from './transfer-in';
import inspect from './inspect';
import ls from './ls';
import rm from './rm';
import verify from './verify';
import move from './move';
import { getPkgName } from '../../util/pkg-name';
@@ -25,17 +24,17 @@ const help = () => {
ls Show all domains in a list
inspect [name] Displays information related to a domain
add [name] Add a new domain that you already own
add [name] [project] Add a new domain that you already own
rm [name] Remove a domain
buy [name] Buy a domain that you don't yet own
move [name] [destination] Move a domain to another user or team.
transfer-in [name] Transfer in a domain to Vercel
verify [name] Run a verification for a domain
${chalk.dim('Options:')}
-h, --help Output usage information
-d, --debug Debug mode [off]
-f, --force Force a domain on a project and remove it from an existing one
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
@@ -82,7 +81,6 @@ const COMMAND_CONFIG = {
move: ['move'],
rm: ['rm', 'remove'],
transferIn: ['transfer-in'],
verify: ['verify'],
};
export default async function main(ctx: NowContext) {
@@ -90,10 +88,9 @@ export default async function main(ctx: NowContext) {
try {
argv = getArgs(ctx.argv.slice(2), {
'--cdn': Boolean,
'--code': String,
'--no-cdn': Boolean,
'--yes': Boolean,
'--force': Boolean,
'--next': Number,
'-N': '--next',
});
@@ -122,8 +119,6 @@ export default async function main(ctx: NowContext) {
return rm(ctx, argv, args, output);
case 'transferIn':
return transferIn(ctx, argv, args, output);
case 'verify':
return verify(ctx, argv, args, output);
default:
return ls(ctx, argv, args, output);
}

View File

@@ -4,13 +4,16 @@ import { NowContext } from '../../types';
import { Output } from '../../util/output';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import dnsTable from '../../util/format-dns-table';
import formatDate from '../../util/format-date';
import formatNSTable from '../../util/format-ns-table';
import getDomainByName from '../../util/domains/get-domain-by-name';
import getScope from '../../util/get-scope';
import formatTable from '../../util/format-table';
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
import getDomainPrice from '../../util/domains/get-domain-price';
import { getCommandName } from '../../util/pkg-name';
import { getDomainConfig } from '../../util/domains/get-domain-config';
import code from '../../util/output/code';
type Options = {
'--debug': boolean;
@@ -70,7 +73,7 @@ export default async function inspect(
.then(res => (res instanceof Error ? null : res.price))
.catch(() => null),
]);
if (domain instanceof DomainNotFound) {
if (!domain || domain instanceof DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
@@ -88,6 +91,15 @@ export default async function inspect(
return 1;
}
const projects = await findProjectsForDomain(client, domainName);
if (projects instanceof Error) {
output.prettyError(projects);
return 1;
}
const domainConfig = await getDomainConfig(client, contextName, domainName);
output.log(
`Domain ${domainName} found under ${chalk.bold(contextName)} ${chalk.gray(
inspectStamp()
@@ -129,6 +141,7 @@ export default async function inspect(
domain.txtVerifiedAt
)}\n`
);
if (renewalPrice && domain.boughtAt) {
output.print(
` ${chalk.cyan('Renewal Price')}\t\t$${renewalPrice} USD\n`
@@ -145,37 +158,57 @@ export default async function inspect(
);
output.print('\n');
output.print(chalk.bold(' Verification Record\n\n'));
output.print(
`${dnsTable([['_now', 'TXT', domain.verificationRecord]], {
extraSpace: ' ',
})}\n`
);
output.print('\n');
if (!domain.verified) {
output.warn(`This domain is not verified. To verify it you should either:`);
output.print(
` ${chalk.gray(
'a)'
)} Change your domain nameservers to the intended set detailed above. ${chalk.gray(
'[recommended]'
)}\n`
if (domainConfig.misconfigured) {
output.warn(
`This domain is not configured properly. To configure it you should either:`
);
output.print(
` ${chalk.gray(
'b)'
)} Add a DNS TXT record with the name and value shown above.\n\n`
` ${chalk.grey('a)')} ` +
`Set the following record on your DNS provider to continue: ` +
`${code(`A ${domainName} 76.76.21.21`)} ` +
`${chalk.grey('[recommended]')}\n`
);
output.print(
` ${chalk.grey('b)')} ` +
`Change your domain nameservers to the intended set detailed above.\n\n`
);
output.print(
` We will run a verification for you and you will receive an email upon completion.\n`
);
output.print(
` If you want to force running a verification, you can run ${getCommandName(
`domains verify <domain>`
)}\n`
output.print(' Read more: https://vercel.link/domain-configuration\n\n');
}
if (Array.isArray(projects) && projects.length > 0) {
output.print(chalk.bold(' Projects\n'));
const table = formatTable(
['Project', 'Domains'],
['l', 'l'],
[
{
rows: projects.map(project => {
const name = project.name;
const domains = (project.alias || [])
.map(target => target.domain)
.filter(alias => alias.endsWith(domainName));
const cols = domains.length ? domains.join(', ') : '-';
return [name, cols];
}),
},
]
);
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
output.print(
table
.split('\n')
.map(line => ` ${line}`)
.join('\n')
);
output.print('\n\n');
}
return null;

View File

@@ -1,22 +1,37 @@
import ms from 'ms';
import psl from 'psl';
import chalk from 'chalk';
import table from 'text-table';
import plural from 'pluralize';
import Client from '../../util/client';
import getDomains from '../../util/domains/get-domains';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import { Output } from '../../util/output';
import { Domain, NowContext } from '../../types';
import formatTable from '../../util/format-table';
import { formatDateWithoutTime } from '../../util/format-date';
import { Domain, Project, NowContext } from '../../types';
import { getProjectsWithDomains } from '../../util/projects/get-projects-with-domains';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import isDomainExternal from '../../util/domains/is-domain-external';
import { isPublicSuffix } from '../../util/domains/is-public-suffix';
type Options = {
'--debug': boolean;
'--next': number;
};
interface DomainInfo {
domain: string;
apexDomain: string;
projectName: string | null;
dns: 'Vercel' | 'External';
configured: boolean;
expiresAt: number | null;
createdAt: number | null;
}
export default async function ls(
ctx: NowContext,
opts: Options,
@@ -60,16 +75,31 @@ export default async function ls(
return 1;
}
const { domains, pagination } = await getDomains(
client,
contextName,
nextTimestamp
);
const [{ domains, pagination }, projects] = await Promise.all([
getDomains(client, contextName),
getProjectsWithDomains(client),
] as const);
if (projects instanceof Error) {
output.prettyError(projects);
return 1;
}
const domainsInfo = createDomainsInfo(domains, projects);
output.log(
`Domains found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}\n`
`${plural(
'project domain',
domainsInfo.length,
true
)} found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}`
);
if (domains.length > 0) {
console.log(`${formatDomainsTable(domains)}\n`);
if (domainsInfo.length > 0) {
output.print(
formatDomainsTable(domainsInfo).replace(/^(.*)/gm, `${' '.repeat(3)}$1`)
);
output.print('\n\n');
}
if (pagination && pagination.count === 20) {
@@ -84,28 +114,92 @@ export default async function ls(
return 0;
}
function formatDomainsTable(domains: Domain[]) {
const current = new Date();
return table(
[
[
'',
chalk.gray('domain'),
chalk.gray('serviceType'),
chalk.gray('verified'),
chalk.gray('cdn'),
chalk.gray('age'),
].map(s => chalk.dim(s)),
...domains.map(domain => {
const url = chalk.bold(domain.name);
const time = chalk.gray(ms(current.getTime() - domain.createdAt));
return ['', url, domain.serviceType, domain.verified, true, time];
}),
],
{
align: ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(4),
stringLength: strlen,
function createDomainsInfo(domains: Domain[], projects: Project[]) {
const info = new Map<string, DomainInfo>();
domains.forEach(domain => {
info.set(domain.name, {
domain: domain.name,
apexDomain: domain.name,
projectName: null,
expiresAt: domain.expiresAt || null,
createdAt: domain.createdAt,
configured: Boolean(domain.verified),
dns: isDomainExternal(domain) ? 'External' : 'Vercel',
});
projects.forEach(project => {
(project.alias || []).forEach(target => {
if (!target.domain.endsWith(domain.name)) return;
info.set(target.domain, {
domain: target.domain,
apexDomain: domain.name,
projectName: project.name,
expiresAt: domain.expiresAt || null,
createdAt: domain.createdAt || target.createdAt || null,
configured: Boolean(domain.verified),
dns: isDomainExternal(domain) ? 'External' : 'Vercel',
});
});
});
});
projects.forEach(project => {
(project.alias || []).forEach(target => {
if (info.has(target.domain)) return;
const { domain: apexDomain } = psl.parse(
target.domain
) as psl.ParsedDomain;
info.set(target.domain, {
domain: target.domain,
apexDomain: apexDomain || target.domain,
projectName: project.name,
expiresAt: null,
createdAt: target.createdAt || null,
configured: isPublicSuffix(target.domain),
dns: isPublicSuffix(target.domain) ? 'Vercel' : 'External',
});
});
});
const list = Array.from(info.values());
return list.sort((a, b) => {
if (a.apexDomain === b.apexDomain) {
if (a.apexDomain === a.domain) return -1;
if (b.apexDomain === b.domain) return 1;
return a.domain.localeCompare(b.domain);
}
);
return a.apexDomain.localeCompare(b.apexDomain);
});
}
function formatDomainsTable(domainsInfo: DomainInfo[]) {
const current = Date.now();
const rows: string[][] = domainsInfo.map(info => {
const expiration = formatDateWithoutTime(info.expiresAt);
const age = info.createdAt ? ms(current - info.createdAt) : '-';
return [
info.domain,
info.projectName || '-',
info.dns,
expiration,
info.configured.toString(),
chalk.gray(age),
];
});
const table = formatTable(
['domain', 'project', 'dns', 'expiration', 'configured', 'age'],
['l', 'l', 'l', 'l', 'l', 'l'],
[{ rows }]
);
return table;
}

View File

@@ -15,6 +15,7 @@ import * as ERRORS from '../../util/errors-ts';
import param from '../../util/output/param';
import promptBool from '../../util/input/prompt-bool';
import setCustomSuffix from '../../util/domains/set-custom-suffix';
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
import { getCommandName } from '../../util/pkg-name';
type Options = {
@@ -67,7 +68,7 @@ export default async function rm(
}
const domain = await getDomainByName(client, contextName, domainName);
if (domain instanceof DomainNotFound) {
if (domain instanceof DomainNotFound || domain.name !== domainName) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
@@ -85,6 +86,18 @@ export default async function rm(
return 1;
}
const projects = await findProjectsForDomain(client, domain.name);
if (Array.isArray(projects) && projects.length > 0) {
output.warn(
`The domain is currently used by ${plural(
'project',
projects.length,
true
)}.`
);
}
const skipConfirmation = opts['--yes'];
if (
!skipConfirmation &&

View File

@@ -1,147 +0,0 @@
import chalk from 'chalk';
import { NowContext } from '../../types';
import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import Client from '../../util/client';
import formatDnsTable from '../../util/format-dns-table';
import formatNSTable from '../../util/format-ns-table';
import getDomainByName from '../../util/domains/get-domain-by-name';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import verifyDomain from '../../util/domains/verify-domain';
import { getCommandName } from '../../util/pkg-name';
type Options = {
'--debug': boolean;
};
export default async function verify(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
) {
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const client = new Client({ apiUrl, token, currentTeam, debug });
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;
if (!domainName) {
output.error(
`${getCommandName(`domains verify <domain>`)} expects one argument`
);
return 1;
}
if (args.length !== 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('domains verify <domain>')}`
)}`
);
return 1;
}
const domain = await getDomainByName(client, contextName, domainName);
if (domain instanceof ERRORS.DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
return 1;
}
if (domain instanceof ERRORS.DomainPermissionDenied) {
output.error(
`You don't have access to the domain ${domainName} under ${chalk.bold(
contextName
)}`
);
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
return 1;
}
const verifyStamp = stamp();
const result = await verifyDomain(client, domain.name, contextName);
if (result instanceof ERRORS.DomainVerificationFailed) {
const { nsVerification, txtVerification } = result.meta;
output.error(
`The domain ${
domain.name
} could not be verified due to the following reasons: ${verifyStamp()}\n`
);
output.print(
` ${chalk.gray(
'a)'
)} Nameservers verification failed since we see a different set than the intended set:`
);
output.print(
`\n${formatNSTable(
nsVerification.intendedNameservers,
nsVerification.nameservers,
{ extraSpace: ' ' }
)}\n\n`
);
output.print(
` ${chalk.gray(
'b)'
)} DNS TXT verification failed since found no matching records.`
);
output.print(
`\n${formatDnsTable(
[['_now', 'TXT', txtVerification.verificationRecord]],
{ extraSpace: ' ' }
)}\n\n`
);
output.print(
` Once your domain uses either the nameservers or the TXT DNS record from above, run again ${getCommandName(
`domains verify <domain>`
)}.\n`
);
output.print(
` We will also periodically run a verification check for you and you will receive an email once your domain is verified.\n`
);
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
return 1;
}
if (result.nsVerifiedAt) {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
domain.name
)} was verified using nameservers. ${verifyStamp()}`
);
return 0;
}
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
domain.name
)} was verified using DNS TXT record. ${verifyStamp()}`
);
output.print(
` You can verify with nameservers too. Run ${getCommandName(
`domains inspect ${domain.name}`
)} to find out the intended set.\n`
);
return 0;
}

View File

@@ -124,7 +124,9 @@ export default async function main(ctx: NowContext) {
return link.exitCode;
} else if (link.status === 'not_linked') {
output.error(
`Your codebase isnt linked to a project on Vercel. Run ${getCommandName()} to link it.`
`Your codebase isnt linked to a project on Vercel. Run ${getCommandName(
'link'
)} to begin.`
);
return 1;
} else {

View File

@@ -4,8 +4,7 @@ import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import getEnvVariables from '../../util/env/get-env-records';
import getDecryptedSecret from '../../util/env/get-decrypted-secret';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
import param from '../../util/output/param';
import withSpinner from '../../util/with-spinner';
import { join } from 'path';
@@ -13,6 +12,7 @@ import { promises, openSync, closeSync, readSync } from 'fs';
import { emoji, prependEmoji } from '../../util/emoji';
import { getCommandName } from '../../util/pkg-name';
const { writeFile } = promises;
import { Env } from '@vercel/build-utils';
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
@@ -84,45 +84,21 @@ export default async function pull(
);
const pullStamp = stamp();
const records = await withSpinner('Downloading', async () => {
const dev = ProjectEnvTarget.Development;
const envs = await getEnvVariables(output, client, project.id, 4, dev);
const decryptedValues = await Promise.all(
envs.map(async env => {
try {
const value = await getDecryptedSecret(output, client, env.value);
return { value, found: true };
} catch (error) {
if (error && error.status === 404) {
return { value: '', found: false };
}
throw error;
}
})
);
const results: { key: string; value: string; found: boolean }[] = [];
for (let i = 0; i < decryptedValues.length; i++) {
const { key } = envs[i];
const { value, found } = decryptedValues[i];
results.push({ key, value, found });
}
return results;
});
const records: Env = await withSpinner(
'Downloading',
async () =>
await getDecryptedEnvRecords(
output,
client,
project,
ProjectEnvTarget.Development
)
);
const contents =
CONTENTS_PREFIX +
records
.filter(obj => {
if (!obj.found) {
output.print('');
output.warn(
`Unable to download variable ${obj.key} because associated secret was deleted`
);
return false;
}
return true;
})
.map(({ key, value }) => `${key}="${escapeValue(value)}"`)
Object.entries(records)
.map(([key, value]) => `${key}="${escapeValue(value)}"`)
.join('\n') +
'\n';
@@ -139,8 +115,10 @@ export default async function pull(
return 0;
}
function escapeValue(value: string) {
function escapeValue(value: string | undefined) {
return value
.replace(new RegExp('\n', 'g'), '\\n') // combine newlines (unix) into one line
.replace(new RegExp('\r', 'g'), '\\r'); // combine newlines (windows) into one line
? value
.replace(new RegExp('\n', 'g'), '\\n') // combine newlines (unix) into one line
.replace(new RegExp('\r', 'g'), '\\r') // combine newlines (windows) into one line
: '';
}

View File

@@ -17,6 +17,7 @@ export default new Map([
['help', 'help'],
['init', 'init'],
['inspect', 'inspect'],
['link', 'link'],
['list', 'list'],
['ln', 'alias'],
['log', 'logs'],

View File

@@ -0,0 +1,98 @@
import chalk from 'chalk';
import { NowContext } from '../../types';
import createOutput from '../../util/output';
import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import setupAndLink from '../../util/link/setup-and-link';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} link`)} [options]
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
--confirm Confirm default options and skip questions
${chalk.dim('Examples:')}
${chalk.gray('')} Link current directory to a Vercel Project
${chalk.cyan(`$ ${getPkgName()} link`)}
${chalk.gray(
''
)} Link current directory with default options and skip questions
${chalk.cyan(`$ ${getPkgName()} link --confirm`)}
${chalk.gray('')} Link a specific directory to a Vercel Project
${chalk.cyan(`$ ${getPkgName()} link /usr/src/project`)}
`);
};
const COMMAND_CONFIG = {
// No subcommands yet
};
export default async function main(ctx: NowContext) {
let argv;
try {
argv = getArgs(ctx.argv.slice(2), {
'--confirm': Boolean,
});
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help();
return 2;
}
const debug = argv['--debug'];
const output = createOutput({ debug });
const { args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG);
const path = args[0] || process.cwd();
const autoConfirm = argv['--confirm'];
const forceDelete = true;
const link = await setupAndLink(
ctx,
output,
path,
forceDelete,
autoConfirm,
'success',
'Set up'
);
if (link.status === 'error') {
return link.exitCode;
} else if (link.status === 'not_linked') {
// User aborted project linking questions
return 0;
} else if (link.status === 'linked') {
// Successfully linked
return 0;
} else {
const err: never = link;
throw new Error('Unknown link status: ' + err);
}
}

View File

@@ -20,8 +20,7 @@ import sourceMap from '@zeit/source-map-support';
import { mkdirp } from 'fs-extra';
import chalk from 'chalk';
import epipebomb from 'epipebomb';
import checkForUpdate from 'update-check';
import ms from 'ms';
import updateNotifier from 'update-notifier';
import { URL } from 'url';
import * as Sentry from '@sentry/node';
import { NowBuildError } from '@vercel/build-utils';
@@ -46,11 +45,20 @@ import reportError from './util/report-error';
import getConfig from './util/get-config';
import * as ERRORS from './util/errors-ts';
import { NowError } from './util/now-error';
import { APIError } from './util/errors-ts.ts';
import { SENTRY_DSN } from './util/constants.ts';
import getUpdateCommand from './util/get-update-command';
import { metrics, shouldCollectMetrics } from './util/metrics.ts';
import { getCommandName, getTitleName } from './util/pkg-name.ts';
const isCanary = pkg.version.includes('canary');
// Checks for available update and returns an instance
const notifier = updateNotifier({
pkg,
distTag: isCanary ? 'canary' : 'latest',
});
const VERCEL_DIR = getGlobalPathConfig();
const VERCEL_CONFIG_PATH = configFiles.getConfigFilePath();
const VERCEL_AUTH_CONFIG_PATH = configFiles.getAuthConfigFilePath();
@@ -65,7 +73,7 @@ sourceMap.install();
Sentry.init({
dsn: SENTRY_DSN,
release: `vercel-cli@${pkg.version}`,
environment: pkg.version.includes('canary') ? 'canary' : 'stable',
environment: isCanary ? 'canary' : 'stable',
});
let debug = () => {};
@@ -127,38 +135,20 @@ const main = async argv_ => {
// (as in: `vercel ls`)
const targetOrSubcommand = argv._[2];
let update = null;
try {
if (targetOrSubcommand !== 'update') {
update = await checkForUpdate(pkg, {
interval: ms('1d'),
distTag: pkg.version.includes('canary') ? 'canary' : 'latest',
});
}
} catch (err) {
console.error(
error(`Checking for updates failed${isDebugging ? ':' : ''}`)
);
if (isDebugging) {
console.error(err);
}
}
if (update && isTTY) {
if (notifier.update && isTTY) {
const { latest } = notifier.update;
console.log(
info(
`${chalk.bgRed('UPDATE AVAILABLE')} ` +
`Run ${cmd(
await getUpdateCommand()
)} to install ${getTitleName()} CLI ${update.latest}`
)} to install ${getTitleName()} CLI ${latest}`
)
);
console.log(
info(
`Changelog: https://github.com/vercel/vercel/releases/tag/vercel@${update.latest}`
`Changelog: https://github.com/vercel/vercel/releases/tag/vercel@${latest}`
)
);
}
@@ -168,7 +158,7 @@ const main = async argv_ => {
`${getTitleName()} CLI ${pkg.version}${
targetOrSubcommand === 'dev' ? ' dev (beta)' : ''
}${
pkg.version.includes('canary') || targetOrSubcommand === 'dev'
isCanary || targetOrSubcommand === 'dev'
? ' — https://vercel.com/feedback'
: ''
}`
@@ -190,8 +180,7 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to find the ' +
'global directory: '}${err.message}`
`An unexpected error occurred while trying to find the global directory: ${err.message}`
)
);
@@ -204,8 +193,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to create the ' +
`global directory "${hp(VERCEL_DIR)}" `}${err.message}`
`${
'An unexpected error occurred while trying to create the ' +
`global directory "${hp(VERCEL_DIR)}" `
}${err.message}`
)
);
}
@@ -219,8 +210,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to find the ' +
`config file "${hp(VERCEL_CONFIG_PATH)}" `}${err.message}`
`${
'An unexpected error occurred while trying to find the ' +
`config file "${hp(VERCEL_CONFIG_PATH)}" `
}${err.message}`
)
);
@@ -235,8 +228,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to read the ' +
`config in "${hp(VERCEL_CONFIG_PATH)}" `}${err.message}`
`${
'An unexpected error occurred while trying to read the ' +
`config in "${hp(VERCEL_CONFIG_PATH)}" `
}${err.message}`
)
);
@@ -267,8 +262,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to write the ' +
`default config to "${hp(VERCEL_CONFIG_PATH)}" `}${err.message}`
`${
'An unexpected error occurred while trying to write the ' +
`default config to "${hp(VERCEL_CONFIG_PATH)}" `
}${err.message}`
)
);
@@ -283,8 +280,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to find the ' +
`auth file "${hp(VERCEL_AUTH_CONFIG_PATH)}" `}${err.message}`
`${
'An unexpected error occurred while trying to find the ' +
`auth file "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
}${err.message}`
)
);
@@ -301,8 +300,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to read the ' +
`auth config in "${hp(VERCEL_AUTH_CONFIG_PATH)}" `}${err.message}`
`${
'An unexpected error occurred while trying to read the ' +
`auth config in "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
}${err.message}`
)
);
@@ -326,10 +327,10 @@ const main = async argv_ => {
} catch (err) {
console.error(
error(
`${'An unexpected error occurred while trying to write the ' +
`default config to "${hp(VERCEL_AUTH_CONFIG_PATH)}" `}${
err.message
}`
`${
'An unexpected error occurred while trying to write the ' +
`default config to "${hp(VERCEL_AUTH_CONFIG_PATH)}" `
}${err.message}`
)
);
return 1;
@@ -639,6 +640,12 @@ const main = async argv_ => {
return 1;
}
if (err instanceof APIError && 400 <= err.status && err.status <= 499) {
err.message = err.serverMessage;
output.prettyError(err);
return 1;
}
if (shouldCollectMetrics) {
metric
.event(eventCategory, '1', pkg.version)

View File

@@ -83,6 +83,16 @@ export type Domain = {
};
};
export type DomainConfig = {
configuredBy: null | 'CNAME' | 'A' | 'http';
misconfigured: boolean;
serviceType: 'zeit.world' | 'external' | 'na';
nameservers: string[];
cnames: string[] & { traceString?: string };
aValues: string[] & { traceString?: string };
dnssecEnabled?: boolean;
};
export type Cert = {
uid: string;
autoRenew: boolean;
@@ -217,6 +227,16 @@ export type DNSRecordData =
| SRVRecordData
| MXRecordData;
export interface ProjectAliasTarget {
createdAt?: number;
domain: string;
redirect?: string | null;
target: 'PRODUCTION' | 'STAGING';
configuredBy?: null | 'CNAME' | 'A';
configuredChangedAt?: null | number;
configuredChangeAttempts?: [number, number];
}
export interface Secret {
uid: string;
name: string;
@@ -244,12 +264,21 @@ export interface ProjectEnvVariable {
system?: boolean;
}
export interface Project {
export interface ProjectSettings {
framework?: string | null;
devCommand?: string | null;
buildCommand?: string | null;
outputDirectory?: string | null;
rootDirectory?: string | null;
}
export interface Project extends ProjectSettings {
id: string;
name: string;
accountId: string;
updatedAt: number;
createdAt: number;
alias?: ProjectAliasTarget[];
devCommand?: string | null;
framework?: string | null;
rootDirectory?: string | null;
@@ -272,3 +301,8 @@ export interface PaginationOptions {
count: number;
next?: number;
}
export type ProjectLinkResult =
| { status: 'linked'; org: Org; project: Project }
| { status: 'not_linked'; org: null; project: null }
| { status: 'error'; exitCode: number };

View File

@@ -56,6 +56,8 @@ async function createBuildProcess(
workPath: string,
output: Output
): Promise<ChildProcess> {
output.debug(`Creating build process for "${match.entrypoint}"`);
const builderWorkerPath = join(__dirname, 'builder-worker.js');
// Ensure that `node` is in the builder's `PATH`
@@ -65,8 +67,6 @@ async function createBuildProcess(
...process.env,
PATH,
...envConfigs.allEnv,
NOW_REGION: 'dev1',
VERCEL_REGION: 'dev1',
};
const buildProcess = fork(builderWorkerPath, [], {
@@ -78,7 +78,7 @@ async function createBuildProcess(
buildProcess.on('exit', (code, signal) => {
output.debug(
`Build process for ${match.src} exited with ${signal || code}`
`Build process for "${match.entrypoint}" exited with ${signal || code}`
);
match.buildProcess = undefined;
});
@@ -128,7 +128,6 @@ export async function executeBuild(
let { buildProcess } = match;
if (!runInProcess && !buildProcess) {
devServer.output.debug(`Creating build process for ${entrypoint}`);
buildProcess = await createBuildProcess(
match,
envConfigs,
@@ -364,8 +363,6 @@ export async function executeBuild(
...nowConfig.env,
...asset.environment,
...envConfigs.runEnv,
NOW_REGION: 'dev1',
VERCEL_REGION: 'dev1',
},
},
});
@@ -431,7 +428,7 @@ export async function getBuildMatches(
}
const files = fileList
.filter(name => name === src || minimatch(name, src))
.filter(name => name === src || minimatch(name, src, { dot: true }))
.map(name => join(cwd, name));
if (files.length === 0) {

View File

@@ -4,7 +4,7 @@ import PCRE from 'pcre-to-regexp';
import isURL from './is-url';
import DevServer from './server';
import { HttpHeadersConfig, RouteResult } from './types';
import { NowConfig, HttpHeadersConfig, RouteResult } from './types';
import { isHandler, Route, HandleValue } from '@vercel/routing-utils';
export function resolveRouteParameters(
@@ -50,6 +50,7 @@ export async function devRouter(
reqMethod?: string,
routes?: Route[],
devServer?: DevServer,
nowConfig?: NowConfig,
previousHeaders?: HttpHeadersConfig,
missRoutes?: Route[],
phase?: HandleValue | null
@@ -117,9 +118,12 @@ export async function devRouter(
continue;
}
if (routeConfig.check && devServer && phase !== 'hit') {
if (routeConfig.check && devServer && nowConfig && phase !== 'hit') {
const { pathname = '/' } = url.parse(destPath);
const hasDestFile = await devServer.hasFilesystem(pathname);
const hasDestFile = await devServer.hasFilesystem(
pathname,
nowConfig
);
if (!hasDestFile) {
if (routeConfig.status && phase !== 'miss') {
@@ -131,6 +135,7 @@ export async function devRouter(
reqMethod,
missRoutes,
devServer,
nowConfig,
combinedHeaders,
[],
'miss'

View File

@@ -12,7 +12,7 @@ import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler';
import { watch, FSWatcher } from 'chokidar';
import { parse as parseDotenv } from 'dotenv';
import { basename, dirname, extname, join } from 'path';
import path, { isAbsolute, basename, dirname, extname, join } from 'path';
import once from '@tootallnate/once';
import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port';
@@ -49,7 +49,7 @@ import getNowConfigPath from '../config/local-path';
import { MissingDotenvVarsError } from '../errors-ts';
import cliPkg from '../pkg';
import { getVercelDirectory } from '../projects/link';
import { staticFiles as getFiles, getAllProjectFiles } from '../get-files';
import { staticFiles as getFiles } from '../get-files';
import { validateConfig } from './validate';
import { devRouter, getRoutesTypes } from './router';
import getMimeType from './mime-type';
@@ -83,12 +83,17 @@ import {
HttpHeadersConfig,
EnvConfigs,
} from './types';
import { ProjectSettings } from '../../types';
interface FSEvent {
type: string;
path: string;
}
type WithFileNameSymbol<T> = T & {
[fileNameSymbol]: string;
};
function sortBuilders(buildA: Builder, buildB: Builder) {
if (buildA && buildA.use && isOfficialRuntime('static-build', buildA.use)) {
return 1;
@@ -107,7 +112,7 @@ export default class DevServer {
public output: Output;
public proxy: httpProxy;
public envConfigs: EnvConfigs;
public frameworkSlug: string | null;
public frameworkSlug?: string;
public files: BuilderInputs;
public address: string;
public devCacheDir: string;
@@ -129,19 +134,26 @@ export default class DevServer {
private devProcess?: ChildProcess;
private devProcessPort?: number;
private devServerPids: Set<number>;
private projectSettings?: ProjectSettings;
private getNowConfigPromise: Promise<NowConfig> | null;
private blockingBuildsPromise: Promise<void> | null;
private updateBuildersPromise: Promise<void> | null;
private updateBuildersTimeout: NodeJS.Timeout | undefined;
private startPromise: Promise<void> | null;
private environmentVars: Env | undefined;
constructor(cwd: string, options: DevServerOptions) {
this.cwd = cwd;
this.debug = options.debug;
this.output = options.output;
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
this.environmentVars = options.environmentVars;
this.files = {};
this.address = '';
this.devCommand = options.devCommand;
this.projectSettings = options.projectSettings;
this.frameworkSlug = options.frameworkSlug;
this.caseSensitive = false;
this.apiDir = null;
@@ -158,17 +170,17 @@ export default class DevServer {
this.inProgressBuilds = new Map();
this.devCacheDir = join(getVercelDirectory(cwd), 'cache');
this.getNowConfigPromise = null;
this.blockingBuildsPromise = null;
this.updateBuildersPromise = null;
this.startPromise = null;
this.watchAggregationId = null;
this.watchAggregationEvents = [];
this.watchAggregationTimeout = 500;
this.filter = path => Boolean(path);
this.podId = Math.random()
.toString(32)
.slice(-5);
this.podId = Math.random().toString(32).slice(-5);
this.devServerPids = new Set();
}
@@ -267,18 +279,19 @@ export default class DevServer {
for (const [result, [requestPath, match]] of needsRebuild) {
if (
requestPath === null ||
(await shouldServe(match, this.files, requestPath, this))
(await shouldServe(match, this.files, requestPath, this, nowConfig))
) {
this.triggerBuild(
match,
requestPath,
null,
nowConfig,
result,
filesChangedArray,
filesRemovedArray
).catch((err: Error) => {
this.output.warn(
`An error occurred while rebuilding ${match.src}:`
`An error occurred while rebuilding \`${match.src}\`:`
);
console.error(err.stack);
});
@@ -365,9 +378,7 @@ export default class DevServer {
const sources = matches.map(m => m.src);
if (isInitial && fileList.length === 0) {
this.output.warn(
'There are no files (or only files starting with a dot) inside your deployment.'
);
this.output.warn('There are no files inside your deployment.');
}
// Delete build matches that no longer exists
@@ -423,6 +434,9 @@ export default class DevServer {
`Cleaning up "blockingBuildsPromise" after error: ${err}`
);
this.blockingBuildsPromise = null;
if (err) {
this.output.prettyError(err);
}
});
}
@@ -470,13 +484,16 @@ export default class DevServer {
const dotenv = await fs.readFile(filePath, 'utf8');
this.output.debug(`Using local env: ${filePath}`);
env = parseDotenv(dotenv);
env = this.populateVercelEnvVars(env);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
try {
return this.validateEnvConfig(fileName, base || {}, env);
return {
...this.validateEnvConfig(fileName, base || {}, env),
};
} catch (err) {
if (err instanceof MissingDotenvVarsError) {
this.output.error(err.message);
@@ -488,41 +505,35 @@ export default class DevServer {
return {};
}
async getNowConfig(): Promise<NowConfig> {
const pkg = await this.getPackageJson();
clearNowConfigPromise = () => {
this.getNowConfigPromise = null;
};
// The default empty `vercel.json` is used to serve all files as static
// when no `vercel.json` is present
let configPath = 'vercel.json';
let config: NowConfig = {
version: 2,
[fileNameSymbol]: configPath,
};
try {
configPath = getNowConfigPath(this.cwd);
this.output.debug(`Reading ${configPath}`);
config = JSON.parse(await fs.readFile(configPath, 'utf8'));
config[fileNameSymbol] = basename(configPath);
} catch (err) {
if (err.code === 'ENOENT') {
this.output.debug(err.toString());
} else if (err.name === 'SyntaxError') {
this.output.warn(
`There is a syntax error in ${configPath}: ${err.message}`
);
} else {
throw err;
}
getNowConfig(): Promise<NowConfig> {
if (this.getNowConfigPromise) {
return this.getNowConfigPromise;
}
this.getNowConfigPromise = this._getNowConfig();
const allFiles = await getAllProjectFiles(this.cwd, this.output);
const files = allFiles.filter(this.filter);
// Clean up the promise once it has resolved
const clear = this.clearNowConfigPromise;
this.getNowConfigPromise.finally(clear);
this.output.debug(
`Found ${allFiles.length} and ` +
`filtered out ${allFiles.length - files.length} files`
);
return this.getNowConfigPromise;
}
async _getNowConfig(): Promise<NowConfig> {
const configPath = getNowConfigPath(this.cwd);
const [
pkg = null,
// The default empty `vercel.json` is used to serve all
// files as static when no `vercel.json` is present
config = { version: 2, [fileNameSymbol]: 'vercel.json' },
] = await Promise.all([
this.readJsonFile<PackageJson>('package.json'),
this.readJsonFile<NowConfig>(configPath),
]);
await this.validateNowConfig(config);
const { error: routeError, routes: maybeRoutes } = getTransformedRoutes({
@@ -539,6 +550,11 @@ export default class DevServer {
const featHandleMiss = true; // enable for zero config
const { projectSettings, cleanUrls, trailingSlash } = config;
const opts = { output: this.output, isBuilds: true };
const files = (await getFiles(this.cwd, config, opts)).map(f =>
relative(this.cwd, f)
);
let {
builders,
warnings,
@@ -550,7 +566,7 @@ export default class DevServer {
} = await detectBuilders(files, pkg, {
tag: getDistTag(cliPkg.version) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
...(projectSettings ? { projectSettings } : {}),
projectSettings: projectSettings || this.projectSettings,
featHandleMiss,
cleanUrls,
trailingSlash,
@@ -572,6 +588,8 @@ export default class DevServer {
config.builds = config.builds || [];
config.builds.push(...builders);
delete config.functions;
}
let routes: Route[] = [];
@@ -610,38 +628,52 @@ export default class DevServer {
this.apiExtensions = detectApiExtensions(config.builds || []);
// Update the env vars configuration
const configBuild = config.build || {};
const [runEnv, buildEnv] = await Promise.all([
let [runEnv, buildEnv] = await Promise.all([
this.getLocalEnv('.env', config.env),
this.getLocalEnv('.env.build', configBuild.env),
this.getLocalEnv('.env.build', config.build?.env),
]);
const allEnv = { ...buildEnv, ...runEnv };
this.envConfigs = { buildEnv, runEnv, allEnv };
let allEnv = { ...buildEnv, ...runEnv };
// If no .env/.build.env is present, fetch and use cloud environment variables
if (Object.keys(allEnv).length === 0) {
const cloudEnv = this.populateVercelEnvVars(this.environmentVars);
allEnv = runEnv = buildEnv = cloudEnv;
}
this.envConfigs = { buildEnv, runEnv, allEnv };
return config;
}
async getPackageJson(): Promise<PackageJson | null> {
const pkgPath = join(this.cwd, 'package.json');
let pkg: PackageJson | null = null;
this.output.debug('Reading `package.json` file');
async readJsonFile<T>(
filePath: string
): Promise<WithFileNameSymbol<T> | void> {
let rel, abs;
if (isAbsolute(filePath)) {
rel = path.relative(this.cwd, filePath);
abs = filePath;
} else {
rel = filePath;
abs = join(this.cwd, filePath);
}
this.output.debug(`Reading \`${rel}\` file`);
try {
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
const raw = await fs.readFile(abs, 'utf8');
const parsed: WithFileNameSymbol<T> = JSON.parse(raw);
parsed[fileNameSymbol] = rel;
return parsed;
} catch (err) {
if (err.code === 'ENOENT') {
this.output.debug('No `package.json` file present');
this.output.debug(`No \`${rel}\` file present`);
} else if (err.name === 'SyntaxError') {
this.output.warn(
`There is a syntax error in the \`package.json\` file: ${err.message}`
`There is a syntax error in the \`${rel}\` file: ${err.message}`
);
} else {
throw err;
}
}
return pkg;
}
async tryValidateOrExit(
@@ -714,6 +746,26 @@ export default class DevServer {
return merged;
}
populateVercelEnvVars(env: Env | undefined): Env {
if (!env) {
return {};
}
for (const name of Object.keys(env)) {
if (name === 'VERCEL_URL') {
const host = new URL(this.address).host;
env['VERCEL_URL'] = host;
} else if (name === 'VERCEL_REGION') {
env['VERCEL_REGION'] = 'dev1';
}
}
// Always set NOW_REGION to match production
env['NOW_REGION'] = 'dev1';
return env;
}
/**
* Create an array of from builder inputs
* and filter them
@@ -722,10 +774,20 @@ export default class DevServer {
return Object.keys(files).filter(this.filter);
}
start(...listenSpec: ListenSpec): Promise<void> {
if (!this.startPromise) {
this.startPromise = this._start(...listenSpec).catch(err => {
this.stop();
throw err;
});
}
return this.startPromise;
}
/**
* Launches the `vercel dev` server.
*/
async start(...listenSpec: ListenSpec): Promise<void> {
async _start(...listenSpec: ListenSpec): Promise<void> {
if (!fs.existsSync(this.cwd)) {
throw new Error(`${chalk.bold(this.cwd)} doesn't exist`);
}
@@ -737,8 +799,39 @@ export default class DevServer {
const { ig } = await getVercelIgnore(this.cwd);
this.filter = ig.createFilter();
// Retrieve the path of the native module
let address: string | null = null;
while (typeof address !== 'string') {
try {
address = await listen(this.server, ...listenSpec);
} catch (err) {
this.output.debug(`Got listen error: ${err.code}`);
if (err.code === 'EADDRINUSE') {
if (typeof listenSpec[0] === 'number') {
// Increase port and try again
this.output.note(
`Requested port ${chalk.yellow(
String(listenSpec[0])
)} is already in use`
);
listenSpec[0]++;
} else {
this.output.error(
`Requested socket ${chalk.cyan(listenSpec[0])} is already in use`
);
process.exit(1);
}
} else {
throw err;
}
}
}
this.address = address
.replace('[::]', 'localhost')
.replace('127.0.0.1', 'localhost');
const nowConfig = await this.getNowConfig();
const devCommandPromise = this.runDevCommand();
const opts = { output: this.output, isBuilds: true };
const files = await getFiles(this.cwd, nowConfig, opts);
@@ -830,39 +923,6 @@ export default class DevServer {
this.proxy.ws(req, socket, head, { target });
});
const devCommandPromise = this.runDevCommand();
let address: string | null = null;
while (typeof address !== 'string') {
try {
address = await listen(this.server, ...listenSpec);
} catch (err) {
this.output.debug(`Got listen error: ${err.code}`);
if (err.code === 'EADDRINUSE') {
if (typeof listenSpec[0] === 'number') {
// Increase port and try again
this.output.note(
`Requested port ${chalk.yellow(
String(listenSpec[0])
)} is already in use`
);
listenSpec[0]++;
} else {
this.output.error(
`Requested socket ${chalk.cyan(listenSpec[0])} is already in use`
);
process.exit(1);
}
} else {
throw err;
}
}
}
this.address = address
.replace('[::]', 'localhost')
.replace('127.0.0.1', 'localhost');
await devCommandPromise;
this.output.ready(`Available at ${link(this.address)}`);
@@ -1071,10 +1131,7 @@ export default class DevServer {
const allHeaders = {
'cache-control': 'public, max-age=0, must-revalidate',
...headers,
server: 'now',
'x-now-trace': 'dev1',
'x-now-id': nowRequestId,
'x-now-cache': 'MISS',
server: 'Vercel',
'x-vercel-id': nowRequestId,
'x-vercel-cache': 'MISS',
};
@@ -1088,29 +1145,31 @@ export default class DevServer {
*/
getNowProxyHeaders(
req: http.IncomingMessage,
nowRequestId: string
nowRequestId: string,
xfwd: boolean
): http.IncomingHttpHeaders {
const ip = this.getRequestIp(req);
const { host } = req.headers;
return {
...req.headers,
Connection: 'close',
'x-forwarded-host': host,
'x-forwarded-proto': 'http',
'x-forwarded-for': ip,
const headers: http.IncomingHttpHeaders = {
connection: 'close',
'x-real-ip': ip,
'x-now-trace': 'dev1',
'x-now-deployment-url': host,
'x-now-id': nowRequestId,
'x-now-log-id': nowRequestId.split('-')[2],
'x-zeit-co-forwarded-for': ip,
'x-vercel-deployment-url': host,
'x-vercel-forwarded-for': ip,
'x-vercel-id': nowRequestId,
};
if (xfwd) {
headers['x-forwarded-host'] = host;
headers['x-forwarded-proto'] = 'http';
headers['x-forwarded-for'] = ip;
}
return headers;
}
async triggerBuild(
match: BuildMatch,
requestPath: string | null,
req: http.IncomingMessage | null,
nowConfig: NowConfig,
previousBuildResult?: BuildResult,
filesChanged?: string[],
filesRemoved?: string[]
@@ -1126,10 +1185,11 @@ export default class DevServer {
// A build for `buildKey` is already in progress, so don't trigger
// another rebuild for this request - just wait on the existing one.
let msg = `De-duping build "${buildKey}"`;
if (req) msg += ` for "${req.method} ${req.url}"`;
if (req) {
msg += ` for "${req.method} ${req.url}"`;
}
this.output.debug(msg);
} else {
const nowConfig = await this.getNowConfig();
if (previousBuildResult) {
// Tear down any `output` assets from a previous build, so that they
// are not available to be served while the rebuild is in progress.
@@ -1140,7 +1200,9 @@ export default class DevServer {
}
}
let msg = `Building asset "${buildKey}"`;
if (req) msg += ` for "${req.method} ${req.url}"`;
if (req) {
msg += ` for "${req.method} ${req.url}"`;
}
this.output.debug(msg);
buildPromise = executeBuild(
nowConfig,
@@ -1182,6 +1244,8 @@ export default class DevServer {
req: http.IncomingMessage,
res: http.ServerResponse
) => {
await this.startPromise;
let nowRequestId = generateRequestId(this.podId);
if (this.stopping) {
@@ -1221,7 +1285,7 @@ export default class DevServer {
const { status, headers, dest } = routeResult;
const location = headers['location'] || dest;
if (status && location && (300 <= status && status <= 399)) {
if (status && location && 300 <= status && status <= 399) {
this.output.debug(`Route found with redirect status code ${status}`);
await this.sendRedirect(req, res, nowRequestId, location, status);
return true;
@@ -1311,6 +1375,7 @@ export default class DevServer {
req.method,
phaseRoutes,
this,
nowConfig,
prevHeaders,
missRoutes,
phase
@@ -1338,7 +1403,8 @@ export default class DevServer {
this.buildMatches,
this.files,
routeResult.dest,
this
this,
nowConfig
);
if (
@@ -1361,6 +1427,7 @@ export default class DevServer {
req.method,
missRoutes,
this,
nowConfig,
routeResult.headers,
[],
'miss'
@@ -1370,7 +1437,8 @@ export default class DevServer {
this.buildMatches,
this.files,
routeResult.dest,
this
this,
nowConfig
);
if (
await this.exitWithStatus(
@@ -1393,6 +1461,7 @@ export default class DevServer {
req.method,
hitRoutes,
this,
nowConfig,
routeResult.headers,
[],
'hit'
@@ -1407,6 +1476,7 @@ export default class DevServer {
req.method,
errorRoutes,
this,
nowConfig,
routeResult.headers,
[],
'error'
@@ -1416,7 +1486,8 @@ export default class DevServer {
this.buildMatches,
this.files,
routeResultForError.dest,
this
this,
nowConfig
);
if (matchForError) {
@@ -1441,9 +1512,9 @@ export default class DevServer {
const { dest, headers, uri_args } = routeResult;
// Set any headers defined in the matched `route` config
Object.entries(headers).forEach(([name, value]) => {
for (const [name, value] of Object.entries(headers)) {
res.setHeader(name, value);
});
}
if (statusCode) {
// Set the `statusCode` as read-only so that `http-proxy`
@@ -1466,6 +1537,13 @@ export default class DevServer {
if (this.devProcessPort) {
const upstream = `http://localhost:${this.devProcessPort}`;
debug(`Proxying to frontend dev server: ${upstream}`);
// Add the Vercel platform proxy request headers
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
for (const [name, value] of Object.entries(headers)) {
req.headers[name] = value;
}
this.setResponseHeaders(res, nowRequestId);
const origUrl = url.parse(req.url || '/', true);
delete origUrl.search;
@@ -1504,7 +1582,8 @@ export default class DevServer {
newUrl,
req.method,
buildResult.routes,
this
this,
nowConfig
);
if (matchedRoute.found && callLevel === 0) {
debug(`Found matching route ${matchedRoute.dest} for ${newUrl}`);
@@ -1584,6 +1663,12 @@ export default class DevServer {
query: parsed.query,
});
// Add the Vercel platform proxy request headers
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
for (const [name, value] of Object.entries(headers)) {
req.headers[name] = value;
}
this.setResponseHeaders(res, nowRequestId);
return proxyPass(
req,
@@ -1600,7 +1685,7 @@ export default class DevServer {
let foundAsset = findAsset(match, requestPath, nowConfig);
if (!foundAsset && callLevel === 0) {
await this.triggerBuild(match, buildRequestPath, req);
await this.triggerBuild(match, buildRequestPath, req, nowConfig);
// Since the `asset` was just built, resolve again to get the new asset
foundAsset = findAsset(match, requestPath, nowConfig);
@@ -1614,6 +1699,13 @@ export default class DevServer {
(!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda'))
) {
debug('Proxying to frontend dev server');
// Add the Vercel platform proxy request headers
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
for (const [name, value] of Object.entries(headers)) {
req.headers[name] = value;
}
this.setResponseHeaders(res, nowRequestId);
return proxyPass(
req,
@@ -1632,8 +1724,9 @@ export default class DevServer {
const { asset, assetKey } = foundAsset;
debug(
`Serving asset: [${asset.type}] ${assetKey} ${(asset as any)
.contentType || ''}`
`Serving asset: [${asset.type}] ${assetKey} ${
(asset as any).contentType || ''
}`
);
/* eslint-disable no-case-declarations */
@@ -1695,7 +1788,10 @@ export default class DevServer {
method: req.method || 'GET',
host: req.headers.host,
path,
headers: this.getNowProxyHeaders(req, nowRequestId),
headers: {
...req.headers,
...this.getNowProxyHeaders(req, nowRequestId, true),
},
encoding: 'base64',
body: body.toString('base64'),
};
@@ -1825,8 +1921,17 @@ export default class DevServer {
return true;
}
async hasFilesystem(dest: string): Promise<boolean> {
if (await findBuildMatch(this.buildMatches, this.files, dest, this, true)) {
async hasFilesystem(dest: string, nowConfig: NowConfig): Promise<boolean> {
if (
await findBuildMatch(
this.buildMatches,
this.files,
dest,
this,
nowConfig,
true
)
) {
return true;
}
return false;
@@ -1856,8 +1961,6 @@ export default class DevServer {
...(this.frameworkSlug === 'create-react-app' ? { BROWSER: 'none' } : {}),
...process.env,
...this.envConfigs.allEnv,
NOW_REGION: 'dev1',
VERCEL_REGION: 'dev1',
PORT: `${port}`,
};
@@ -2007,13 +2110,23 @@ async function findBuildMatch(
files: BuilderInputs,
requestPath: string,
devServer: DevServer,
isFilesystem?: boolean
nowConfig: NowConfig,
isFilesystem = false
): Promise<BuildMatch | null> {
requestPath = requestPath.replace(/^\//, '');
let bestIndexMatch: undefined | BuildMatch;
for (const match of matches.values()) {
if (await shouldServe(match, files, requestPath, devServer, isFilesystem)) {
if (
await shouldServe(
match,
files,
requestPath,
devServer,
nowConfig,
isFilesystem
)
) {
if (!isIndex(match.src)) {
return match;
} else {
@@ -2035,14 +2148,14 @@ async function shouldServe(
files: BuilderInputs,
requestPath: string,
devServer: DevServer,
isFilesystem?: boolean
nowConfig: NowConfig,
isFilesystem = false
): Promise<boolean> {
const {
src,
config,
builderWithPkg: { builder },
} = match;
const nowConfig = await devServer.getNowConfig();
const cleanSrc = src.endsWith('.html') ? src.slice(0, -5) : src;
const trimmedPath = requestPath.endsWith('/')
? requestPath.slice(0, -1)
@@ -2086,7 +2199,7 @@ async function shouldServe(
return true;
} else if (
!isFilesystem &&
(await findMatchingRoute(match, requestPath, devServer))
(await findMatchingRoute(match, requestPath, devServer, nowConfig))
) {
// If there's no `shouldServe()` function and no matched asset, then look
// up if there's a matching build route on the `match` that has already
@@ -2099,7 +2212,8 @@ async function shouldServe(
async function findMatchingRoute(
match: BuildMatch,
requestPath: string,
devServer: DevServer
devServer: DevServer,
nowConfig: NowConfig
): Promise<RouteResult | void> {
const reqUrl = `/${requestPath}`;
for (const buildResult of match.buildResults.values()) {
@@ -2108,7 +2222,8 @@ async function findMatchingRoute(
reqUrl,
undefined,
buildResult.routes,
devServer
devServer,
nowConfig
);
if (route.found) {
return route;
@@ -2163,7 +2278,9 @@ function isIndex(path: string): boolean {
}
function minimatches(files: string[], pattern: string): boolean {
return files.some(file => file === pattern || minimatch(file, pattern));
return files.some(
file => file === pattern || minimatch(file, pattern, { dot: true })
);
}
function fileChanged(

View File

@@ -17,19 +17,22 @@ import {
import { NowConfig } from '@vercel/client';
import { HandleValue, Route } from '@vercel/routing-utils';
import { Output } from '../output';
import { ProjectSettings } from '../../types';
export { NowConfig };
export interface DevServerOptions {
output: Output;
debug: boolean;
devCommand: string | undefined;
frameworkSlug: string | null;
devCommand?: string;
frameworkSlug?: string;
projectSettings?: ProjectSettings;
environmentVars?: Env;
}
export interface EnvConfigs {
/**
* environment variables from `.env.build` file (deprecated)
* environment variables from `.env.build` file
*/
buildEnv: Env;

View File

@@ -34,10 +34,9 @@ const vercelConfigSchema = {
};
const ajv = new Ajv();
const validate = ajv.compile(vercelConfigSchema);
export function validateConfig(config: NowConfig): NowBuildError | null {
const validate = ajv.compile(vercelConfigSchema);
if (!validate(config)) {
if (validate.errors && validate.errors[0]) {
const error = validate.errors[0];
@@ -48,5 +47,14 @@ export function validateConfig(config: NowConfig): NowBuildError | null {
}
}
if (config.functions && config.builds) {
return new NowBuildError({
code: 'FUNCTIONS_AND_BUILDS',
message:
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.',
link: 'https://vercel.link/functions-and-builds',
});
}
return null;
}

View File

@@ -0,0 +1,29 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { DomainConfig } from '../../types';
export async function getDomainConfig(
client: Client,
contextName: string,
domainName: string
) {
const cancelWait = wait(
`Fetching domain config ${domainName} under ${chalk.bold(contextName)}`
);
try {
const config = await client.fetch<DomainConfig>(
`/v4/domains/${domainName}/config`
);
return config;
} catch (error) {
if (error.status < 500) {
return error;
}
throw error;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,33 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { Domain } from '../../types';
type Response = {
domain: Domain;
};
export async function getDomain(
client: Client,
contextName: string,
domainName: string
) {
const cancelWait = wait(
`Fetching domain ${domainName} under ${chalk.bold(contextName)}`
);
try {
const { domain } = await client.fetch<Response>(
`/v4/domains/${domainName}`
);
return domain;
} catch (error) {
if (error.status < 500) {
return error;
}
throw error;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,3 @@
export function isPublicSuffix(domainName: string) {
return domainName.endsWith('.vercel.app') || domainName.endsWith('.now.sh');
}

View File

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

View File

@@ -18,3 +18,18 @@ export default function formatDate(dateStrOrNumber?: number | string | null) {
`[in ${ms(diff)}]`
)}`;
}
export function formatDateWithoutTime(
dateStrOrNumber?: number | string | null
) {
if (!dateStrOrNumber) {
return chalk.gray('-');
}
const date = new Date(dateStrOrNumber);
const diff = date.getTime() - Date.now();
return diff < 0
? `${format(date, 'MMM DD YYYY')} ${chalk.gray(`[${ms(-diff)} ago]`)}`
: `${format(date, 'MMM DD YYYY')} ${chalk.gray(`[in ${ms(diff)}]`)}`;
}

View File

@@ -20,7 +20,7 @@ import strlen from './strlen';
export default function formatTable(
header: string[],
align: Array<'l' | 'r' | 'c' | '.'>,
blocks: { name: string; rows: string[][] }[],
blocks: { name?: string; rows: string[][] }[],
hsep = ' '
) {
const nrCols = header.length;
@@ -50,8 +50,10 @@ export default function formatTable(
for (let j = 0; j < nrCols; j++) {
const col = `${row[j]}`;
const al = align[j] || 'l';
const spaces = Math.max(padding[j] * 8 - strlen(col), 0);
const pad = ' '.repeat(spaces);
const repeat = padding[j] > 1 ? padding[j] * 8 - strlen(col) : 0;
const pad = repeat > 0 ? ' '.repeat(repeat) : '';
rows[i][j] = al === 'l' ? col + pad : pad + col;
}
}

View File

@@ -0,0 +1,46 @@
import getEnvVariables from './env/get-env-records';
import getDecryptedSecret from './env/get-decrypted-secret';
import Client from './client';
import { Output } from './output/create-output';
import { ProjectEnvTarget, Project } from '../types';
import { Env } from '@vercel/build-utils';
export default async function getDecryptedEnvRecords(
output: Output,
client: Client,
project: Project,
target: ProjectEnvTarget
): Promise<Env> {
const envs = await getEnvVariables(output, client, project.id, 4, target);
const decryptedValues = await Promise.all(
envs.map(async env => {
try {
const value = await getDecryptedSecret(output, client, env.value);
return { value, found: true };
} catch (error) {
if (error && error.status === 404) {
return { value: '', found: false };
}
throw error;
}
})
);
const results: Env = {};
for (let i = 0; i < decryptedValues.length; i++) {
const { key } = envs[i];
const { value, found } = decryptedValues[i];
if (!found) {
output.print('');
output.warn(
`Unable to download variable ${key} because associated secret was deleted`
);
continue;
}
results[key] = value ? value : '';
}
return results;
}

View File

@@ -1,7 +1,7 @@
import { resolve, join } from 'path';
import { resolve } from 'path';
import ignore from 'ignore';
import dockerignore from '@zeit/dockerignore';
import _glob, { IOptions } from 'glob';
import _glob, { IOptions as GlobOptions } from 'glob';
import fs from 'fs-extra';
import { getVercelIgnore } from '@vercel/client';
import IGNORED from './ignored';
@@ -12,11 +12,11 @@ import { NowConfig } from './dev/types';
type NullableString = string | null;
const flatten = (
function flatten(
arr: NullableString[] | NullableString[][],
res: NullableString[] = []
) => {
for (let cur of arr) {
): NullableString[] {
for (const cur of arr) {
if (Array.isArray(cur)) {
flatten(cur, res);
} else {
@@ -24,21 +24,17 @@ const flatten = (
}
}
return res;
};
}
const glob = async function(pattern: string, options: IOptions) {
return new Promise<string[]>((resolve, reject) => {
_glob(pattern, options, (error, files) => {
if (error) {
reject(error);
} else {
resolve(files);
}
async function glob(pattern: string, options: GlobOptions): Promise<string[]> {
return new Promise((resolve, reject) => {
_glob(pattern, options, (err, files) => {
err ? reject(err) : resolve(files);
});
});
};
}
interface WalkSyncOptions {
interface WalkOptions {
output: Output;
}
@@ -51,27 +47,27 @@ interface WalkSyncOptions {
* - `output` {Object} "output" helper object
* @returns {Array}
*/
const walkSync = async (
async function walk(
dir: string,
path: string,
filelist: string[] = [],
opts: WalkSyncOptions
) => {
opts: WalkOptions
) {
const { debug } = opts.output;
const dirc = await fs.readdir(asAbsolute(dir, path));
for (let file of dirc) {
file = asAbsolute(file, dir);
try {
const file_stat = await fs.stat(file);
filelist = file_stat.isDirectory()
? await walkSync(file, path, filelist, opts)
const fileStat = await fs.stat(file);
filelist = fileStat.isDirectory()
? await walk(file, path, filelist, opts)
: filelist.concat(file);
} catch (e) {
debug(`Ignoring invalid file ${file}`);
}
}
return filelist;
};
}
interface FilesInWhitelistOptions {
output: Output;
@@ -85,7 +81,7 @@ interface FilesInWhitelistOptions {
* - `output` {Object} "output" helper object
* @returns {Array} the expanded list of whitelisted files.
*/
const getFilesInWhitelist = async function(
const getFilesInWhitelist = async function (
whitelist: string[],
path: string,
opts: FilesInWhitelistOptions
@@ -97,10 +93,10 @@ const getFilesInWhitelist = async function(
whitelist.map(async (file: string) => {
file = asAbsolute(file, path);
try {
const file_stat = await fs.stat(file);
if (file_stat.isDirectory()) {
const dir_files = await walkSync(file, path, [], opts);
files.push(...dir_files);
const fileStat = await fs.stat(file);
if (fileStat.isDirectory()) {
const dirFiles = await walk(file, path, [], opts);
files.push(...dirFiles);
} else {
files.push(file);
}
@@ -117,7 +113,7 @@ const getFilesInWhitelist = async function(
* because ignore doesn't like them :|
*/
const clearRelative = function(str: string) {
const clearRelative = function (str: string) {
return str.replace(/(\n|^)\.\//g, '$1');
};
@@ -127,7 +123,7 @@ const clearRelative = function(str: string) {
* @return {String} results or `''`
*/
const maybeRead = async function<T>(path: string, default_: T) {
const maybeRead = async function <T>(path: string, default_: T) {
try {
return await fs.readFile(path, 'utf8');
} catch (err) {
@@ -143,7 +139,7 @@ const maybeRead = async function<T>(path: string, default_: T) {
* @param {String} parent full path
*/
const asAbsolute = function(path: string, parent: string) {
const asAbsolute = function (path: string, parent: string) {
if (path[0] === '/') {
return path;
}
@@ -272,7 +268,7 @@ export async function npm(
const search = Array.prototype.concat.apply(
[],
await Promise.all(
search_.map(file =>
search_.map((file) =>
glob(file, { cwd: path, absolute: true, dot: true })
)
)
@@ -364,7 +360,7 @@ export async function docker(
const search_ = ['.'];
// Convert all filenames into absolute paths
const search = search_.map(file => asAbsolute(file, path));
const search = search_.map((file) => asAbsolute(file, path));
// Compile list of ignored patterns and files
const dockerIgnore = await maybeRead(resolve(path, '.dockerignore'), null);
@@ -382,7 +378,7 @@ export async function docker(
.createFilter();
const prefixLength = path.length + 1;
const accepts = function(file: string) {
const accepts = function (file: string) {
const relativePath = file.substr(prefixLength);
if (relativePath === '') {
@@ -415,24 +411,6 @@ export async function docker(
return uniqueStrings(files);
}
/**
* Get a list of all files inside the project folder
*
* @param {String} of the current working directory
* @param {Object} output instance
* @return {Array} of {String}s with the found files
*/
export async function getAllProjectFiles(cwd: string, { debug }: Output) {
// We need a slash at the end to remove it later on from the matched files
const current = join(resolve(cwd), '/');
debug(`Searching files inside of ${current}`);
const list = await glob('**', { cwd: current, absolute: true, nodir: true });
// We need to replace \ with / for windows
return list.map(file => file.replace(current.replace(/\\/g, '/'), ''));
}
interface ExplodeOptions {
accepts: (file: string) => boolean;
output: Output;
@@ -482,7 +460,7 @@ async function explode(
if (s.isDirectory()) {
const all = await fs.readdir(file);
/* eslint-disable no-use-before-define */
const recursive = many(all.map(subdir => asAbsolute(subdir, file)));
const recursive = many(all.map((subdir) => asAbsolute(subdir, file)));
return (recursive as any) as Promise<string | null>;
/* eslint-enable no-use-before-define */
}
@@ -494,7 +472,7 @@ async function explode(
return path;
};
const many = (all: string[]) => Promise.all(all.map(file => list(file)));
const many = (all: string[]) => Promise.all(all.map((file) => list(file)));
const arrayOfArrays = await many(paths);
return flatten(arrayOfArrays).filter(notNull);
}

View File

@@ -13,6 +13,6 @@ export default async function getTeamById(
team = await client.fetch<Team>(`/teams/${teamId}`);
teamCache.set(teamId, team);
}
return team;
}

View File

@@ -4,18 +4,15 @@ import chalk from 'chalk';
import { Output } from '../output';
import { Framework } from '@vercel/frameworks';
import { isSettingValue } from '../is-setting-value';
import { ProjectSettings } from '../../types';
export interface ProjectSettings {
export interface PartialProjectSettings {
buildCommand: string | null;
outputDirectory: string | null;
devCommand: string | null;
}
export interface ProjectSettingsWithFramework extends ProjectSettings {
framework: string | null;
}
const fields: { name: string; value: keyof ProjectSettings }[] = [
const fields: { name: string; value: keyof PartialProjectSettings }[] = [
{ name: 'Build Command', value: 'buildCommand' },
{ name: 'Output Directory', value: 'outputDirectory' },
{ name: 'Development Command', value: 'devCommand' },
@@ -23,13 +20,15 @@ const fields: { name: string; value: keyof ProjectSettings }[] = [
export default async function editProjectSettings(
output: Output,
projectSettings: ProjectSettings | null,
framework: Framework | null
) {
projectSettings: PartialProjectSettings | null,
framework: Framework | null,
autoConfirm: boolean
): Promise<ProjectSettings> {
// create new settings object, missing values will be filled with `null`
const settings: Partial<ProjectSettingsWithFramework> = {
...projectSettings,
};
const settings: ProjectSettings = Object.assign(
{ framework: null },
projectSettings
);
for (let field of fields) {
settings[field.value] =
@@ -44,8 +43,8 @@ export default async function editProjectSettings(
output.print(
!framework.slug
? `No framework detected. Default project settings:\n`
: `Auto-detected project settings (${chalk.bold(framework.name)}):\n`
? `No framework detected. Default Project Settings:\n`
: `Auto-detected Project Settings (${chalk.bold(framework.name)}):\n`
);
settings.framework = framework.slug;
@@ -64,7 +63,10 @@ export default async function editProjectSettings(
);
}
if (!(await confirm(`Want to override the settings?`, false))) {
if (
autoConfirm ||
!(await confirm(`Want to override the settings?`, false))
) {
return settings;
}
@@ -75,7 +77,7 @@ export default async function editProjectSettings(
choices: fields,
});
for (let setting of settingFields as (keyof ProjectSettings)[]) {
for (let setting of settingFields as (keyof PartialProjectSettings)[]) {
const field = fields.find(f => f.value === setting);
const name = `${Date.now()}`;
const answers = await inquirer.prompt({

View File

@@ -15,10 +15,6 @@ export default async function inputProject(
detectedProjectName: string,
autoConfirm: boolean
): Promise<Project | string> {
if (autoConfirm) {
return detectedProjectName;
}
const slugifiedName = slugify(detectedProjectName);
// attempt to auto-detect a project to link
@@ -42,6 +38,10 @@ export default async function inputProject(
} catch (error) {}
existingProjectSpinner();
if (autoConfirm) {
return detectedProject || detectedProjectName;
}
let shouldLinkProject;
if (!detectedProject) {

View File

@@ -0,0 +1,229 @@
import { join, basename } from 'path';
import chalk from 'chalk';
import { remove } from 'fs-extra';
import { NowContext, ProjectLinkResult } from '../../types';
import { NowConfig } from '../dev/types';
import { Output } from '../output';
import {
getLinkedProject,
linkFolderToProject,
getVercelDirectory,
} from '../projects/link';
import createProject from '../projects/create-project';
import updateProject from '../projects/update-project';
import Client from '../client';
import handleError from '../handle-error';
import confirm from '../input/confirm';
import toHumanPath from '../humanize-path';
import { isDirectory } from '../config/global-path';
import selectOrg from '../input/select-org';
import inputProject from '../input/input-project';
import { validateRootDirectory } from '../validate-paths';
import { inputRootDirectory } from '../input/input-root-directory';
import editProjectSettings from '../input/edit-project-settings';
import stamp from '../output/stamp';
import { EmojiLabel } from '../emoji';
//@ts-expect-error
import createDeploy from '../deploy/create-deploy';
//@ts-expect-error
import Now from '../index';
export default async function setupAndLink(
ctx: NowContext,
output: Output,
path: string,
forceDelete: boolean,
autoConfirm: boolean,
successEmoji: EmojiLabel,
setupMsg: string
): Promise<ProjectLinkResult> {
const {
authConfig: { token },
config,
} = ctx;
const { apiUrl } = ctx;
const debug = output.isDebugEnabled();
const client = new Client({
apiUrl,
token,
currentTeam: config.currentTeam,
debug,
});
const isFile = !isDirectory(path);
if (isFile) {
output.error(`Expected directory but found file: ${path}`);
return { status: 'error', exitCode: 1 };
}
const link = await getLinkedProject(output, client, path);
const isTTY = process.stdout.isTTY;
const quiet = !isTTY;
let rootDirectory: string | null = null;
let newProjectName: string;
let org;
if (!forceDelete && link.status === 'linked') {
return link;
}
if (forceDelete) {
const vercelDir = getVercelDirectory(path);
remove(vercelDir);
}
const shouldStartSetup =
autoConfirm ||
(await confirm(
`${setupMsg} ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
if (!shouldStartSetup) {
output.print(`Aborted. Project not set up.\n`);
return { status: 'not_linked', org: null, project: null };
}
try {
org = await selectOrg(
output,
'Which scope should contain your project?',
client,
config.currentTeam,
autoConfirm
);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.prettyError(err);
return { status: 'error', exitCode: 1 };
}
throw err;
}
const detectedProjectName = basename(path);
const projectOrNewProjectName = await inputProject(
output,
client,
org,
detectedProjectName,
autoConfirm
);
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
} else {
const project = projectOrNewProjectName;
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug,
successEmoji
);
return { status: 'linked', org, project };
}
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
if (
rootDirectory &&
!(await validateRootDirectory(output, path, sourcePath, ''))
) {
return { status: 'error', exitCode: 1 };
}
let localConfig: NowConfig = {};
if (ctx.localConfig && !(ctx.localConfig instanceof Error)) {
localConfig = ctx.localConfig;
}
client.currentTeam = org.type === 'team' ? org.id : undefined;
const now = new Now({
apiUrl,
token,
debug,
currentTeam: client.currentTeam,
});
let deployment = null;
try {
const createArgs: any = {
name: newProjectName,
env: {},
build: { env: {} },
forceNew: undefined,
withCache: undefined,
quiet,
wantsPublic: localConfig.public,
isFile,
type: null,
nowConfig: localConfig,
regions: undefined,
meta: {},
deployStamp: stamp(),
target: undefined,
skipAutoDetectionConfirmation: false,
};
deployment = await createDeploy(
output,
now,
client.currentTeam || 'current user',
[sourcePath],
createArgs,
org,
!isFile,
path
);
if (
!deployment ||
!('code' in deployment) ||
deployment.code !== 'missing_project_settings'
) {
output.error('Failed to detect project settings. Please try again.');
if (output.isDebugEnabled()) {
console.log(deployment);
}
return { status: 'error', exitCode: 1 };
}
const { projectSettings, framework } = deployment;
if (rootDirectory) {
projectSettings.rootDirectory = rootDirectory;
}
const settings = await editProjectSettings(
output,
projectSettings,
framework,
autoConfirm
);
const project = await createProject(client, newProjectName);
await updateProject(client, project.id, settings);
Object.assign(project, settings);
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug,
successEmoji
);
return { status: 'linked', org, project };
} catch (err) {
handleError(err);
return { status: 'error', exitCode: 1 };
}
}

View File

@@ -27,7 +27,8 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
function warn(
str: string,
slug: string | null = null,
link: string | null = null
link: string | null = null,
action: string = 'Learn More'
) {
const prevTerm = process.env.TERM;
@@ -42,7 +43,7 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
boxen(
chalk.bold.yellow('WARN! ') +
str +
(details ? `\nMore details: ${renderLink(details)}` : ''),
(details ? `\n${action}: ${renderLink(details)}` : ''),
{
padding: {
top: 0,
@@ -67,7 +68,7 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
str: string,
slug?: string,
link?: string,
action = 'More details'
action = 'Learn More'
) {
print(`${chalk.red(`Error!`)} ${str}\n`);
const details = slug ? `https://err.sh/now/${slug}` : link;

View File

@@ -8,11 +8,11 @@ const metric = metrics();
export default function error(...input: string[] | [APIError]) {
let messages = input;
if (typeof input[0] === 'object') {
const { slug, message, link } = input[0];
const { slug, message, link, action = 'Learn More' } = input[0];
messages = [message];
const details = slug ? `https://err.sh/now/${slug}` : link;
if (details) {
messages.push(`${chalk.bold('More details')}: ${renderLink(details)}`);
messages.push(`${chalk.bold(action)}: ${renderLink(details)}`);
}
}

View File

@@ -0,0 +1,46 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { ProjectAliasTarget } from '../../types';
export async function addDomainToProject(
client: Client,
projectNameOrId: string,
domain: string
) {
const cancelWait = wait(
`Adding domain ${domain} to project ${chalk.bold(projectNameOrId)}`
);
try {
const response = await client.fetch<ProjectAliasTarget[]>(
`/projects/${encodeURIComponent(projectNameOrId)}/alias`,
{
method: 'POST',
body: JSON.stringify({
target: 'PRODUCTION',
domain,
}),
}
);
const aliasTarget: ProjectAliasTarget | undefined = response.find(
aliasTarget => aliasTarget.domain === domain
);
if (!aliasTarget) {
throw new Error(
`Unexpected error when adding the domain "${domain}" to project "${projectNameOrId}".`
);
}
return aliasTarget;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,13 @@
import Client from '../client';
import { Project } from '../../types';
export default async function createProject(
client: Client,
projectName: string
) {
const project = await client.fetch<Project>('/v1/projects', {
method: 'POST',
body: JSON.stringify({ name: projectName }),
});
return project;
}

View File

@@ -0,0 +1,46 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { Project } from '../../types';
import { URLSearchParams } from 'url';
export async function findProjectsForDomain(
client: Client,
domainName: string
): Promise<Project[] | Error> {
const cancelWait = wait(
`Searching project for domain ${chalk.bold(domainName)}`
);
try {
const limit = 50;
let result: Project[] = [];
const query = new URLSearchParams({
hasProductionDomains: '1',
limit: limit.toString(),
domain: domainName,
});
for (let i = 0; i < 1000; i++) {
const response = await client.fetch<Project[]>(`/v2/projects/?${query}`);
result.push(...response);
if (response.length !== limit) {
break;
}
const [latest] = response.sort((a, b) => b.updatedAt - a.updatedAt);
query.append('from', latest.updatedAt.toString());
}
return result;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,39 @@
import Client from '../client';
import wait from '../output/wait';
import { Project } from '../../types';
import { URLSearchParams } from 'url';
export async function getProjectsWithDomains(
client: Client
): Promise<Project[] | Error> {
const cancelWait = wait(`Fetching projects with domains`);
try {
const limit = 50;
let result: Project[] = [];
const query = new URLSearchParams({
hasProductionDomains: '1',
limit: limit.toString(),
});
for (let i = 0; i < 1000; i++) {
const response = await client.fetch<Project[]>(`/v2/projects/?${query}`);
result.push(...response);
const [latest] = response.sort((a, b) => b.updatedAt - a.updatedAt);
query.append('from', latest.updatedAt.toString());
if (response.length !== limit) break;
}
return result;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -8,13 +8,14 @@ import { ProjectNotFound } from '../errors-ts';
import getUser from '../get-user';
import getTeamById from '../get-team-by-id';
import { Output } from '../output';
import { Project } from '../../types';
import { Project, ProjectLinkResult } from '../../types';
import { Org, ProjectLink } from '../../types';
import chalk from 'chalk';
import { prependEmoji, emoji } from '../emoji';
import { prependEmoji, emoji, EmojiLabel } from '../emoji';
import AJV from 'ajv';
import { isDirectory } from '../config/global-path';
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
import outputCode from '../output/code';
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
@@ -73,7 +74,7 @@ async function getLinkFromDir(dir: string): Promise<ProjectLink | null> {
if (!ajv.validate(linkSchema, link)) {
throw new Error(
`Project settings are invalid. To link your project again, remove the ${dir} directory.`
`Project Settings are invalid. To link your project again, remove the ${dir} directory.`
);
}
@@ -87,7 +88,7 @@ async function getLinkFromDir(dir: string): Promise<ProjectLink | null> {
// link file can't be read
if (error.name === 'SyntaxError') {
throw new Error(
`Project settings could not be retrieved. To link your project again, remove the ${dir} directory.`
`Project Settings could not be retrieved. To link your project again, remove the ${dir} directory.`
);
}
@@ -111,11 +112,7 @@ export async function getLinkedProject(
output: Output,
client: Client,
path?: string
): Promise<
| { status: 'linked'; org: Org; project: Project }
| { status: 'not_linked'; org: null; project: null }
| { status: 'error'; exitCode: number }
> {
): Promise<ProjectLinkResult> {
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
const shouldUseEnv = Boolean(VERCEL_ORG_ID && VERCEL_PROJECT_ID);
@@ -141,13 +138,27 @@ export async function getLinkedProject(
}
const spinner = output.spinner('Retrieving project…', 1000);
let org: Org | null;
let project: Project | ProjectNotFound | null;
let org: Org | null = null;
let project: Project | ProjectNotFound | null = null;
try {
[org, project] = await Promise.all([
getOrgById(client, link.orgId),
getProjectByIdOrName(client, link.projectId, link.orgId),
]);
} catch (err) {
if (err?.status === 403) {
spinner();
throw new NowBuildError({
message: `Could not retrieve Project Settings. To link your Project, remove the ${outputCode(
VERCEL_DIR
)} directory and deploy again.`,
code: 'PROJECT_UNAUTHORIZED',
link: 'https://vercel.link/cannot-load-project-settings',
});
}
// Not a special case 403, we should still throw it
throw err;
} finally {
spinner();
}
@@ -181,7 +192,8 @@ export async function linkFolderToProject(
path: string,
projectLink: ProjectLink,
projectName: string,
orgSlug: string
orgSlug: string,
successEmoji: EmojiLabel = 'link'
) {
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
@@ -251,7 +263,7 @@ export async function linkFolderToProject(
)} (created ${VERCEL_DIR}${
isGitIgnoreUpdated ? ' and added it to .gitignore' : ''
})`,
emoji('link')
emoji(successEmoji)
) + '\n'
);
}

View File

@@ -0,0 +1,34 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { ProjectAliasTarget } from '../../types';
export async function removeDomainFromProject(
client: Client,
projectNameOrId: string,
domain: string
) {
const cancelWait = wait(
`Removing domain ${domain} from project ${chalk.bold(projectNameOrId)}`
);
try {
const response = await client.fetch<ProjectAliasTarget[]>(
`/projects/${encodeURIComponent(
projectNameOrId
)}/alias?domain=${encodeURIComponent(domain)}`,
{
method: 'DELETE',
}
);
return response;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,24 @@
import Client from '../client';
import { ProjectSettings } from '../../types';
interface ProjectSettingsResponse extends ProjectSettings {
id: string;
name: string;
updatedAt: number;
createdAt: number;
}
export default async function updateProject(
client: Client,
prjNameOrId: string,
settings: ProjectSettings
) {
const res = await client.fetch<ProjectSettingsResponse>(
`/v2/projects/${encodeURIComponent(prjNameOrId)}`,
{
method: 'PATCH',
body: JSON.stringify(settings),
}
);
return res;
}

View File

@@ -6,10 +6,9 @@ export default async function responseError(
fallbackMessage = null,
parsedBody = {}
) {
let message;
let bodyError;
if (res.status >= 400 && res.status < 500) {
if (!res.ok) {
let body;
try {
@@ -20,12 +19,9 @@ export default async function responseError(
// Some APIs wrongly return `err` instead of `error`
bodyError = body.error || body.err || body;
message = bodyError.message;
}
if (message == null) {
message = fallbackMessage === null ? 'Response Error' : fallbackMessage;
}
const msg = bodyError?.message || fallbackMessage || 'Response Error';
return new APIError(message, res, bodyError);
return new APIError(msg, res, bodyError);
}

View File

@@ -3,7 +3,11 @@ import { devRouter } from '../src/util/dev/router';
test('[dev-router] 301 redirection', async t => {
const routesConfig = [
{ src: '/redirect', status: 301, headers: { Location: 'https://vercel.com' } },
{
src: '/redirect',
status: 301,
headers: { Location: 'https://vercel.com' },
},
];
const result = await devRouter('/redirect', 'GET', routesConfig);

View File

@@ -71,7 +71,7 @@ function testFixture(name, fn) {
}
function validateResponseHeaders(t, res, podId = null) {
t.is(res.headers.get('server'), 'now');
t.is(res.headers.get('server'), 'Vercel');
t.truthy(res.headers.get('cache-control').length > 0);
t.truthy(
/^dev1::(dev1::)?[0-9a-z]{5}-[1-9][0-9]+-[a-f0-9]{12}$/.test(

View File

@@ -1,18 +1,17 @@
import test from 'ava';
import { validateConfig } from '../src/util/dev/validate';
test('[dev-validate] should not error with empty config', async t => {
test('[dev-validate] should not error with empty config', async (t) => {
const config = {};
const error = validateConfig(config);
t.deepEqual(error, null);
});
test('[dev-validate] should not error with complete config', async t => {
test('[dev-validate] should not error with complete config', async (t) => {
const config = {
version: 2,
public: true,
regions: ['sfo1', 'iad1'],
builds: [{ src: 'package.json', use: '@vercel/next' }],
cleanUrls: true,
headers: [{ source: '/', headers: [{ key: 'x-id', value: '123' }] }],
rewrites: [{ source: '/help', destination: '/support' }],
@@ -24,7 +23,7 @@ test('[dev-validate] should not error with complete config', async t => {
t.deepEqual(error, null);
});
test('[dev-validate] should not error with builds and routes', async t => {
test('[dev-validate] should not error with builds and routes', async (t) => {
const config = {
builds: [{ src: 'api/index.js', use: '@vercel/node' }],
routes: [{ src: '/(.*)', dest: '/api/index.js' }],
@@ -33,7 +32,7 @@ test('[dev-validate] should not error with builds and routes', async t => {
t.deepEqual(error, null);
});
test('[dev-validate] should error with invalid rewrites due to additional property and offer suggestion', async t => {
test('[dev-validate] should error with invalid rewrites due to additional property and offer suggestion', async (t) => {
const config = {
rewrites: [{ src: '/(.*)', dest: '/api/index.js' }],
};
@@ -48,7 +47,7 @@ test('[dev-validate] should error with invalid rewrites due to additional proper
);
});
test('[dev-validate] should error with invalid routes due to additional property and offer suggestion', async t => {
test('[dev-validate] should error with invalid routes due to additional property and offer suggestion', async (t) => {
const config = {
routes: [{ source: '/(.*)', destination: '/api/index.js' }],
};
@@ -63,7 +62,7 @@ test('[dev-validate] should error with invalid routes due to additional property
);
});
test('[dev-validate] should error with invalid routes array type', async t => {
test('[dev-validate] should error with invalid routes array type', async (t) => {
const config = {
routes: { src: '/(.*)', dest: '/api/index.js' },
};
@@ -75,7 +74,7 @@ test('[dev-validate] should error with invalid routes array type', async t => {
);
});
test('[dev-validate] should error with invalid redirects array object', async t => {
test('[dev-validate] should error with invalid redirects array object', async (t) => {
const config = {
redirects: [
{
@@ -94,7 +93,7 @@ test('[dev-validate] should error with invalid redirects array object', async t
);
});
test('[dev-validate] should error with invalid redirects.permanent poperty', async t => {
test('[dev-validate] should error with invalid redirects.permanent poperty', async (t) => {
const config = {
redirects: [{ source: '/', destination: '/go', permanent: 'yes' }],
};
@@ -109,7 +108,7 @@ test('[dev-validate] should error with invalid redirects.permanent poperty', asy
);
});
test('[dev-validate] should error with invalid cleanUrls type', async t => {
test('[dev-validate] should error with invalid cleanUrls type', async (t) => {
const config = {
cleanUrls: 'true',
};
@@ -124,7 +123,7 @@ test('[dev-validate] should error with invalid cleanUrls type', async t => {
);
});
test('[dev-validate] should error with invalid trailingSlash type', async t => {
test('[dev-validate] should error with invalid trailingSlash type', async (t) => {
const config = {
trailingSlash: [true],
};
@@ -139,7 +138,7 @@ test('[dev-validate] should error with invalid trailingSlash type', async t => {
);
});
test('[dev-validate] should error with invalid headers property', async t => {
test('[dev-validate] should error with invalid headers property', async (t) => {
const config = {
headers: [{ 'Content-Type': 'text/html' }],
};
@@ -154,7 +153,7 @@ test('[dev-validate] should error with invalid headers property', async t => {
);
});
test('[dev-validate] should error with invalid headers.source type', async t => {
test('[dev-validate] should error with invalid headers.source type', async (t) => {
const config = {
headers: [{ source: [{ 'Content-Type': 'text/html' }] }],
};
@@ -169,7 +168,7 @@ test('[dev-validate] should error with invalid headers.source type', async t =>
);
});
test('[dev-validate] should error with invalid headers additional property', async t => {
test('[dev-validate] should error with invalid headers additional property', async (t) => {
const config = {
headers: [{ source: '/', stuff: [{ 'Content-Type': 'text/html' }] }],
};
@@ -184,7 +183,7 @@ test('[dev-validate] should error with invalid headers additional property', asy
);
});
test('[dev-validate] should error with invalid headers wrong nested headers type', async t => {
test('[dev-validate] should error with invalid headers wrong nested headers type', async (t) => {
const config = {
headers: [{ source: '/', headers: [{ 'Content-Type': 'text/html' }] }],
};
@@ -199,7 +198,7 @@ test('[dev-validate] should error with invalid headers wrong nested headers type
);
});
test('[dev-validate] should error with invalid headers wrong nested headers additional property', async t => {
test('[dev-validate] should error with invalid headers wrong nested headers additional property', async (t) => {
const config = {
headers: [
{ source: '/', headers: [{ key: 'Content-Type', val: 'text/html' }] },
@@ -216,7 +215,7 @@ test('[dev-validate] should error with invalid headers wrong nested headers addi
);
});
test('[dev-validate] should error with too many redirects', async t => {
test('[dev-validate] should error with too many redirects', async (t) => {
const config = {
redirects: Array.from({ length: 5000 }).map((_, i) => ({
source: `/${i}`,
@@ -234,7 +233,7 @@ test('[dev-validate] should error with too many redirects', async t => {
);
});
test('[dev-validate] should error with too many nested headers', async t => {
test('[dev-validate] should error with too many nested headers', async (t) => {
const config = {
headers: [
{
@@ -260,3 +259,25 @@ test('[dev-validate] should error with too many nested headers', async t => {
'https://vercel.com/docs/configuration#project/headers'
);
});
test('[dev-validate] should error with "functions" and "builds"', async (t) => {
const config = {
builds: [
{
src: 'index.html',
use: '@vercel/static',
},
],
functions: {
'api/test.js': {
memory: 1024,
},
},
};
const error = validateConfig(config);
t.deepEqual(
error.message,
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.'
);
t.deepEqual(error.link, 'https://vercel.link/functions-and-builds');
});

View File

@@ -0,0 +1 @@
proof goes here

View File

@@ -1,7 +1,7 @@
{
"functions": {
"api/user.sh": {
"runtime": "vercel-bash@3.0.7"
"runtime": "vercel-bash@3.0.8"
}
}
}

View File

@@ -1 +1 @@
{ "env": { "FOO": "bar" } }
{"env":{"FOO":"bar"}}

View File

@@ -1 +1 @@
{ "builds": [{ "src": "index.js", "use": "@vercel/node@canary" }] }
{"builds":[{"src":"index.js","use":"@vercel/node@canary"}]}

View File

@@ -0,0 +1,3 @@
# Created by Vercel CLI
VERCEL_REGION=""
VERCEL_URL=""

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