Compare commits

..

45 Commits

Author SHA1 Message Date
Nathan Rajlich
63cc9009c8 Publish Stable
- @vercel/frameworks@0.0.15
 - @vercel/build-utils@2.4.0
 - @vercel/cgi@1.0.6
 - vercel@19.1.0
 - @vercel/client@8.1.0
 - @vercel/go@1.1.2
 - @vercel/next@2.6.6
 - @vercel/node-bridge@1.3.1
 - @vercel/node@1.7.0
 - @vercel/python@1.2.2
 - @vercel/routing-utils@1.8.3
 - @vercel/ruby@1.2.2
 - @vercel/static-build@0.17.2
2020-06-11 16:30:31 -07:00
Nathan Rajlich
ccf6e3c432 Publish Canary
- vercel@19.0.2-canary.17
 - @vercel/client@8.0.2-canary.4
2020-06-11 15:17:05 -07:00
Steven
8d015e3138 [client] Fix .vercelignore override default ignore list (#4627)
- Fixes #3747 
- Related to #4325
- Related to [tweet](https://twitter.com/Rich_Harris/status/1270871878018699264) from @Rich-Harris
- Docs updated in https://github.com/vercel/docs/pull/1909

There was an issue that was likely introduced when we started using `@vercel/client`: the [default ignore list](https://vercel.com/docs/v2/build-step#ignored-files-and-folders) could not be overridden. This is because the default `ignores` array was always used instead of the combined `default ignore + user ignore`. The solution was to utilize [`recursive-readdir` ignore function](https://www.npmjs.com/package/recursive-readdir#usage) along with [`ig.ignores` function](https://www.npmjs.com/package/ignore#usage) so that the combined result is used for ignoring files while walking the file tree.

This ensures that the fix from PR #4325 is still effective and also fixes the longstanding bug from Issue #3747.
2020-06-11 18:15:16 -04:00
Nathan Rajlich
42f2fa1a20 [cli] Invalidate the configuration if vercel.json is deleted (#4629)
Before this, if the `vercel.json` file gets deleted while the dev server
is running, then it would still act like the file exists since it would
use the cached version.

Now it properly invalidates to an empty configuration if the
`vercel.json` file does not exist.
2020-06-11 22:06:44 +00:00
Steven
8397aac0e3 [github] Update codeowners (#4630)
* Update codeowners

* Make TooTallNate taller
2020-06-11 16:59:18 -04:00
Nathan Rajlich
7bcdc144eb [cli] Update "npm-install-error" link to "npm-install-failed-dev" (#4628)
Depends on https://github.com/vercel/docs/pull/1908.
2020-06-11 19:08:48 +00:00
Nathan Rajlich
624da9170d Publish Canary
- @vercel/frameworks@0.0.15-canary.5
 - @vercel/build-utils@2.3.2-canary.6
 - vercel@19.0.2-canary.16
 - @vercel/client@8.0.2-canary.3
 - @vercel/node@1.6.2-canary.6
2020-06-10 14:27:18 -07:00
Nathan Rajlich
fb5b013a03 [node] Force module: "commonjs" in startDevServer() (#4621)
This is a follow up to #4514 to handle the case where there is no
`tsconfig.json` closer to the entrypoint. This is likely the case when
`.js` files with ES Modules syntax are being used instead of `.ts`.
2020-06-10 14:26:28 -07:00
Mark Glagola
0a4bb53a58 [cli] Handle aliasWarning and bump to v13/now/deployments (#4605)
* Handle aliasWarning

* Pick best production project domain

* Fix currentTeam assignment

* Adds `https` url check to cli integration tests
2020-06-10 16:00:50 -05:00
Nathan Rajlich
2fbd9c78e3 [cli] Better errors for conflicting configuration files (#4612)
* [cli] Better errors for conflicting configuration files

Renders the link https://vercel.link/combining-old-and-new-config
for all conflicting config errors.

* Fix unit test

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-06-10 12:38:02 -07:00
Nathan Rajlich
1ed2b7a57d [cli] Update @vercel/next to v2.6.5 (#4611)
It got out of sync somehow in 55c60d30e6.
2020-06-10 03:37:58 +00:00
Nathan Rajlich
88cd9ca3c3 [cli] Invalidate build match for changes to config in builds (#4603)
* Fixes changing a `config` value in the `builds` array (such as `helpers: false` for `@vercel/node`) and having it be reflected in `vc dev` without restarting the dev server.
* Re-validates the env vars configuration when a `.env` file changes or the `env` object is changed in `vercel.json` (same for the builds equivalents).
* Ensures that the `NODEJS_HELPERS` build env var is being properly checked in `startDevServer()`.
* Regenerated the `yarn.lock` file because yarn was erroring when trying to add the `fast-deep-equal` dependency.
2020-06-09 19:56:55 +00:00
JJ Kasper
ecea2ca4a3 Publish Stable
- @vercel/next@2.6.5
2020-06-09 13:02:11 -05:00
JJ Kasper
3f132bc15b Publish Canary
- @vercel/build-utils@2.3.2-canary.5
 - vercel@19.0.2-canary.15
 - @vercel/next@2.6.5-canary.0
 - @vercel/routing-utils@1.8.3-canary.6
2020-06-09 11:47:51 -05:00
JJ Kasper
61d95094c0 [next] Re-disable shared lambdas by default (#4609)
As noticed, `now.json` and `vercel.json` files aren't available during the build currently which makes feature detecting and opting out when `routes` are used not work currently so this re-disables the shared lambdas optimization by default while we investigate detecting this
2020-06-09 16:24:21 +00:00
Steven
f7c47975e3 [tests] Add log for error response (#4601) 2020-06-08 17:51:31 -07:00
Nathan Rajlich
7c96f9f9a5 [cli] Show npm output when failing to install Runtimes in vc dev (#4598)
If `npm install` doesn't exit with 0, then log the output to the
termial.

This was already happening with `--debug` due to `stdio: "inherit"`
when debug is enabled, so this change is really only relevant for the
non-debug case.

### Before

```
$ vc dev
Vercel CLI 19.0.2-canary.13 dev (beta) — https://vercel.com/feedback
Error! Command failed with exit code 1: npm install --save-exact --no-package-lock --no-audit --no-progress @now/build-utils@canary @vercel/build-utils@canary now-php@0.0.0
```

### After

Bad dependency:

<img width="641" alt="Screen Shot 2020-06-08 at 12 22 00 PM" src="https://user-images.githubusercontent.com/71256/84071523-b07c4c00-a982-11ea-9200-f18498ae917c.png">

No `npm` installed:

<img width="672" alt="Screen Shot 2020-06-08 at 12 21 16 PM" src="https://user-images.githubusercontent.com/71256/84071456-95114100-a982-11ea-87cc-05ed7fb2cb80.png">
2020-06-08 22:36:47 +00:00
Nathan Rajlich
a5c805b6eb [cli] Use getArgs() instead of mri to parse arguments in vc projects (#4570)
* Ensures that `vc --debug projects ls` properly prints the Projects listing, rather than the usage help info.
 * `vc projects` (without a subcommand) shows the Projects listing (this is consistent with i.e. `vc domains`).
 * Returns with exit code `2` when the usage help info is printed (standard Unix convention).
2020-06-08 21:05:18 +00:00
JJ Kasper
ff2a22023d Publish Stable
- @vercel/next@2.6.4
2020-06-08 14:56:04 -05:00
JJ Kasper
c6efc028aa Publish Canary
- vercel@19.0.2-canary.14
 - @vercel/next@2.6.4-canary.0
2020-06-08 14:48:59 -05:00
JJ Kasper
96565da1cf [next] Add shared lambdas opt-out for functions config (#4596)
As discussed this adds opting out of the shared lambdas optimization when a user adds a functions config in `now.json` or `vercel.json` since this could potentially be a breaking change. We plan to add new handling to still allow customizing this config for the combined lambdas that are created
2020-06-08 19:40:48 +00:00
Steven
afb5e7fc85 [cli] Update tests for 404.html (#4597)
Now that we updated api-deployments, we can remove this TODO and run tests agains `vercel dev` and real deployments.
2020-06-08 18:42:45 +00:00
Nathan Rajlich
34cc987be8 [cli] Make proxyPass() return a 502 if the proxying fails (#4586)
This happens, for example, with a `startDevServer()` process that
crashes (i.e. a syntax error in a Node.js API endpoint) before
responding to the HTTP request.
2020-06-08 18:02:05 +00:00
JJ Kasper
55c60d30e6 Publish Stable
- @vercel/next@2.6.3
2020-06-08 09:54:44 -05:00
Nathan Rajlich
eb993d47ac [cli] Update 502 error template for vc dev (#4583)
* Fix the "Developer Documentation" link.
 * Remove the "If you're a visitor" section - doesn't make sense for `vc dev` since there are no "visitors".
 * Don't link to `_logs` since it's not supported in `vc dev`. Instead direct the user to look at their terminal window to see error logs.
 * Link to new GH issue for non-app error 502 (I don't think this code path ever happens in `vc dev`, but might as well make it correct in case we do in the future).

<img width="1077" alt="Screen Shot 2020-06-05 at 4 15 16 PM" src="https://user-images.githubusercontent.com/71256/83929319-c7832a00-a747-11ea-9cae-b0adac97dfa5.png">
2020-06-05 23:20:33 +00:00
JJ Kasper
d2184628d1 Publish Canary
- vercel@19.0.2-canary.13
 - @vercel/go@1.1.2-canary.2
 - @vercel/next@2.6.3-canary.6
2020-06-05 14:52:45 -05:00
JJ Kasper
65f0cc6797 De-experimentalize shared lambdas optimization by default (#4519)
As discussed this de-experimentalizes the shared lambdas optimization now that we have tested it, it also bails out of the optimization when a `now.json` or `vercel.json` is detected that contains legacy routes
2020-06-05 19:51:21 +00:00
Nathan Rajlich
c628c7b58e [go] Fix typo in go-bridge import (#4578)
Introduced in 56c8af51b.
2020-06-05 13:09:26 +00:00
Steven
4e005274f9 Publish Canary
- @vercel/build-utils@2.3.2-canary.4
 - vercel@19.0.2-canary.12
2020-06-04 18:43:42 -04:00
Steven
482373f711 [cli][build-utils] Add support for custom 404.html for zero config deployments (#4563)
Next.js already has support for [customizing the 404 page](https://nextjs.org/docs/advanced-features/custom-error-page#customizing-the-404-page), but many other frameworks do not or they expect a 404.html in the output directory.

This PR adds support for rendering the a `404.html` page for all zero config deployments.

- Implements ch337
- Related to #3491
2020-06-04 22:36:41 +00:00
Nathan Rajlich
c80bb37e8d Publish Canary
- @vercel/frameworks@0.0.15-canary.4
 - @vercel/build-utils@2.3.2-canary.3
 - vercel@19.0.2-canary.11
 - @vercel/client@8.0.2-canary.2
 - @vercel/go@1.1.2-canary.1
 - @vercel/next@2.6.3-canary.5
 - @vercel/node-bridge@1.3.1-canary.2
 - @vercel/node@1.6.2-canary.5
 - @vercel/python@1.2.2-canary.2
 - @vercel/routing-utils@1.8.3-canary.5
 - @vercel/ruby@1.2.2-canary.1
 - @vercel/static-build@0.17.2-canary.1
2020-06-03 13:58:37 -07:00
Nathan Rajlich
a7acd92ffd [cli][client] Throw an error if both vercel.json and now.json exist (#4316)
* [client] Throw an error if both `vercel.json` and `now.json` exist

* Update packages/now-client/src/create-deployment.ts

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

* Check in CLI as well

* Add integration test

* Add logic to `getLocalPathConfig()` as well

* Fix import path

Co-authored-by: Steven <steven@ceriously.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-06-03 13:51:50 -07:00
Max Leiter
035720ca82 [cli] Prioritize index.html over other files named index (#4564)
Mirrors production more closely. Initially reported [on spectrum](https://spectrum.chat/zeit/now/statically-served-default-document-is-the-first-index-file-not-index-html~0ad66f8a-a139-4a0e-94d9-7e5ceb0fa043)
2020-06-03 19:56:36 +00:00
Nathan Rajlich
a6ae923a7a [all] Update TypeScript to v3.9.3 and regenerate yarn.lock file (#4565)
To fix GitHub Dependabot security alerts. See: https://github.com/vercel/vercel/network/alerts

<img width="835" alt="Screen Shot 2020-06-03 at 1 36 50 AM" src="https://user-images.githubusercontent.com/71256/83614929-b7e2c600-a53a-11ea-80b6-190e350283ac.png">

Also updates "typescript" to `3.9.3` for all packages, which is necessary because a downstream dependency is using syntax that requires a newer version of TypeScript.
2020-06-03 09:57:16 -07:00
Nathan Rajlich
83c0711d6e [node] Pass in the tsconfig.json file to startDevServer() (#4514)
So that if a `tsconfig.json` exists closer to the entrypoint file,
then that config file will be correctly used (rather than say, the
root-level `tsconfig.json` file, which may be specific to the frontend
configuration in Next.js for example).
2020-06-03 00:56:44 +00:00
Nathan Rajlich
231f18d56b [cli] Remove unnecessary build outputs (#4547)
Upon investigation into the `dist` dir, it appears that `ncc` is bundling some assets that don't need to be there. This change is a quick band-aid fix to remove those assets, without addressing the underlying cause of _why_ they're being bundled, which requires further investigation.

Overall about 1mb of disk space is saved.
2020-06-02 13:43:34 +00:00
Steven
9d73091d8c Publish Canary
- vercel@19.0.2-canary.10
 - @vercel/routing-utils@1.8.3-canary.4
2020-06-01 18:07:23 -04:00
Steven
0ca3189f79 [routing-utils] Fix headers with content-security-policy URL (#4550)
Fixes a case when the header value contains a URL which was mistaken for a named segment.

https://sentry.io/organizations/zeithq/issues/1702692084/?project=1351065

The regression was introduced in PR #4484 where unnamed segments were implemented for `redirects` and `rewrites` but not handled properly in `headers`.
2020-06-01 22:06:37 +00:00
Nathan Rajlich
9ff5bb9cb3 [cli] Throw an error if both .vercel and .now dirs exist (#4543) 2020-06-01 16:33:19 +00:00
Steven
45d05a603b Publish Canary
- @vercel/routing-utils@1.8.3-canary.3
2020-06-01 10:59:25 -04:00
Steven
6ef3b12fde [build-utils] Revert type of routing errors (#4549)
In PR #4498, the type of the routing error was changed from first error and then the remaining errors. This PR changes the type back such that `error.errors` returns all errors. This will avoid any breaking change.
2020-06-01 14:58:27 +00:00
Steven
b16f94098a Publish Canary
- vercel@19.0.2-canary.9
 - @vercel/routing-utils@1.8.3-canary.2
2020-06-01 08:29:32 -04:00
Steven
be315bebcf [routing-utils] Fix error when segment in query string (#4532)
This PR fixes an issue where the `destination` property defines a query string with a path segment that is not defined in the `source` property.
2020-05-30 00:07:05 +00:00
Steven
5608a4c42c [routing-utils] Improve error messages (#4498)
This PR improves the way we handle routing errors in a few ways:

- The error response is a single error (the first) instead of an array of errors when mixing routing properties
- The error message says which route index has the error
- The error includes `link` and `action` properties to match our API
- The error message for mixed routes with new routing properties has been updated per [ch341](https://app.clubhouse.io/vercel/story/341)

Related to #3491
2020-05-29 22:15:39 +00:00
Steven
66458fe3e0 [cli] Update zeit.ink to vercel.link shortener (#4531)
- Update `zeit.ink` to `vercel.link` shortener
- Support for `output.error()` action parameter
2020-05-29 16:46:35 -04:00
153 changed files with 14517 additions and 2870 deletions

28
.github/CODEOWNERS vendored
View File

@@ -1,27 +1,27 @@
# Documentation # Documentation
# https://help.github.com/en/articles/about-code-owners # https://help.github.com/en/articles/about-code-owners
* @tootallnate * @TooTallNate
/.github/workflows @AndyBitz @styfle /.github/workflows @AndyBitz @styfle
/packages/frameworks @AndyBitz /packages/frameworks @AndyBitz
/packages/now-cli/src/commands/dev/ @tootallnate @styfle @AndyBitz /packages/now-cli/src/commands/dev @TooTallNate @styfle @AndyBitz
/packages/now-cli/src/util/dev/ @tootallnate @styfle @AndyBitz /packages/now-cli/src/util/dev @TooTallNate @styfle @AndyBitz
/packages/now-cli/src/commands/domains/ @javivelasco @mglagola @anatrajkovska /packages/now-cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
/packages/now-cli/src/commands/certs/ @javivelasco @mglagola @anatrajkovska /packages/now-cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
/packages/now-cli/src/commands/env @styfle @lucleray /packages/now-cli/src/commands/env @styfle @lucleray
/packages/now-client @rdev /packages/now-client @rdev @styfle @TooTallNate
/packages/now-build-utils @styfle @AndyBitz /packages/now-build-utils @styfle @AndyBitz @TooTallNate
/packages/now-node @styfle @tootallnate @lucleray /packages/now-node @styfle @TooTallNate @lucleray
/packages/now-node-bridge @styfle @tootallnate @lucleray /packages/now-node-bridge @styfle @TooTallNate @lucleray
/packages/now-next @Timer @ijjk /packages/now-next @Timer @ijjk
/packages/now-go @styfle @sophearak /packages/now-go @styfle @TooTallNate
/packages/now-python @styfle @sophearak /packages/now-python @styfle @TooTallNate
/packages/now-ruby @styfle @coetry @nathancahill /packages/now-ruby @styfle @coetry @TooTallNate
/packages/now-static-build @styfle @AndyBitz /packages/now-static-build @styfle @AndyBitz
/packages/now-routing-utils @styfle @dav-is @ijjk /packages/now-routing-utils @styfle @dav-is @ijjk
/examples @mcsdevv @timothyis /examples @mcsdevv @timothyis
/examples/create-react-app @Timer /examples/create-react-app @Timer
/examples/nextjs @timneutkens /examples/nextjs @timneutkens @Timer
/examples/hugo @mcsdevv @timothyis @styfle /examples/hugo @mcsdevv @timothyis @styfle
/examples/jekyll @mcsdevv @timothyis @sarupbanskota /examples/jekyll @mcsdevv @timothyis @styfle
/examples/zola @mcsdevv @timothyis @styfle /examples/zola @mcsdevv @timothyis @styfle

View File

@@ -0,0 +1,14 @@
# `@vercel/next` Functions Config Optimized Lambdas Opt-out
#### Why This Warning Occurred
`@vercel/next` by default now bundles pages into optimized functions, minimizing bootup time and increasing overall application throughput.
When the `functions` config is added in `now.json` or `vercel.json`, it causes conflicts with this optimization, so it is opted-out.
#### Possible Ways to Fix It
Remove the `functions` config from your `now.json` or `vercel.json` to take advantage of this optimization.
### Useful Links
- [Functions Config Documentation](https://vercel.com/docs/configuration?query=functions#project/functions)

View File

@@ -0,0 +1,16 @@
# `@vercel/next` Legacy Routes Optimized Lambdas Opt-out
#### Why This Warning Occurred
`@vercel/next` by default now bundles pages into optimized functions, minimizing bootup time and increasing overall application throughput.
When legacy `routes` are added in `now.json` or `vercel.json`, they cause conflicts with this optimization, so it is opted-out.
#### Possible Ways to Fix It
Migrate from using legacy `routes` to the new `rewrites`, `redirects`, and `headers` configurations in your `now.json` or `vercel.json` file or leverage them directly in your `next.config.js` with the built-in [custom routes support](https://github.com/zeit/next.js/issues/9081)
### Useful Links
- [Rewrites Documentation](https://vercel.com/docs/configuration?query=rewrites#project/rewrites)
- [Redirects Documentation](https://vercel.com/docs/configuration?query=rewrites#project/redirects)
- [Headers Documentation](https://vercel.com/docs/configuration?query=rewrites#project/headers)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/frameworks", "name": "@vercel/frameworks",
"version": "0.0.15-canary.3", "version": "0.0.15",
"main": "frameworks.json", "main": "frameworks.json",
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
@@ -9,9 +9,9 @@
"devDependencies": { "devDependencies": {
"@types/jest": "24.0.22", "@types/jest": "24.0.22",
"@types/node": "12.0.4", "@types/node": "12.0.4",
"ajv": "6.10.2", "ajv": "6.12.2",
"jest": "24.9.0", "jest": "24.9.0",
"ts-jest": "24.1.0", "ts-jest": "24.1.0",
"typescript": "3.5.2" "typescript": "3.9.3"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/build-utils", "name": "@vercel/build-utils",
"version": "2.3.2-canary.2", "version": "2.4.0",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",
@@ -45,7 +45,7 @@
"node-fetch": "2.2.0", "node-fetch": "2.2.0",
"semver": "6.1.1", "semver": "6.1.1",
"ts-jest": "24.1.0", "ts-jest": "24.1.0",
"typescript": "3.5.2", "typescript": "3.9.3",
"yazl": "2.4.3" "yazl": "2.4.3"
} }
} }

View File

@@ -82,6 +82,7 @@ export async function detectBuilders(
defaultRoutes: Route[] | null; defaultRoutes: Route[] | null;
redirectRoutes: Route[] | null; redirectRoutes: Route[] | null;
rewriteRoutes: Route[] | null; rewriteRoutes: Route[] | null;
errorRoutes: Route[] | null;
}> { }> {
const errors: ErrorResponse[] = []; const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = []; const warnings: ErrorResponse[] = [];
@@ -99,6 +100,7 @@ export async function detectBuilders(
defaultRoutes: null, defaultRoutes: null,
redirectRoutes: null, redirectRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null,
}; };
} }
@@ -154,6 +156,7 @@ export async function detectBuilders(
defaultRoutes: null, defaultRoutes: null,
redirectRoutes: null, redirectRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null,
}; };
} }
@@ -231,6 +234,7 @@ export async function detectBuilders(
redirectRoutes: null, redirectRoutes: null,
defaultRoutes: null, defaultRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null,
}; };
} }
@@ -272,6 +276,7 @@ export async function detectBuilders(
redirectRoutes: null, redirectRoutes: null,
defaultRoutes: null, defaultRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null,
}; };
} }
@@ -309,6 +314,7 @@ export async function detectBuilders(
redirectRoutes: routesResult.redirectRoutes, redirectRoutes: routesResult.redirectRoutes,
defaultRoutes: routesResult.defaultRoutes, defaultRoutes: routesResult.defaultRoutes,
rewriteRoutes: routesResult.rewriteRoutes, rewriteRoutes: routesResult.rewriteRoutes,
errorRoutes: routesResult.errorRoutes,
}; };
} }
@@ -898,10 +904,17 @@ function getRouteResult(
defaultRoutes: Route[]; defaultRoutes: Route[];
redirectRoutes: Route[]; redirectRoutes: Route[];
rewriteRoutes: Route[]; rewriteRoutes: Route[];
errorRoutes: Route[];
} { } {
const defaultRoutes: Route[] = []; const defaultRoutes: Route[] = [];
const redirectRoutes: Route[] = []; const redirectRoutes: Route[] = [];
const rewriteRoutes: Route[] = []; const rewriteRoutes: Route[] = [];
const errorRoutes: Route[] = [];
const isNextjs =
frontendBuilder &&
((frontendBuilder.use && frontendBuilder.use.startsWith('@vercel/next')) ||
(frontendBuilder.config &&
frontendBuilder.config.framework === 'nextjs'));
if (apiRoutes && apiRoutes.length > 0) { if (apiRoutes && apiRoutes.length > 0) {
if (options.featHandleMiss) { if (options.featHandleMiss) {
@@ -968,10 +981,21 @@ function getRouteResult(
}); });
} }
if (options.featHandleMiss && !isNextjs) {
// Exclude Next.js to avoid overriding custom error page
// https://nextjs.org/docs/advanced-features/custom-error-page
errorRoutes.push({
status: 404,
src: '^/(?!.*api).*$',
dest: options.cleanUrls ? '/404' : '/404.html',
});
}
return { return {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
}; };
} }

View File

@@ -7,11 +7,13 @@ export class NowBuildError extends Error {
public hideStackTrace = true; public hideStackTrace = true;
public code: string; public code: string;
public link?: string; public link?: string;
public action?: string;
constructor({ message, code, link }: Props) { constructor({ message, code, link, action }: Props) {
super(message); super(message);
this.code = code; this.code = code;
this.link = link; this.link = link;
this.action = action;
} }
} }
@@ -31,4 +33,8 @@ interface Props {
* link to more information about this error. * link to more information about this error.
*/ */
link?: string; link?: string;
/**
* Optional "action" to display before the `link`, such as "More details".
*/
action?: string;
} }

View File

@@ -27,6 +27,7 @@ import {
getLatestNodeVersion, getLatestNodeVersion,
getDiscontinuedNodeVersions, getDiscontinuedNodeVersions,
} from './fs/node-version'; } from './fs/node-version';
import { NowBuildError } from './errors';
import streamToBuffer from './fs/stream-to-buffer'; import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve'; import shouldServe from './should-serve';
import debug from './debug'; import debug from './debug';
@@ -111,9 +112,11 @@ export const getPlatformEnv = (name: string): string | undefined => {
const n = process.env[nName]; const n = process.env[nName];
if (typeof v === 'string') { if (typeof v === 'string') {
if (typeof n === 'string') { if (typeof n === 'string') {
throw new Error( throw new NowBuildError({
`Both "${vName}" and "${nName}" env vars are defined. Please only define the "${vName}" env var` code: 'CONFLICTING_ENV_VAR_NAMES',
); message: `Both "${vName}" and "${nName}" env vars are defined. Please only define the "${vName}" env var.`,
link: 'https://vercel.link/combining-old-and-new-config',
});
} }
return v; return v;
} }

View File

@@ -531,6 +531,7 @@ describe('Test `detectBuilders`', () => {
const files = ['api/user.php']; const files = ['api/user.php'];
// @ts-ignore // @ts-ignore
const { errors } = await detectBuilders(files, null, { const { errors } = await detectBuilders(files, null, {
// @ts-ignore
functions, functions,
}); });
@@ -867,12 +868,14 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, pkg, { featHandleMiss }); } = await detectBuilders(files, pkg, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/next'); expect(builders![0].use).toBe('@vercel/next');
expect(errors).toBe(null); expect(errors).toBe(null);
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([]); expect(rewriteRoutes).toStrictEqual([]);
expect(errorRoutes).toStrictEqual([]);
}); });
it('package.json + no build + next', async () => { it('package.json + no build + next', async () => {
@@ -887,12 +890,14 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, pkg, { featHandleMiss }); } = await detectBuilders(files, pkg, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/next'); expect(builders![0].use).toBe('@vercel/next');
expect(errors).toBe(null); expect(errors).toBe(null);
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([]); expect(rewriteRoutes).toStrictEqual([]);
expect(errorRoutes).toStrictEqual([]);
}); });
it('package.json + no build', async () => { it('package.json + no build', async () => {
@@ -913,12 +918,15 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, { featHandleMiss }); } = await detectBuilders(files, null, { featHandleMiss });
expect(builders).toBe(null); expect(builders).toBe(null);
expect(errors).toBe(null); expect(errors).toBe(null);
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([]); expect(rewriteRoutes).toStrictEqual([]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('no package.json + public', async () => { it('no package.json + public', async () => {
@@ -929,6 +937,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, { featHandleMiss }); } = await detectBuilders(files, null, { featHandleMiss });
expect(builders![1].use).toBe('@vercel/static'); expect(builders![1].use).toBe('@vercel/static');
expect(errors).toBe(null); expect(errors).toBe(null);
@@ -939,6 +948,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('no package.json + no build + raw static + api', async () => { it('no package.json + no build + raw static + api', async () => {
@@ -949,6 +960,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, { featHandleMiss }); } = await detectBuilders(files, null, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/users.js'); expect(builders![0].src).toBe('api/users.js');
@@ -963,6 +975,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('package.json + no build + root + api', async () => { it('package.json + no build + root + api', async () => {
@@ -990,6 +1004,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, undefined, { featHandleMiss }); } = await detectBuilders(files, undefined, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/[endpoint]/[id].js'); expect(builders![0].src).toBe('api/[endpoint]/[id].js');
@@ -1002,6 +1017,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(rewriteRoutes!.length).toBe(2); expect(rewriteRoutes!.length).toBe(2);
expect((rewriteRoutes![0] as Source).src).toBe('^/api/([^/]+)/([^/]+)$'); expect((rewriteRoutes![0] as Source).src).toBe('^/api/([^/]+)/([^/]+)$');
expect((rewriteRoutes![1] as Source).status).toBe(404); expect((rewriteRoutes![1] as Source).status).toBe(404);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('api + next + public', async () => { it('api + next + public', async () => {
@@ -1016,6 +1033,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, pkg, { featHandleMiss }); } = await detectBuilders(files, pkg, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/endpoint.js'); expect(builders![0].src).toBe('api/endpoint.js');
@@ -1029,6 +1047,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes).toStrictEqual([]);
}); });
it('api + next + raw static', async () => { it('api + next + raw static', async () => {
@@ -1043,6 +1062,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, pkg, { featHandleMiss }); } = await detectBuilders(files, pkg, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/endpoint.js'); expect(builders![0].src).toBe('api/endpoint.js');
@@ -1056,6 +1076,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes).toStrictEqual([]);
}); });
it('api + raw static', async () => { it('api + raw static', async () => {
@@ -1066,6 +1087,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, { featHandleMiss }); } = await detectBuilders(files, null, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/endpoint.js'); expect(builders![0].src).toBe('api/endpoint.js');
@@ -1079,6 +1101,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('api + raw static + package.json no build script', async () => { it('api + raw static + package.json no build script', async () => {
@@ -1093,6 +1117,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, pkg, { featHandleMiss }); } = await detectBuilders(files, pkg, { featHandleMiss });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/version.js'); expect(builders![0].src).toBe('api/version.js');
@@ -1106,6 +1131,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('api + public', async () => { it('api + public', async () => {
@@ -1116,7 +1143,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
'README.md', 'README.md',
]; ];
const { builders } = await detectBuilders(files, undefined, { const { builders, errorRoutes } = await detectBuilders(files, undefined, {
featHandleMiss, featHandleMiss,
}); });
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
@@ -1124,6 +1151,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(builders![1].use).toBe('@vercel/static'); expect(builders![1].use).toBe('@vercel/static');
expect(builders![1].src).toBe('public/**/*'); expect(builders![1].src).toBe('public/**/*');
expect(builders!.length).toBe(2); expect(builders!.length).toBe(2);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('api go with test files', async () => { it('api go with test files', async () => {
@@ -1142,21 +1171,26 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
'api/src/controllers/user.module_test.go', 'api/src/controllers/user.module_test.go',
]; ];
const { builders } = await detectBuilders(files, undefined, { const { builders, errorRoutes } = await detectBuilders(files, undefined, {
featHandleMiss, featHandleMiss,
}); });
expect(builders!.length).toBe(7); expect(builders!.length).toBe(7);
expect(builders!.some(b => b.src.endsWith('_test.go'))).toBe(false); expect(builders!.some(b => b.src.endsWith('_test.go'))).toBe(false);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('just public', async () => { it('just public', async () => {
const files = ['public/index.html', 'public/favicon.ico', 'README.md']; const files = ['public/index.html', 'public/favicon.ico', 'README.md'];
const { builders } = await detectBuilders(files, undefined, { const { builders, errorRoutes } = await detectBuilders(files, undefined, {
featHandleMiss, featHandleMiss,
}); });
expect(builders![0].src).toBe('public/**/*'); expect(builders![0].src).toBe('public/**/*');
expect(builders![0].use).toBe('@vercel/static');
expect(builders!.length).toBe(1); expect(builders!.length).toBe(1);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('next + public', async () => { it('next + public', async () => {
@@ -1166,10 +1200,13 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}; };
const files = ['package.json', 'public/index.html', 'README.md']; const files = ['package.json', 'public/index.html', 'README.md'];
const { builders } = await detectBuilders(files, pkg, { featHandleMiss }); const { builders, errorRoutes } = await detectBuilders(files, pkg, {
featHandleMiss,
});
expect(builders![0].use).toBe('@vercel/next'); expect(builders![0].use).toBe('@vercel/next');
expect(builders![0].src).toBe('package.json'); expect(builders![0].src).toBe('package.json');
expect(builders!.length).toBe(1); expect(builders!.length).toBe(1);
expect(errorRoutes!.length).toBe(0);
}); });
it('nuxt', async () => { it('nuxt', async () => {
@@ -1179,10 +1216,14 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}; };
const files = ['package.json', 'pages/index.js']; const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg, { featHandleMiss }); const { builders, errorRoutes } = await detectBuilders(files, pkg, {
featHandleMiss,
});
expect(builders![0].use).toBe('@vercel/static-build'); expect(builders![0].use).toBe('@vercel/static-build');
expect(builders![0].src).toBe('package.json'); expect(builders![0].src).toBe('package.json');
expect(builders!.length).toBe(1); expect(builders!.length).toBe(1);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('nuxt + tag canary', async () => { it('nuxt + tag canary', async () => {
@@ -1192,23 +1233,29 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}; };
const files = ['package.json', 'pages/index.js']; const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg, { const { builders, errorRoutes } = await detectBuilders(files, pkg, {
tag: 'canary', tag: 'canary',
featHandleMiss, featHandleMiss,
}); });
expect(builders![0].use).toBe('@vercel/static-build@canary'); expect(builders![0].use).toBe('@vercel/static-build@canary');
expect(builders![0].src).toBe('package.json'); expect(builders![0].src).toBe('package.json');
expect(builders!.length).toBe(1); expect(builders!.length).toBe(1);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('package.json with no build + api', async () => { it('package.json with no build + api', async () => {
const pkg = { dependencies: { next: '9.0.0' } }; const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'api/[endpoint].js']; const files = ['package.json', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg, { featHandleMiss }); const { builders, errorRoutes } = await detectBuilders(files, pkg, {
featHandleMiss,
});
expect(builders![0].use).toBe('@vercel/node'); expect(builders![0].use).toBe('@vercel/node');
expect(builders![0].src).toBe('api/[endpoint].js'); expect(builders![0].src).toBe('api/[endpoint].js');
expect(builders!.length).toBe(1); expect(builders!.length).toBe(1);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('package.json with no build + public directory', async () => { it('package.json with no build + public directory', async () => {
@@ -1327,7 +1374,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
it('many static files + one api file', async () => { it('many static files + one api file', async () => {
const files = Array.from({ length: 5000 }).map((_, i) => `file${i}.html`); const files = Array.from({ length: 5000 }).map((_, i) => `file${i}.html`);
files.push('api/index.ts'); files.push('api/index.ts');
const { builders } = await detectBuilders(files, undefined, { const { builders, errorRoutes } = await detectBuilders(files, undefined, {
featHandleMiss, featHandleMiss,
}); });
@@ -1336,6 +1383,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(builders![0].src).toBe('api/index.ts'); expect(builders![0].src).toBe('api/index.ts');
expect(builders![1].use).toBe('@vercel/static'); expect(builders![1].use).toBe('@vercel/static');
expect(builders![1].src).toBe('!{api/**,package.json}'); expect(builders![1].src).toBe('!{api/**,package.json}');
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('functions with nextjs', async () => { it('functions with nextjs', async () => {
@@ -1530,6 +1579,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
const files = ['api/user.php']; const files = ['api/user.php'];
// @ts-ignore // @ts-ignore
const { errors } = await detectBuilders(files, null, { const { errors } = await detectBuilders(files, null, {
// @ts-ignore
functions, functions,
featHandleMiss, featHandleMiss,
}); });
@@ -1630,6 +1680,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
// @ts-ignore // @ts-ignore
const { errors } = await detectBuilders(files, null, { const { errors } = await detectBuilders(files, null, {
// @ts-ignore
functions, functions,
featHandleMiss, featHandleMiss,
}); });
@@ -1646,6 +1697,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
// @ts-ignore: Since we test an invalid type // @ts-ignore: Since we test an invalid type
const { errors } = await detectBuilders(files, null, { const { errors } = await detectBuilders(files, null, {
// @ts-ignore
functions, functions,
featHandleMiss, featHandleMiss,
}); });
@@ -1681,6 +1733,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, { } = await detectBuilders(files, null, {
projectSettings, projectSettings,
featHandleMiss, featHandleMiss,
@@ -1693,6 +1746,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([]); expect(rewriteRoutes).toStrictEqual([]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('Custom static output directory with api', async () => { it('Custom static output directory with api', async () => {
@@ -1707,6 +1762,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, { } = await detectBuilders(files, null, {
projectSettings, projectSettings,
featHandleMiss, featHandleMiss,
@@ -1722,6 +1778,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(redirectRoutes).toStrictEqual([]); expect(redirectRoutes).toStrictEqual([]);
expect(rewriteRoutes!.length).toBe(1); expect(rewriteRoutes!.length).toBe(1);
expect((rewriteRoutes![0] as Source).status).toBe(404); expect((rewriteRoutes![0] as Source).status).toBe(404);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('Framework with non-package.json entrypoint', async () => { it('Framework with non-package.json entrypoint', async () => {
@@ -1730,7 +1788,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
framework: 'hugo', framework: 'hugo',
}; };
const { builders } = await detectBuilders(files, null, { const { builders, errorRoutes } = await detectBuilders(files, null, {
projectSettings, projectSettings,
featHandleMiss, featHandleMiss,
}); });
@@ -1745,6 +1803,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}, },
}, },
]); ]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('No framework, only package.json', async () => { it('No framework, only package.json', async () => {
@@ -1755,7 +1815,9 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}, },
}; };
const { builders } = await detectBuilders(files, pkg, { featHandleMiss }); const { builders, errorRoutes } = await detectBuilders(files, pkg, {
featHandleMiss,
});
expect(builders).toEqual([ expect(builders).toEqual([
{ {
@@ -1766,13 +1828,15 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}, },
}, },
]); ]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('Framework with an API', async () => { it('Framework with an API', async () => {
const files = ['config.rb', 'api/date.rb']; const files = ['config.rb', 'api/date.rb'];
const projectSettings = { framework: 'middleman' }; const projectSettings = { framework: 'middleman' };
const { builders } = await detectBuilders(files, null, { const { builders, errorRoutes } = await detectBuilders(files, null, {
projectSettings, projectSettings,
featHandleMiss, featHandleMiss,
}); });
@@ -1794,6 +1858,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
}, },
}, },
]); ]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('Error for non-api functions', async () => { it('Error for non-api functions', async () => {
@@ -1843,13 +1909,19 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
it('All static if `buildCommand` is an empty string with an `outputDirectory`', async () => { it('All static if `buildCommand` is an empty string with an `outputDirectory`', async () => {
const files = ['out/index.html']; const files = ['out/index.html'];
const projectSettings = { buildCommand: '', outputDirectory: 'out' }; const projectSettings = { buildCommand: '', outputDirectory: 'out' };
const { builders, errors } = await detectBuilders(files, null, { const { builders, errors, errorRoutes } = await detectBuilders(
projectSettings, files,
featHandleMiss, null,
}); {
projectSettings,
featHandleMiss,
}
);
expect(errors).toBe(null); expect(errors).toBe(null);
expect(builders![0]!.use).toBe('@vercel/static'); expect(builders![0]!.use).toBe('@vercel/static');
expect(builders![0]!.src).toBe('out/**/*'); expect(builders![0]!.src).toBe('out/**/*');
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
}); });
it('do not require build script when `buildCommand` is an empty string', async () => { it('do not require build script when `buildCommand` is an empty string', async () => {
@@ -2025,9 +2097,13 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
{ {
const files = ['api/user.go', 'api/team.js', 'api/package.json']; const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { defaultRoutes, rewriteRoutes } = await detectBuilders(files, null, { const { defaultRoutes, rewriteRoutes, errorRoutes } = await detectBuilders(
featHandleMiss, files,
}); null,
{
featHandleMiss,
}
);
expect(defaultRoutes).toStrictEqual([ expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' }, { handle: 'miss' },
{ {
@@ -2043,6 +2119,44 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
continue: true, continue: true,
}, },
]); ]);
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
dest: '/404.html',
},
]);
const pattern = new RegExp(errorRoutes![0].src!);
[
'/',
'/index.html',
'/page.html',
'/page',
'/another/index.html',
'/another/page.html',
'/another/page',
'/another/sub/index.html',
'/another/sub/page.html',
'/another/sub/page',
].forEach(file => {
expect(file).toMatch(pattern);
});
[
'/api',
'/api/',
'/api/index.html',
'/api/page.html',
'/api/page',
'/api/sub',
'/api/sub/index.html',
'/api/sub/page.html',
'/api/sub/page',
].forEach(file => {
expect(file).not.toMatch(pattern);
});
} }
{ {
@@ -2326,6 +2440,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, options); } = await detectBuilders(files, null, options);
testHeaders(redirectRoutes); testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
@@ -2336,6 +2451,13 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
continue: true, continue: true,
}, },
]); ]);
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
dest: '/404',
},
]);
// expected redirect should match inputs // expected redirect should match inputs
const getLocation = createReplaceLocation(redirectRoutes); const getLocation = createReplaceLocation(redirectRoutes);

View File

@@ -39,7 +39,7 @@ describe('Test `getPlatformEnv()`', () => {
assert(err); assert(err);
assert.equal( assert.equal(
err!.message, err!.message,
'Both "VERCEL_FOO" and "NOW_FOO" env vars are defined. Please only define the "VERCEL_FOO" env var' 'Both "VERCEL_FOO" and "NOW_FOO" env vars are defined. Please only define the "VERCEL_FOO" env var.'
); );
}); });
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/cgi", "name": "@vercel/cgi",
"version": "1.0.6-canary.0", "version": "1.0.6",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel", "name": "vercel",
"version": "19.0.2-canary.8", "version": "19.1.0",
"preferGlobal": true, "preferGlobal": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "The command-line interface for Now", "description": "The command-line interface for Now",
@@ -62,13 +62,13 @@
"node": ">= 10" "node": ">= 10"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "2.3.2-canary.2", "@vercel/build-utils": "2.4.0",
"@vercel/go": "1.1.2-canary.0", "@vercel/go": "1.1.2",
"@vercel/next": "2.6.3-canary.4", "@vercel/next": "2.6.6",
"@vercel/node": "1.6.2-canary.4", "@vercel/node": "1.7.0",
"@vercel/python": "1.2.2-canary.1", "@vercel/python": "1.2.2",
"@vercel/ruby": "1.2.2-canary.0", "@vercel/ruby": "1.2.2",
"@vercel/static-build": "0.17.2-canary.0" "@vercel/static-build": "0.17.2"
}, },
"devDependencies": { "devDependencies": {
"@sentry/node": "5.5.0", "@sentry/node": "5.5.0",
@@ -106,7 +106,7 @@
"@zeit/fun": "0.11.2", "@zeit/fun": "0.11.2",
"@zeit/ncc": "0.18.5", "@zeit/ncc": "0.18.5",
"@zeit/source-map-support": "0.6.2", "@zeit/source-map-support": "0.6.2",
"ajv": "6.10.2", "ajv": "6.12.2",
"alpha-sort": "2.0.1", "alpha-sort": "2.0.1",
"ansi-escapes": "3.0.0", "ansi-escapes": "3.0.0",
"ansi-regex": "3.0.0", "ansi-regex": "3.0.0",
@@ -136,6 +136,7 @@
"escape-html": "1.0.3", "escape-html": "1.0.3",
"esm": "3.1.4", "esm": "3.1.4",
"execa": "3.2.0", "execa": "3.2.0",
"fast-deep-equal": "3.1.3",
"fs-extra": "7.0.1", "fs-extra": "7.0.1",
"get-port": "5.1.1", "get-port": "5.1.1",
"glob": "7.1.2", "glob": "7.1.2",
@@ -182,7 +183,7 @@
"tmp-promise": "1.0.3", "tmp-promise": "1.0.3",
"tree-kill": "1.2.1", "tree-kill": "1.2.1",
"ts-node": "8.3.0", "ts-node": "8.3.0",
"typescript": "3.6.4", "typescript": "3.9.3",
"universal-analytics": "0.4.20", "universal-analytics": "0.4.20",
"update-check": "1.5.3", "update-check": "1.5.3",
"utility-types": "2.1.0", "utility-types": "2.1.0",

View File

@@ -75,6 +75,17 @@ async function main() {
const dest = join(dirRoot, 'dist/runtimes'); const dest = join(dirRoot, 'dist/runtimes');
await cpy('**/*', dest, { parents: true, cwd: runtimes }); await cpy('**/*', dest, { parents: true, cwd: runtimes });
// Band-aid to delete stuff that `ncc` bundles, but it shouldn't:
// TypeScript definition files from `@vercel/build-utils`
await remove(join(dirRoot, 'dist', 'dist'));
// The Readme and `package.json` from "config-chain" module
await remove(join(dirRoot, 'dist', 'config-chain'));
// A bunch of source `.ts` files from CLI's `util` directory
await remove(join(dirRoot, 'dist', 'util'));
console.log('Finished building `now-cli`'); console.log('Finished building `now-cli`');
} }

View File

@@ -39,7 +39,6 @@ import {
} from '../../util/errors-ts'; } from '../../util/errors-ts';
import { SchemaValidationFailed } from '../../util/errors'; import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available'; import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
import isWildcardAlias from '../../util/alias/is-wildcard-alias';
import confirm from '../../util/input/confirm'; import confirm from '../../util/input/confirm';
import editProjectSettings from '../../util/input/edit-project-settings'; import editProjectSettings from '../../util/input/edit-project-settings';
import { import {
@@ -56,6 +55,7 @@ import validatePaths, {
} from '../../util/validate-paths'; } from '../../util/validate-paths';
import { readLocalConfig } from '../../util/config/files'; import { readLocalConfig } from '../../util/config/files';
import { getCommandName } from '../../util/pkg-name.ts'; import { getCommandName } from '../../util/pkg-name.ts';
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url.ts';
const addProcessEnv = async (log, env) => { const addProcessEnv = async (log, env) => {
let val; let val;
@@ -87,6 +87,7 @@ const addProcessEnv = async (log, env) => {
const printDeploymentStatus = async ( const printDeploymentStatus = async (
output, output,
client,
{ {
readyState, readyState,
alias: aliasList, alias: aliasList,
@@ -119,18 +120,14 @@ const printDeploymentStatus = async (
let previewUrl; let previewUrl;
let isWildcard; let isWildcard;
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) { if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
// search for a non now.sh/non wildcard domain const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
// but fallback to the first alias in the list if (previewUrlInfo) {
const mainAlias = isWildcard = previewUrlInfo.isWildcard;
aliasList.find( previewUrl = previewUrlInfo.previewUrl;
alias => } else {
!alias.endsWith('.now.sh') && isWildcard = false;
!alias.endsWith('.vercel.app') && previewUrl = `https://${deploymentUrl}`;
!isWildcardAlias(alias) }
) || aliasList[0];
isWildcard = isWildcardAlias(mainAlias);
previewUrl = isWildcard ? mainAlias : `https://${mainAlias}`;
} else { } else {
// fallback to deployment url // fallback to deployment url
isWildcard = false; isWildcard = false;
@@ -251,7 +248,9 @@ export default async function main(
if (argv['--name']) { if (argv['--name']) {
output.print( output.print(
`${prependEmoji( `${prependEmoji(
`The ${param('--name')} flag is deprecated (https://zeit.ink/1B)`, `The ${param(
'--name'
)} option is deprecated (https://vercel.link/name-flag)`,
emoji('warning') emoji('warning')
)}\n` )}\n`
); );
@@ -389,7 +388,7 @@ export default async function main(
`${prependEmoji( `${prependEmoji(
`The ${code('name')} property in ${highlight( `The ${code('name')} property in ${highlight(
localConfig[fileNameSymbol] localConfig[fileNameSymbol]
)} is deprecated (https://zeit.ink/5F)`, )} is deprecated (https://vercel.link/name-prop)`,
emoji('warning') emoji('warning')
)}\n` )}\n`
); );
@@ -573,8 +572,11 @@ export default async function main(
if (deployment instanceof Error) { if (deployment instanceof Error) {
output.error( output.error(
`${deployment.message || deployment.message ||
'An unexpected error occurred while deploying your project'} (http://zeit.ink/P4)` 'An unexpected error occurred while deploying your project',
null,
'https://vercel.link/help',
'Contact Support'
); );
return 1; return 1;
} }
@@ -701,6 +703,12 @@ export default async function main(
return printDeploymentStatus( return printDeploymentStatus(
output, output,
new Client({
apiUrl: ctx.apiUrl,
token: ctx.authConfig.token,
currentTeam: org.type === 'team' ? org.id : null,
debug: debugEnabled,
}),
deployment, deployment,
deployStamp, deployStamp,
!argv['--no-clipboard'], !argv['--no-clipboard'],

View File

@@ -119,7 +119,7 @@ export default async function main(ctx: NowContext) {
try { try {
return await dev(ctx, argv, args, output); return await dev(ctx, argv, args, output);
} catch (err) { } catch (err) {
output.error(err.message); output.prettyError(err);
output.debug(stringifyError(err)); output.debug(stringifyError(err));
return 1; return 1;
} }

View File

@@ -257,7 +257,7 @@ const login = async ctx => {
output.print( output.print(
`${prependEmoji( `${prependEmoji(
`Connect your Git Repositories to deploy every branch push automatically (https://zeit.ink/1X).`, `Connect your Git Repositories to deploy every branch push automatically (https://vercel.link/git).`,
emoji('tip') emoji('tip')
)}\n` )}\n`
); );

View File

@@ -1,8 +1,8 @@
import chalk from 'chalk'; import chalk from 'chalk';
import table from 'text-table'; import table from 'text-table';
import mri from 'mri';
import ms from 'ms'; import ms from 'ms';
import strlen from '../util/strlen'; import strlen from '../util/strlen';
import getArgs from '../util/get-args';
import { handleError, error } from '../util/error'; import { handleError, error } from '../util/error';
import exit from '../util/exit'; import exit from '../util/exit';
import Client from '../util/client.ts'; import Client from '../util/client.ts';
@@ -11,7 +11,6 @@ import getScope from '../util/get-scope';
import createOutput from '../util/output'; import createOutput from '../util/output';
import getCommandFlags from '../util/get-command-flags'; import getCommandFlags from '../util/get-command-flags';
import wait from '../util/output/wait'; import wait from '../util/output/wait';
import getPrefixedFlags from '../util/get-prefixed-flags';
import { getPkgName, getCommandName } from '../util/pkg-name.ts'; import { getPkgName, getCommandName } from '../util/pkg-name.ts';
const e = encodeURIComponent; const e = encodeURIComponent;
@@ -56,23 +55,25 @@ let apiUrl;
let subcommand; let subcommand;
const main = async ctx => { const main = async ctx => {
argv = mri(ctx.argv.slice(2), { try {
boolean: ['help'], argv = getArgs(ctx.argv.slice(2), {
alias: { '--next': Number,
help: 'h', '-N': '--next',
next: 'N', });
}, } catch (error) {
}); handleError(error);
return exit(1);
}
argv._ = argv._.slice(1); argv._ = argv._.slice(1);
debug = argv.debug; debug = argv['--debug'];
apiUrl = ctx.apiUrl; apiUrl = ctx.apiUrl;
subcommand = argv._[0]; subcommand = argv._[0] || 'list';
if (argv.help || !subcommand) { if (argv['--help']) {
help(); help();
await exit(0); return exit(2);
} }
const output = createOutput({ debug }); const output = createOutput({ debug });
@@ -126,15 +127,16 @@ async function run({ client, contextName }) {
)}` )}`
) )
); );
return exit(1); return exit(2);
} }
const stopSpinner = wait(`Fetching projects in ${chalk.bold(contextName)}`); const stopSpinner = wait(`Fetching projects in ${chalk.bold(contextName)}`);
let projectsUrl = '/v4/projects/?limit=20'; let projectsUrl = '/v4/projects/?limit=20';
if (argv.next) { const next = argv['--next'];
projectsUrl += `&until=${argv.next}`; if (next) {
projectsUrl += `&until=${next}`;
} }
const { projects: list, pagination } = await client.fetch(projectsUrl, { const { projects: list, pagination } = await client.fetch(projectsUrl, {
@@ -175,14 +177,7 @@ async function run({ client, contextName }) {
} }
if (pagination && pagination.count === 20) { if (pagination && pagination.count === 20) {
const prefixedArgs = getPrefixedFlags(argv); const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
const flags = getCommandFlags(prefixedArgs, [
'_',
'--next',
'-N',
'-d',
'-y',
]);
const nextCmd = `projects ls${flags} --next ${pagination.next}`; const nextCmd = `projects ls${flags} --next ${pagination.next}`;
console.log(`To display the next page run ${getCommandName(nextCmd)}`); console.log(`To display the next page run ${getCommandName(nextCmd)}`);
} }
@@ -271,7 +266,7 @@ async function run({ client, contextName }) {
console.error(error('Please specify a valid subcommand: ls | add | rm')); console.error(error('Please specify a valid subcommand: ls | add | rm'));
help(); help();
exit(1); exit(2);
} }
process.on('uncaughtException', err => { process.on('uncaughtException', err => {

View File

@@ -24,6 +24,7 @@ import checkForUpdate from 'update-check';
import ms from 'ms'; import ms from 'ms';
import { URL } from 'url'; import { URL } from 'url';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { NowBuildError } from '@vercel/build-utils';
import getGlobalPathConfig from './util/config/global-path'; import getGlobalPathConfig from './util/config/global-path';
import { import {
getDefaultConfig, getDefaultConfig,
@@ -114,10 +115,10 @@ const main = async argv_ => {
} }
if ( if (
localConfig instanceof NowError && (localConfig instanceof NowError || localConfig instanceof NowBuildError) &&
!(localConfig instanceof ERRORS.CantFindConfig) !(localConfig instanceof ERRORS.CantFindConfig)
) { ) {
output.error(`Failed to load local config file: ${localConfig.message}`); output.prettyError(localConfig);
return 1; return 1;
} }
@@ -529,7 +530,7 @@ const main = async argv_ => {
if (argv['--team']) { if (argv['--team']) {
output.warn( output.warn(
`The ${param('--team')} flag is deprecated. Please use ${param( `The ${param('--team')} option is deprecated. Please use ${param(
'--scope' '--scope'
)} instead.` )} instead.`
); );
@@ -644,27 +645,6 @@ const main = async argv_ => {
)} could not be resolved. Please verify your internet connectivity and DNS configuration.` )} could not be resolved. Please verify your internet connectivity and DNS configuration.`
); );
output.debug(err.stack); output.debug(err.stack);
return 1;
}
await reportError(Sentry, err, apiUrl, configFiles);
// If there is a code we should not consider the error unexpected
// but instead show the message. Any error that is handled by this should
// actually be handled in the sub command instead. Please make sure
// that happens for anything that lands here. It should NOT bubble up to here.
if (err.code) {
output.debug(err.stack);
output.error(err.message);
if (shouldCollectMetrics) {
metric
.event(eventCategory, '1', pkg.version)
.exception(err.message)
.send();
}
return 1; return 1;
} }
@@ -675,9 +655,23 @@ const main = async argv_ => {
.send(); .send();
} }
// Otherwise it is an unexpected error and we should show the trace // If there is a code we should not consider the error unexpected
// and an unexpected error message // but instead show the message. Any error that is handled by this should
output.error(`An unexpected error occurred in ${subcommand}: ${err.stack}`); // actually be handled in the sub command instead. Please make sure
// that happens for anything that lands here. It should NOT bubble up to here.
if (err.code) {
output.debug(err.stack);
output.prettyError(err);
} else {
await reportError(Sentry, err, apiUrl, configFiles);
// Otherwise it is an unexpected error and we should show the trace
// and an unexpected error message
output.error(
`An unexpected error occurred in ${subcommand}: ${err.stack}`
);
}
return 1; return 1;
} }
@@ -688,8 +682,6 @@ const main = async argv_ => {
return exitCode; return exitCode;
}; };
debug('start');
const handleRejection = async err => { const handleRejection = async err => {
debug('handling rejection'); debug('handling rejection');

View File

@@ -0,0 +1,17 @@
import { stringify } from 'querystring';
import { Cert } from '../../types';
import Client from '../client';
/**
* Returns certs that contain @param cn.
*/
export async function getCertsForCn(
client: Client,
cn: string,
{ limit }: { limit?: number } = {}
) {
const { certs } = await client.fetch<{
certs: Cert[];
}>(`/v4/now/certs?${stringify({ cn, ...(limit ? { limit } : {}) })}`);
return certs;
}

View File

@@ -24,7 +24,7 @@ export default class Client extends EventEmitter {
_withCache: boolean; _withCache: boolean;
_output: Output; _output: Output;
_token: string; _token: string;
currentTeam?: string; currentTeam?: string | null;
constructor({ constructor({
apiUrl, apiUrl,
@@ -36,7 +36,7 @@ export default class Client extends EventEmitter {
}: { }: {
apiUrl: string; apiUrl: string;
token: string; token: string;
currentTeam?: string; currentTeam?: string | null;
forceNew?: boolean; forceNew?: boolean;
withCache?: boolean; withCache?: boolean;
debug?: boolean; debug?: boolean;

View File

@@ -1,6 +1,7 @@
import path from 'path'; import path from 'path';
import mri from 'mri'; import mri from 'mri';
import { InvalidLocalConfig } from '../errors'; import { InvalidLocalConfig } from '../errors';
import { ConflictingConfigFiles } from '../errors-ts';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
export default function getLocalPathConfig(prefix: string) { export default function getLocalPathConfig(prefix: string) {
@@ -11,20 +12,30 @@ export default function getLocalPathConfig(prefix: string) {
}, },
}); });
// If `--local-config` flag was specified, then that takes priority
const customPath = args['local-config']; const customPath = args['local-config'];
if (customPath) {
if (customPath && typeof customPath !== 'string') { if (typeof customPath !== 'string') {
throw new InvalidLocalConfig(customPath); throw new InvalidLocalConfig(customPath);
}
return path.resolve(prefix, customPath);
} }
const possibleConfigFiles = [ // Otherwise check for either `vercel.json` or `now.json`.
path.join(prefix, 'vercel.json'), // Throw an error if both exist.
path.join(prefix, 'now.json'), const vercelConfigPath = path.join(prefix, 'vercel.json');
]; const nowConfigPath = path.join(prefix, 'now.json');
return ( const vercelConfigExists = existsSync(vercelConfigPath);
(customPath && path.resolve(prefix, customPath)) || const nowConfigExists = existsSync(nowConfigPath);
possibleConfigFiles.find(configFile => existsSync(configFile)) ||
possibleConfigFiles[0] if (nowConfigExists && vercelConfigExists) {
); throw new ConflictingConfigFiles([vercelConfigPath, nowConfigPath]);
}
if (nowConfigExists) {
return nowConfigPath;
}
return vercelConfigPath;
} }

View File

@@ -0,0 +1,48 @@
import isWildcardAlias from '../alias/is-wildcard-alias';
import { getCertsForCn } from '../certs/get-certs-for-cn';
import Client from '../client';
/**
* Tries to find the "best" alias url.
* @param aliasList
*/
export async function getPreferredPreviewURL(
client: Client,
aliasList: string[]
) {
if (aliasList.length === 0) {
return null;
}
/**
* First checks for non public aliases and non wildcard domains.
*/
const preferredAliases = aliasList.filter(
alias =>
!alias.endsWith('.now.sh') &&
!alias.endsWith('.vercel.app') &&
!isWildcardAlias(alias)
);
for (const alias of preferredAliases) {
const certs = await getCertsForCn(client, alias, { limit: 1 }).catch(() => {
return null;
});
if (certs && certs.length > 0) {
return { previewUrl: `https://${alias}`, isWildcard: false };
}
}
/**
* Fallback to first alias
*/
const [firstAlias] = aliasList;
if (isWildcardAlias(firstAlias)) {
return { previewUrl: firstAlias, isWildcard: true };
}
if (firstAlias.endsWith('.vercel.app') || firstAlias.endsWith('.now.sh')) {
return { previewUrl: `https://${firstAlias}`, isWildcard: false };
}
return { previewUrl: `http://${firstAlias}`, isWildcard: false };
}

View File

@@ -3,9 +3,9 @@ import semver from 'semver';
import npa from 'npm-package-arg'; import npa from 'npm-package-arg';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { basename, join } from 'path'; import { basename, join } from 'path';
import { PackageJson } from '@vercel/build-utils';
import XDGAppPaths from 'xdg-app-paths'; import XDGAppPaths from 'xdg-app-paths';
import { mkdirp, readJSON, writeJSON } from 'fs-extra'; import { mkdirp, readJSON, writeJSON } from 'fs-extra';
import { NowBuildError, PackageJson } from '@vercel/build-utils';
import cliPkg from '../pkg'; import cliPkg from '../pkg';
import { NoBuilderCacheError } from '../errors-ts'; import { NoBuilderCacheError } from '../errors-ts';
@@ -232,21 +232,41 @@ async function npmInstall(
output.debug(`Running npm install in ${cwd}`); output.debug(`Running npm install in ${cwd}`);
try { try {
await execa( const args = [
'npm', 'install',
[ '--save-exact',
'install', '--no-package-lock',
'--save-exact', '--no-audit',
'--no-package-lock', '--no-progress',
'--no-audit', ];
'--no-progress', if (process.stderr.isTTY) {
...sortedPackages, // Force colors in the npm child process
], // https://docs.npmjs.com/misc/config#color
{ args.push('--color=always');
cwd, }
stdio: output.isDebugEnabled() ? 'inherit' : undefined, args.push(...sortedPackages);
const result = await execa('npm', args, {
cwd,
reject: false,
stdio: output.isDebugEnabled() ? 'inherit' : 'pipe',
});
if (result.failed) {
stopSpinner();
if (result.stdout) {
console.log(result.stdout);
} }
); if (result.stderr) {
console.error(result.stderr);
}
throw new NowBuildError({
message:
(result as any).code === 'ENOENT'
? '`npm` is not installed'
: 'Failed to install `vercel dev` dependencies',
code: 'NPM_INSTALL_ERROR',
link: 'https://vercel.link/npm-install-failed-dev',
});
}
} finally { } finally {
stopSpinner(); stopSpinner();
} }

View File

@@ -18,6 +18,7 @@ import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port'; import getPort from 'get-port';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import isPortReachable from 'is-port-reachable'; import isPortReachable from 'is-port-reachable';
import deepEqual from 'fast-deep-equal';
import which from 'which'; import which from 'which';
import { getVercelIgnore, fileNameSymbol } from '@vercel/client'; import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
@@ -114,6 +115,7 @@ export default class DevServer {
public cwd: string; public cwd: string;
public debug: boolean; public debug: boolean;
public output: Output; public output: Output;
public proxy: httpProxy;
public envConfigs: EnvConfigs; public envConfigs: EnvConfigs;
public frameworkSlug: string | null; public frameworkSlug: string | null;
public files: BuilderInputs; public files: BuilderInputs;
@@ -124,7 +126,6 @@ export default class DevServer {
private caseSensitive: boolean; private caseSensitive: boolean;
private apiDir: string | null; private apiDir: string | null;
private apiExtensions: Set<string>; private apiExtensions: Set<string>;
private proxy: httpProxy;
private server: http.Server; private server: http.Server;
private stopping: boolean; private stopping: boolean;
private buildMatches: Map<string, BuildMatch>; private buildMatches: Map<string, BuildMatch>;
@@ -234,8 +235,18 @@ export default class DevServer {
} }
} }
// Update the build matches in case an entrypoint was created or deleted
const nowConfig = await this.getNowConfig(false); const nowConfig = await this.getNowConfig(false);
// Update the env vars configuration
const nowConfigBuild = nowConfig.build || {};
const [runEnv, buildEnv] = await Promise.all([
this.getLocalEnv('.env', nowConfig.env),
this.getLocalEnv('.env.build', nowConfigBuild.env),
]);
const allEnv = { ...buildEnv, ...runEnv };
this.envConfigs = { buildEnv, runEnv, allEnv };
// Update the build matches in case an entrypoint was created or deleted
await this.updateBuildMatches(nowConfig); await this.updateBuildMatches(nowConfig);
const filesChangedArray = [...filesChanged]; const filesChangedArray = [...filesChanged];
@@ -400,7 +411,7 @@ export default class DevServer {
const blockingBuilds: Promise<void>[] = []; const blockingBuilds: Promise<void>[] = [];
for (const match of matches) { for (const match of matches) {
const currentMatch = this.buildMatches.get(match.src); const currentMatch = this.buildMatches.get(match.src);
if (!currentMatch || currentMatch.use !== match.use) { if (!buildMatchEquals(currentMatch, match)) {
this.output.debug( this.output.debug(
`Adding build match for "${match.src}" with "${match.use}"` `Adding build match for "${match.src}" with "${match.use}"`
); );
@@ -522,18 +533,11 @@ export default class DevServer {
// The default empty `vercel.json` is used to serve all files as static // The default empty `vercel.json` is used to serve all files as static
// when no `vercel.json` is present // when no `vercel.json` is present
let configPath = 'vercel.json'; let configPath = 'vercel.json';
let config: NowConfig = this.cachedNowConfig || { let config: NowConfig = {
version: 2, version: 2,
[fileNameSymbol]: configPath, [fileNameSymbol]: configPath,
}; };
// We need to delete these properties for zero config to work
// with file changes
if (this.cachedNowConfig) {
delete this.cachedNowConfig.builds;
delete this.cachedNowConfig.routes;
}
try { try {
configPath = getNowConfigPath(this.cwd); configPath = getNowConfigPath(this.cwd);
this.output.debug(`Reading ${configPath}`); this.output.debug(`Reading ${configPath}`);
@@ -564,7 +568,7 @@ export default class DevServer {
nowConfig: config, nowConfig: config,
}); });
if (routeError) { if (routeError) {
this.output.error(routeError.message); this.output.prettyError(routeError);
await this.exit(); await this.exit();
} }
config.routes = maybeRoutes || []; config.routes = maybeRoutes || [];
@@ -581,6 +585,7 @@ export default class DevServer {
defaultRoutes, defaultRoutes,
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, pkg, { } = await detectBuilders(files, pkg, {
tag: getDistTag(cliPkg.version) === 'canary' ? 'canary' : 'latest', tag: getDistTag(cliPkg.version) === 'canary' ? 'canary' : 'latest',
functions: config.functions, functions: config.functions,
@@ -606,20 +611,25 @@ export default class DevServer {
config.builds = config.builds || []; config.builds = config.builds || [];
config.builds.push(...builders); config.builds.push(...builders);
const routes: Route[] = [];
const { routes: nowConfigRoutes } = config;
routes.push(...(redirectRoutes || []));
routes.push(
...appendRoutesToPhase({
routes: nowConfigRoutes,
newRoutes: rewriteRoutes,
phase: 'filesystem',
})
);
routes.push(...(defaultRoutes || []));
config.routes = routes;
} }
let routes: Route[] = [];
const { routes: nowConfigRoutes } = config;
routes.push(...(redirectRoutes || []));
routes.push(
...appendRoutesToPhase({
routes: nowConfigRoutes,
newRoutes: rewriteRoutes,
phase: 'filesystem',
})
);
routes = appendRoutesToPhase({
routes,
newRoutes: errorRoutes,
phase: 'error',
});
routes.push(...(defaultRoutes || []));
config.routes = routes;
} }
if (Array.isArray(config.builds)) { if (Array.isArray(config.builds)) {
@@ -767,7 +777,6 @@ export default class DevServer {
this.getLocalEnv('.env.build', nowConfigBuild.env), this.getLocalEnv('.env.build', nowConfigBuild.env),
]); ]);
const allEnv = { ...buildEnv, ...runEnv }; const allEnv = { ...buildEnv, ...runEnv };
Object.assign(process.env, allEnv);
this.envConfigs = { buildEnv, runEnv, allEnv }; this.envConfigs = { buildEnv, runEnv, allEnv };
const opts = { output: this.output, isBuilds: true }; const opts = { output: this.output, isBuilds: true };
@@ -1323,8 +1332,7 @@ export default class DevServer {
const handleMap = getRoutesTypes(routes); const handleMap = getRoutesTypes(routes);
const missRoutes = handleMap.get('miss') || []; const missRoutes = handleMap.get('miss') || [];
const hitRoutes = handleMap.get('hit') || []; const hitRoutes = handleMap.get('hit') || [];
handleMap.delete('miss'); const errorRoutes = handleMap.get('error') || [];
handleMap.delete('hit');
const phases: (HandleValue | null)[] = [null, 'filesystem']; const phases: (HandleValue | null)[] = [null, 'filesystem'];
let routeResult: RouteResult | null = null; let routeResult: RouteResult | null = null;
@@ -1362,7 +1370,7 @@ export default class DevServer {
debug(`ProxyPass: ${destUrl}`); debug(`ProxyPass: ${destUrl}`);
this.setResponseHeaders(res, nowRequestId); this.setResponseHeaders(res, nowRequestId);
return proxyPass(req, res, destUrl, this.proxy, this.output); return proxyPass(req, res, destUrl, this, nowRequestId);
} }
match = await findBuildMatch( match = await findBuildMatch(
@@ -1431,6 +1439,32 @@ export default class DevServer {
routeResult.status = prevStatus; routeResult.status = prevStatus;
} }
if (!match && errorRoutes.length > 0) {
// error phase
const routeResultForError = await devRouter(
getReqUrl(routeResult),
req.method,
errorRoutes,
this,
routeResult.headers,
[],
'error'
);
const matchForError = await findBuildMatch(
this.buildMatches,
this.files,
routeResultForError.dest,
this
);
if (matchForError) {
// error phase only applies if the file was found
routeResult = routeResultForError;
match = matchForError;
}
}
statusCode = routeResult.status; statusCode = routeResult.status;
if (match) { if (match) {
@@ -1475,8 +1509,8 @@ export default class DevServer {
req, req,
res, res,
`http://localhost:${this.devProcessPort}`, `http://localhost:${this.devProcessPort}`,
this.proxy, this,
this.output, nowRequestId,
false false
); );
} }
@@ -1560,7 +1594,7 @@ export default class DevServer {
req, req,
res, res,
nowRequestId, nowRequestId,
'NO_STATUS_CODE_FROM_DEV_SERVER', 'NO_RESPONSE_FROM_FUNCTION',
502 502
); );
return; return;
@@ -1595,15 +1629,14 @@ export default class DevServer {
req, req,
res, res,
`http://localhost:${port}`, `http://localhost:${port}`,
this.proxy, this,
this.output, nowRequestId,
false false
); );
} else { } else {
debug(`Skipping \`startDevServer()\` for ${match.entrypoint}`); debug(`Skipping \`startDevServer()\` for ${match.entrypoint}`);
} }
} }
let foundAsset = findAsset(match, requestPath, nowConfig); let foundAsset = findAsset(match, requestPath, nowConfig);
if (!foundAsset && callLevel === 0) { if (!foundAsset && callLevel === 0) {
@@ -1626,8 +1659,8 @@ export default class DevServer {
req, req,
res, res,
`http://localhost:${this.devProcessPort}`, `http://localhost:${this.devProcessPort}`,
this.proxy, this,
this.output, nowRequestId,
false false
); );
} }
@@ -1721,7 +1754,7 @@ export default class DevServer {
req, req,
res, res,
nowRequestId, nowRequestId,
'NO_STATUS_CODE_FROM_LAMBDA', 'NO_RESPONSE_FROM_FUNCTION',
502 502
); );
return; return;
@@ -1937,24 +1970,27 @@ function proxyPass(
req: http.IncomingMessage, req: http.IncomingMessage,
res: http.ServerResponse, res: http.ServerResponse,
dest: string, dest: string,
proxy: httpProxy, devServer: DevServer,
output: Output, nowRequestId: string,
ignorePath: boolean = true ignorePath: boolean = true
): void { ): void {
return proxy.web( return devServer.proxy.web(
req, req,
res, res,
{ target: dest, ignorePath }, { target: dest, ignorePath },
(error: NodeJS.ErrnoException) => { (error: NodeJS.ErrnoException) => {
// If the client hangs up a socket, we do not devServer.output.error(
// want to do anything, as the client just expects `Failed to complete request to ${req.url}: ${error}`
// the connection to be closed. );
if (error.code === 'ECONNRESET') { if (!res.headersSent) {
res.end(); devServer.sendError(
return; req,
res,
nowRequestId,
'NO_RESPONSE_FROM_FUNCTION',
502
);
} }
output.error(`Failed to complete request to ${req.url}: ${error}`);
} }
); );
} }
@@ -2014,12 +2050,24 @@ async function findBuildMatch(
isFilesystem?: boolean isFilesystem?: boolean
): Promise<BuildMatch | null> { ): Promise<BuildMatch | null> {
requestPath = requestPath.replace(/^\//, ''); requestPath = requestPath.replace(/^\//, '');
let bestIndexMatch: undefined | BuildMatch;
for (const match of matches.values()) { for (const match of matches.values()) {
if (await shouldServe(match, files, requestPath, devServer, isFilesystem)) { if (await shouldServe(match, files, requestPath, devServer, isFilesystem)) {
return match; if (!isIndex(match.src)) {
return match;
} else {
// if isIndex === true and ends in .html, we're done. Otherwise, keep searching
bestIndexMatch = match;
if (extname(match.src) === '.html') {
return bestIndexMatch;
}
}
} }
} }
return null;
// return a non-.html index file or none are found
return bestIndexMatch || null;
} }
async function shouldServe( async function shouldServe(
@@ -2216,3 +2264,11 @@ function hasNewRoutingProperties(nowConfig: NowConfig) {
typeof nowConfig.trailingSlash !== undefined typeof nowConfig.trailingSlash !== undefined
); );
} }
function buildMatchEquals(a?: BuildMatch, b?: BuildMatch): boolean {
if (!a || !b) return false;
if (a.src !== b.src) return false;
if (a.use !== b.use) return false;
if (!deepEqual(a.config || {}, b.config || {})) return false;
return true;
}

View File

@@ -44,22 +44,16 @@
<p> <p>
<ul> <ul>
<li> <li>
If you are a visitor; contact the website owner or try again later. Check the logs in your terminal window to see the application error.
</li>
<li>
If you are the owner; <a href="/_logs" target="_blank">check the logs</a> for the application error.
</li> </li>
</ul> </ul>
<a target="_blank" href="https://vercel.com/docs/router-status/{{= it.http_status_code }}" class="docs-link" rel="noopener noreferrer">Developer Documentation →</a> <a target="_blank" href="https://vercel.com/docs/error/application/{{= it.error_code }}" class="docs-link" rel="noopener noreferrer">Developer Documentation →</a>
</p> </p>
{{??}} {{??}}
<p> <p>
<ul> <ul>
<li> <li>
If you are a visitor, please try again later. Please open a <a target="_blank" href="https://github.com/vercel/vercel/issues/new/choose">GitHub issue</a> describing the problem you are experiencing with <code>vercel dev</code>.
</li>
<li>
If you are the owner, no action is needed. Our engineers have been notified.
</li> </li>
</ul> </ul>
</p> </p>

View File

@@ -1,5 +1,6 @@
import bytes from 'bytes'; import bytes from 'bytes';
import { Response } from 'node-fetch'; import { Response } from 'node-fetch';
import { NowBuildError } from '@vercel/build-utils';
import { NowError } from './now-error'; import { NowError } from './now-error';
import code from './output/code'; import code from './output/code';
import { getCommandName } from './pkg-name'; import { getCommandName } from './pkg-name';
@@ -771,6 +772,20 @@ export class CantParseJSONFile extends NowError<
} }
} }
export class ConflictingConfigFiles extends NowBuildError {
files: string[];
constructor(files: string[]) {
super({
code: 'CONFLICTING_CONFIG_FILES',
message:
'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.',
link: 'https://vercel.link/combining-old-and-new-config',
});
this.files = files;
}
}
export class CantFindConfig extends NowError< export class CantFindConfig extends NowError<
'CANT_FIND_CONFIG', 'CANT_FIND_CONFIG',
{ paths: string[] } { paths: string[] }

View File

@@ -3,6 +3,7 @@ import { fileNameSymbol } from '@vercel/client';
import { import {
CantParseJSONFile, CantParseJSONFile,
CantFindConfig, CantFindConfig,
ConflictingConfigFiles,
WorkingDirectoryDoesNotExist, WorkingDirectoryDoesNotExist,
} from './errors-ts'; } from './errors-ts';
import humanizePath from './humanize-path'; import humanizePath from './humanize-path';
@@ -49,28 +50,31 @@ export default async function getConfig(
} }
} }
// Then try with vercel.json in the same directory // Then try with `vercel.json` or `now.json` in the same directory
const vercelFilePath = path.resolve(localPath, 'vercel.json'); const vercelFilePath = path.resolve(localPath, 'vercel.json');
const vercelConfig = await readJSONFile(vercelFilePath); const nowFilePath = path.resolve(localPath, 'now.json');
const [vercelConfig, nowConfig] = await Promise.all([
readJSONFile(vercelFilePath),
readJSONFile(nowFilePath),
]);
if (vercelConfig instanceof CantParseJSONFile) { if (vercelConfig instanceof CantParseJSONFile) {
return vercelConfig; return vercelConfig;
} }
if (nowConfig instanceof CantParseJSONFile) {
return nowConfig;
}
if (vercelConfig && nowConfig) {
return new ConflictingConfigFiles([vercelFilePath, nowFilePath]);
}
if (vercelConfig !== null) { if (vercelConfig !== null) {
output.debug(`Found config in file ${vercelFilePath}`); output.debug(`Found config in file "${vercelFilePath}"`);
config = vercelConfig as NowConfig; config = vercelConfig as NowConfig;
config[fileNameSymbol] = 'vercel.json'; config[fileNameSymbol] = 'vercel.json';
return config; return config;
} }
if (nowConfig !== null) {
// Then try with now.json in the same directory output.debug(`Found config in file "${nowFilePath}"`);
const nowFilePath = path.resolve(localPath, 'now.json'); config = nowConfig as NowConfig;
const mainConfig = await readJSONFile(nowFilePath);
if (mainConfig instanceof CantParseJSONFile) {
return mainConfig;
}
if (mainConfig !== null) {
output.debug(`Found config in file ${nowFilePath}`);
config = mainConfig as NowConfig;
config[fileNameSymbol] = 'now.json'; config[fileNameSymbol] = 'now.json';
return config; return config;
} }

View File

@@ -2,6 +2,7 @@ import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import { format } from 'util'; import { format } from 'util';
import { Console } from 'console'; import { Console } from 'console';
import renderLink from './link';
import wait from './wait'; import wait from './wait';
export type Output = ReturnType<typeof createOutput>; export type Output = ReturnType<typeof createOutput>;
@@ -41,7 +42,7 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
boxen( boxen(
chalk.bold.yellow('WARN! ') + chalk.bold.yellow('WARN! ') +
str + str +
(details ? `\nMore details: ${details}` : ''), (details ? `\nMore details: ${renderLink(details)}` : ''),
{ {
padding: { padding: {
top: 0, top: 0,
@@ -64,16 +65,21 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
function error( function error(
str: string, str: string,
slug: string | null = null, slug?: string,
link: string | null = null link?: string,
action = 'More details'
) { ) {
print(`${chalk.red(`Error!`)} ${str}\n`); print(`${chalk.red(`Error!`)} ${str}\n`);
const details = slug ? `https://err.sh/now/${slug}` : link; const details = slug ? `https://err.sh/now/${slug}` : link;
if (details) { if (details) {
print(`More details: ${details}\n`); print(`${chalk.bold(action)}: ${renderLink(details)}\n`);
} }
} }
function prettyError(err: Error & { link?: string; action?: string }) {
return error(err.message, undefined, err.link, err.action);
}
function ready(str: string) { function ready(str: string) {
print(`${chalk.cyan('> Ready!')} ${str}\n`); print(`${chalk.cyan('> Ready!')} ${str}\n`);
} }
@@ -106,8 +112,6 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
return wait(message, delay); return wait(message, delay);
} }
// This is pretty hacky, but since we control the version of Node.js
// being used because of `pkg` it's safe to do in this case.
const c = { const c = {
_times: new Map(), _times: new Map(),
log(a: string, ...args: string[]) { log(a: string, ...args: string[]) {
@@ -134,6 +138,7 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
log, log,
warn, warn,
error, error,
prettyError,
ready, ready,
success, success,
debug, debug,

View File

@@ -1,6 +1,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import { metrics, shouldCollectMetrics } from '../metrics'; import { metrics, shouldCollectMetrics } from '../metrics';
import { APIError } from '../errors-ts'; import { APIError } from '../errors-ts';
import renderLink from './link';
const metric = metrics(); const metric = metrics();
@@ -9,10 +10,9 @@ export default function error(...input: string[] | [APIError]) {
if (typeof input[0] === 'object') { if (typeof input[0] === 'object') {
const { slug, message, link } = input[0]; const { slug, message, link } = input[0];
messages = [message]; messages = [message];
if (slug) { const details = slug ? `https://err.sh/now/${slug}` : link;
messages.push(`> More details: https://err.sh/now/${slug}`); if (details) {
} else if (link) { messages.push(`${chalk.bold('More details')}: ${renderLink(details)}`);
messages.push(`> More details: ${link}`);
} }
} }

View File

@@ -14,7 +14,7 @@ import chalk from 'chalk';
import { prependEmoji, emoji } from '../emoji'; import { prependEmoji, emoji } from '../emoji';
import AJV from 'ajv'; import AJV from 'ajv';
import { isDirectory } from '../config/global-path'; import { isDirectory } from '../config/global-path';
import { getPlatformEnv } from '@vercel/build-utils'; import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
const readFile = promisify(fs.readFile); const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile); const writeFile = promisify(fs.writeFile);
@@ -42,10 +42,21 @@ const linkSchema = {
/** /**
* Returns the `<cwd>/.vercel` directory for the current project * Returns the `<cwd>/.vercel` directory for the current project
* with a fallback to <cwd>/.now` if it exists. * with a fallback to <cwd>/.now` if it exists.
*
* Throws an error if *both* `.vercel` and `.now` directories exist.
*/ */
export function getVercelDirectory(cwd: string = process.cwd()) { export function getVercelDirectory(cwd: string = process.cwd()): string {
const possibleDirs = [join(cwd, VERCEL_DIR), join(cwd, VERCEL_DIR_FALLBACK)]; const possibleDirs = [join(cwd, VERCEL_DIR), join(cwd, VERCEL_DIR_FALLBACK)];
return possibleDirs.find(d => isDirectory(d)) || possibleDirs[0]; const existingDirs = possibleDirs.filter(d => isDirectory(d));
if (existingDirs.length > 1) {
throw new NowBuildError({
code: 'CONFLICTING_CONFIG_DIRECTORIES',
message:
'Both `.vercel` and `.now` directories exist. Please remove the `.now` directory.',
link: 'https://vercel.link/combining-old-and-new-config',
});
}
return existingDirs[0] || possibleDirs[0];
} }
async function getLink(path?: string): Promise<ProjectLink | null> { async function getLink(path?: string): Promise<ProjectLink | null> {

View File

@@ -82,7 +82,7 @@ export default async function validatePaths(
if (isFile) { if (isFile) {
output.print( output.print(
`${prependEmoji( `${prependEmoji(
'Deploying files with Vercel is deprecated (https://zeit.ink/3Z)', 'Deploying files with Vercel is deprecated (https://vercel.link/faq-deploy-file)',
emoji('warning') emoji('warning')
)}\n` )}\n`
); );

View File

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

View File

@@ -0,0 +1,11 @@
import React from 'react';
function Custom404() {
return (
<main>
<h1>Custom Gatsby 404</h1>
</main>
);
}
export default Custom404;

View File

@@ -0,0 +1,3 @@
export default function Custom404() {
return <h1>Custom Next 404</h1>;
}

View File

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

View File

@@ -0,0 +1,3 @@
export default function(req, res) {
res.end('Force "module: commonjs" JavaScript with ES Modules API endpoint');
}

View File

@@ -0,0 +1,5 @@
import { IncomingMessage, ServerResponse } from 'http';
export default function(req: IncomingMessage, res: ServerResponse) {
res.end('Force "module: commonjs" TypeScript API endpoint');
}

View File

@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

View File

@@ -0,0 +1,19 @@
{
"name": "force-module-commonjs",
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^9.3.4",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"@types/node": "14.0.9",
"@types/react": "^16.9.32",
"typescript": "^3.8.3"
}
}

View File

@@ -0,0 +1,3 @@
export default function () {
return <div>Force "module: commonjs" test page</div>;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
This is index.css

View File

@@ -0,0 +1,2 @@
This is index.html

View File

@@ -0,0 +1,3 @@
export default (req, res) => {
res.send(process.env);
};

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
import { IncomingMessage, ServerResponse } from 'http';
export default function(req: IncomingMessage, res: ServerResponse) {
res.end('Nested `tsconfig.json` API endpoint');
}

View File

@@ -0,0 +1,6 @@
{
"name": "api",
"devDependencies": {
"@types/node": "12"
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"module": "CommonJS",
"esModuleInterop": true
},
"exclude": ["node_modules"],
"include": ["**/*.ts"]
}

View File

@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@12":
version "12.12.43"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.43.tgz#b60ce047822e526e7a9252e50844eee79d5386ff"
integrity sha512-KUyZdkGCnVPuXfsKmDUu2XLui65LZIJ2s0M57noy5e+ixUT2oK33ep7zlvgzI8LElcWqbf8AR+o/3GqAPac2zA==

View File

@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

View File

@@ -0,0 +1,19 @@
{
"name": "nested-tsconfig",
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^9.3.4",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"@types/node": "14.0.9",
"@types/react": "^16.9.32",
"typescript": "^3.8.3"
}
}

View File

@@ -0,0 +1,3 @@
export default function () {
return <div>Nested tsconfig.json test page</div>;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
export default (req, res) => {
const hasHelpers = typeof req.query !== 'undefined';
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
hasHelpers,
query: req.query,
})
);
};

View File

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

View File

@@ -0,0 +1 @@
!public

View File

@@ -0,0 +1 @@
Custom 404 Page

View File

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

View File

@@ -0,0 +1,3 @@
module.exports = (_req, res) => {
res.end('Hello');
};

View File

@@ -0,0 +1 @@
Exact Custom 404

View File

@@ -0,0 +1 @@
Home Page

View File

@@ -0,0 +1 @@
Custom User 404

View File

@@ -0,0 +1,8 @@
{
"version": 2,
"routes": [
{ "src": "/exact", "status": 404, "dest": "/exact-404.html" },
{ "handle": "filesystem" },
{ "src": "/(.*)", "status": 404, "dest": "/user-404.html" }
]
}

View File

@@ -0,0 +1,3 @@
{
"builds": [{ "src": "*.js", "use": "@vercel/does-not-exist" }]
}

View File

@@ -0,0 +1 @@
Custom 404 Page

View File

@@ -0,0 +1 @@
The about page

View File

@@ -0,0 +1 @@
Contact Me Subdirectory

View File

@@ -0,0 +1 @@
This is the home page

View File

@@ -0,0 +1,3 @@
{
"cleanUrls": true
}

View File

@@ -0,0 +1 @@
Custom 404 Page

View File

@@ -0,0 +1 @@
The about page

View File

@@ -0,0 +1 @@
Contact Subdirectory

View File

@@ -0,0 +1 @@
This is the home page

View File

@@ -0,0 +1,4 @@
{
"version": 2,
"trailingSlash": true
}

View File

@@ -161,7 +161,7 @@ async function testPath(
Object.entries(headers).forEach(([key, expectedValue]) => { Object.entries(headers).forEach(([key, expectedValue]) => {
let actualValue = res.headers.get(key); let actualValue = res.headers.get(key);
if (key.toLowerCase() === 'location' && actualValue === '//') { if (key.toLowerCase() === 'location' && actualValue === '//') {
// HACK: `node-fetch` has strang behavior for location header so fix it // HACK: `node-fetch` has strange behavior for location header so fix it
// with `manual-dont-change` opt and convert double slash to single. // with `manual-dont-change` opt and convert double slash to single.
// See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352 // See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352
actualValue = '/'; actualValue = '/';
@@ -187,20 +187,29 @@ async function testFixture(directory, opts = {}, args = []) {
} }
); );
const stdoutList = []; let stdout = '';
const stderrList = []; let stderr = '';
const readyResolver = createResolver();
const exitResolver = createResolver(); const exitResolver = createResolver();
dev.stderr.on('data', data => stderrList.push(Buffer.from(data))); dev.stdout.setEncoding('utf8');
dev.stdout.on('data', data => stdoutList.push(Buffer.from(data))); dev.stderr.setEncoding('utf8');
dev.stdout.on('data', data => {
stdout += data;
});
dev.stderr.on('data', data => {
stderr += data;
if (stderr.includes('Ready! Available at')) {
readyResolver.resolve();
}
});
let printedOutput = false; let printedOutput = false;
dev.on('exit', () => { dev.on('exit', () => {
if (!printedOutput) { if (!printedOutput) {
const stdout = Buffer.concat(stdoutList).toString();
const stderr = Buffer.concat(stderrList).toString();
printOutput(directory, stdout, stderr); printOutput(directory, stdout, stderr);
printedOutput = true; printedOutput = true;
} }
@@ -209,8 +218,6 @@ async function testFixture(directory, opts = {}, args = []) {
dev.on('error', () => { dev.on('error', () => {
if (!printedOutput) { if (!printedOutput) {
const stdout = Buffer.concat(stdoutList).toString();
const stderr = Buffer.concat(stderrList).toString();
printOutput(directory, stdout, stderr); printOutput(directory, stdout, stderr);
printedOutput = true; printedOutput = true;
} }
@@ -226,6 +233,7 @@ async function testFixture(directory, opts = {}, args = []) {
return { return {
dev, dev,
port, port,
readyResolver,
}; };
} }
@@ -267,14 +275,12 @@ function testFixtureStdio(
await runNpmInstall(cwd); await runNpmInstall(cwd);
const stdoutList = []; let stdout = '';
const stderrList = []; let stderr = '';
const readyResolver = createResolver(); const readyResolver = createResolver();
const exitResolver = createResolver(); const exitResolver = createResolver();
try { try {
let stderr = '';
let printedOutput = false; let printedOutput = false;
const env = skipDeploy const env = skipDeploy
@@ -285,17 +291,19 @@ function testFixtureStdio(
env, env,
}); });
dev.stdout.setEncoding('utf8');
dev.stderr.setEncoding('utf8');
dev.stdout.pipe(process.stdout); dev.stdout.pipe(process.stdout);
dev.stderr.pipe(process.stderr); dev.stderr.pipe(process.stderr);
dev.stdout.on('data', data => { dev.stdout.on('data', data => {
stdoutList.push(data); stdout += data;
}); });
dev.stderr.on('data', data => { dev.stderr.on('data', data => {
stderrList.push(data); stderr += data;
stderr += data.toString();
if (stderr.includes('Ready! Available at')) { if (stderr.includes('Ready! Available at')) {
readyResolver.resolve(); readyResolver.resolve();
} }
@@ -315,8 +323,6 @@ function testFixtureStdio(
dev.on('exit', () => { dev.on('exit', () => {
if (!printedOutput) { if (!printedOutput) {
const stdout = Buffer.concat(stdoutList).toString();
const stderr = Buffer.concat(stderrList).toString();
printOutput(directory, stdout, stderr); printOutput(directory, stdout, stderr);
printedOutput = true; printedOutput = true;
} }
@@ -325,8 +331,6 @@ function testFixtureStdio(
dev.on('error', () => { dev.on('error', () => {
if (!printedOutput) { if (!printedOutput) {
const stdout = Buffer.concat(stdoutList).toString();
const stderr = Buffer.concat(stderrList).toString();
printOutput(directory, stdout, stderr); printOutput(directory, stdout, stderr);
printedOutput = true; printedOutput = true;
} }
@@ -375,6 +379,153 @@ test.afterEach(async () => {
); );
}); });
test('[vercel dev] prints `npm install` errors', async t => {
const dir = fixture('runtime-not-installed');
const result = await exec(dir);
t.truthy(result.stderr.includes('npm ERR! 404'));
t.truthy(
result.stderr.includes('Failed to install `vercel dev` dependencies')
);
t.truthy(
result.stderr.includes('https://vercel.link/npm-install-failed-dev')
);
});
test('[vercel dev] `vercel.json` should be invalidated if deleted', async t => {
const dir = fixture('invalidate-vercel-config');
const configPath = join(dir, 'vercel.json');
const originalConfig = await fs.readJSON(configPath);
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
{
// Env var should be set from `vercel.json`
const res = await fetch(`http://localhost:${port}/api`);
const body = await res.json();
t.is(body.FOO, 'bar');
}
{
// Env var should not be set after `vercel.json` is deleted
await fs.remove(configPath);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/api`);
const body = await res.json();
t.is(body.FOO, undefined);
}
} finally {
await dev.kill('SIGTERM');
await fs.writeJSON(configPath, originalConfig);
}
});
test('[vercel dev] reflects changes to config and env without restart', async t => {
const dir = fixture('node-helpers');
const configPath = join(dir, 'vercel.json');
const originalConfig = await fs.readJSON(configPath);
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
{
// Node.js helpers should be available by default
const res = await fetch(`http://localhost:${port}/?foo=bar`);
const body = await res.json();
t.is(body.hasHelpers, true);
t.is(body.query.foo, 'bar');
}
{
// Disable the helpers via `config.helpers = false`
const config = {
...originalConfig,
builds: [
{
...originalConfig.builds[0],
config: {
helpers: false,
},
},
],
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=bar`);
const body = await res.json();
t.is(body.hasHelpers, false);
t.is(body.query, undefined);
}
{
// Enable the helpers via `config.helpers = true`
const config = {
...originalConfig,
builds: [
{
...originalConfig.builds[0],
config: {
helpers: true,
},
},
],
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=baz`);
const body = await res.json();
t.is(body.hasHelpers, true);
t.is(body.query.foo, 'baz');
}
{
// Disable the helpers via `NODEJS_HELPERS = '0'`
const config = {
...originalConfig,
build: {
env: {
NODEJS_HELPERS: '0',
},
},
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=baz`);
const body = await res.json();
t.is(body.hasHelpers, false);
t.is(body.query, undefined);
}
{
// Enable the helpers via `NODEJS_HELPERS = '1'`
const config = {
...originalConfig,
build: {
env: {
NODEJS_HELPERS: '1',
},
},
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=boo`);
const body = await res.json();
t.is(body.hasHelpers, true);
t.is(body.query.foo, 'boo');
}
} finally {
await dev.kill('SIGTERM');
await fs.writeJSON(configPath, originalConfig);
}
});
test( test(
'[vercel dev] validate routes that use `check: true`', '[vercel dev] validate routes that use `check: true`',
testFixtureStdio('routes-check-true', async testPath => { testFixtureStdio('routes-check-true', async testPath => {
@@ -391,6 +542,17 @@ test(
}) })
); );
test(
'[vercel dev] validate routes that use custom 404 page',
testFixtureStdio('routes-custom-404', async testPath => {
await testPath(200, '/', 'Home Page');
await testPath(404, '/nothing', 'Custom User 404');
await testPath(404, '/exact', 'Exact Custom 404');
await testPath(200, '/api/hello', 'Hello');
await testPath(404, '/api/nothing', 'Custom User 404');
})
);
test( test(
'[vercel dev] handles miss after route', '[vercel dev] handles miss after route',
testFixtureStdio('handle-miss-after-route', async testPath => { testFixtureStdio('handle-miss-after-route', async testPath => {
@@ -477,7 +639,8 @@ test(
await testPath(200, '/api/date', /current date/); await testPath(200, '/api/date', /current date/);
await testPath(200, '/api/rand', /random number/); await testPath(200, '/api/rand', /random number/);
await testPath(200, '/api/rand.js', /random number/); await testPath(200, '/api/rand.js', /random number/);
await testPath(404, '/api/api'); await testPath(404, '/api/api', /NOT_FOUND/m);
await testPath(404, '/nothing', /Custom 404 Page/);
}) })
); );
@@ -573,7 +736,11 @@ test('[vercel dev] validate mixed routes and rewrites', async t => {
const output = await exec(directory); const output = await exec(directory);
t.is(output.exitCode, 1, formatOutput(output)); t.is(output.exitCode, 1, formatOutput(output));
t.regex(output.stderr, /Cannot define both `routes` and `rewrites`/m); t.regex(
output.stderr,
/If `rewrites`, `redirects`, `headers`, `cleanUrls` or `trailingSlash` are used, then `routes` cannot be present./m
);
t.regex(output.stderr, /vercel\.link\/mix-routing-props/m);
}); });
// Test seems unstable: It won't return sometimes. // Test seems unstable: It won't return sometimes.
@@ -672,6 +839,17 @@ test(
}) })
); );
test(
'[vercel dev] should serve custom 404 when `cleanUrls: true`',
testFixtureStdio('test-clean-urls-custom-404', async testPath => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about', 'The about page');
await testPath(200, '/contact/me', 'Contact Me Subdirectory');
await testPath(404, '/nothing', 'Custom 404 Page');
await testPath(404, '/nothing/', 'Custom 404 Page');
})
);
test( test(
'[vercel dev] test cleanUrls and trailingSlash serve correct content', '[vercel dev] test cleanUrls and trailingSlash serve correct content',
testFixtureStdio('test-clean-urls-trailing-slash', async testPath => { testFixtureStdio('test-clean-urls-trailing-slash', async testPath => {
@@ -740,6 +918,16 @@ test(
}) })
); );
test(
'[vercel dev] should serve custom 404 when `trailingSlash: true`',
testFixtureStdio('test-trailing-slash-custom-404', async testPath => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about.html', 'The about page');
await testPath(200, '/contact/', 'Contact Subdirectory');
await testPath(404, '/nothing/', 'Custom 404 Page');
})
);
test( test(
'[vercel dev] test trailingSlash false serve correct content', '[vercel dev] test trailingSlash false serve correct content',
testFixtureStdio('test-trailing-slash-false', async testPath => { testFixtureStdio('test-trailing-slash-false', async testPath => {
@@ -772,7 +960,11 @@ test(
testFixtureStdio( testFixtureStdio(
'invalid-builder-routes', 'invalid-builder-routes',
async testPath => { async testPath => {
await testPath(500, '/', /Invalid regular expression/m); await testPath(
500,
'/',
/Route at index 0 has invalid `src` regular expression/m
);
}, },
{ skipDeploy: true } { skipDeploy: true }
) )
@@ -897,6 +1089,8 @@ test(
'[vercel dev] 10-nextjs-node', '[vercel dev] 10-nextjs-node',
testFixtureStdio('10-nextjs-node', async testPath => { testFixtureStdio('10-nextjs-node', async testPath => {
await testPath(200, '/', /Next.js \+ Node.js API/m); await testPath(200, '/', /Next.js \+ Node.js API/m);
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
await testPath(404, '/nothing', /Custom Next 404/);
}) })
); );
@@ -1339,3 +1533,36 @@ test(
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m); await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
}) })
); );
test(
'[vercel dev] Should work with nested `tsconfig.json` files',
testFixtureStdio('nested-tsconfig', async testPath => {
await testPath(200, `/`, /Nested tsconfig.json test page/);
await testPath(200, `/api`, 'Nested `tsconfig.json` API endpoint');
})
);
test(
'[vercel dev] Should force `tsc` option "module: commonjs" for `startDevServer()`',
testFixtureStdio('force-module-commonjs', async testPath => {
await testPath(200, `/`, /Force &quot;module: commonjs&quot; test page/);
await testPath(
200,
`/api`,
'Force "module: commonjs" JavaScript with ES Modules API endpoint'
);
await testPath(
200,
`/api/ts`,
'Force "module: commonjs" TypeScript API endpoint'
);
})
);
test(
'[vercel dev] should prioritize index.html over other file named index.*',
testFixtureStdio('index-html-priority', async testPath => {
await testPath(200, '/', 'This is index.html');
await testPath(200, '/index.css', 'This is index.css');
})
);

View File

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

View File

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

View File

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

View File

@@ -514,6 +514,11 @@ CMD ["node", "index.js"]`,
], ],
}), }),
}, },
'conflicting-now-json-vercel-json': {
'index.html': '<h1>I am a website.</h1>',
'vercel.json': getConfigFile(true),
'now.json': getConfigFile(true),
},
}; };
for (const typeName of Object.keys(spec)) { for (const typeName of Object.keys(spec)) {

View File

@@ -1703,7 +1703,7 @@ test('ensure we are getting a warning for the old team flag', async t => {
// Ensure the warning is printed // Ensure the warning is printed
t.true( t.true(
stderr.includes( stderr.includes(
'WARN! The "--team" flag is deprecated. Please use "--scope" instead.' 'WARN! The "--team" option is deprecated. Please use "--scope" instead.'
) )
); );

View File

@@ -1589,7 +1589,7 @@ test('create a staging deployment', async t => {
/Setting target to staging/gm, /Setting target to staging/gm,
formatOutput(targetCall) formatOutput(targetCall)
); );
t.regex(targetCall.stdout, /https:\/\//gm);
t.is(targetCall.exitCode, 0, formatOutput(targetCall)); t.is(targetCall.exitCode, 0, formatOutput(targetCall));
const { host } = new URL(targetCall.stdout); const { host } = new URL(targetCall.stdout);
@@ -1625,6 +1625,7 @@ test('create a production deployment', async t => {
/Setting target to production/gm, /Setting target to production/gm,
formatOutput(targetCall) formatOutput(targetCall)
); );
t.regex(targetCall.stdout, /https:\/\//gm);
const { host: targetHost } = new URL(targetCall.stdout); const { host: targetHost } = new URL(targetCall.stdout);
const targetDeployment = await apiFetch( const targetDeployment = await apiFetch(
@@ -1648,6 +1649,7 @@ test('create a production deployment', async t => {
/Setting target to production/gm, /Setting target to production/gm,
formatOutput(targetCall) formatOutput(targetCall)
); );
t.regex(call.stdout, /https:\/\//gm);
const { host } = new URL(call.stdout); const { host } = new URL(call.stdout);
const deployment = await apiFetch( const deployment = await apiFetch(
@@ -2554,7 +2556,7 @@ test('should prefill "project name" prompt with --name', async t => {
let isDeprecated = false; let isDeprecated = false;
await waitForPrompt(now, chunk => { await waitForPrompt(now, chunk => {
if (chunk.includes('The "--name" flag is deprecated')) { if (chunk.includes('The "--name" option is deprecated')) {
isDeprecated = true; isDeprecated = true;
} }
@@ -2895,3 +2897,40 @@ test('deploys with only vercel.json and README.md', async t => {
const text = await res.text(); const text = await res.text();
t.regex(text, /readme contents/); t.regex(text, /readme contents/);
}); });
test('reject conflicting `vercel.json` and `now.json` files', async t => {
const directory = fixture('conflicting-now-json-vercel-json');
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--confirm'],
{
cwd: directory,
reject: false,
}
);
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
t.true(
stderr.includes(
'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.'
),
formatOutput({ stderr, stdout })
);
});
test('`vc --debug project ls` should output the projects listing', async t => {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--debug', 'project', 'ls'],
{
reject: false,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.true(
stdout.includes('> Projects found under'),
formatOutput({ stderr, stdout })
);
});

View File

@@ -1,4 +1,4 @@
import { join, sep } from 'path'; import { basename, join, sep } from 'path';
import { send } from 'micro'; import { send } from 'micro';
import test from 'ava'; import test from 'ava';
import sinon from 'sinon'; import sinon from 'sinon';
@@ -24,6 +24,7 @@ import { isValidName } from '../src/util/is-valid-name';
import preferV2Deployment from '../src/util/prefer-v2-deployment'; import preferV2Deployment from '../src/util/prefer-v2-deployment';
import getUpdateCommand from '../src/util/get-update-command'; import getUpdateCommand from '../src/util/get-update-command';
import { isCanary } from '../src/util/is-canary'; import { isCanary } from '../src/util/is-canary';
import { getVercelDirectory } from '../src/util/projects/link';
const output = createOutput({ debug: false }); const output = createOutput({ debug: false });
const prefix = `${join(__dirname, 'fixtures', 'unit')}${sep}`; const prefix = `${join(__dirname, 'fixtures', 'unit')}${sep}`;
@@ -1091,3 +1092,29 @@ test('detect update command', async t => {
const updateCommand = await getUpdateCommand(); const updateCommand = await getUpdateCommand();
t.is(updateCommand, `yarn add vercel@${isCanary() ? 'canary' : 'latest'}`); t.is(updateCommand, `yarn add vercel@${isCanary() ? 'canary' : 'latest'}`);
}); });
test('`getVercelDirectory()` returns ".vercel"', t => {
const cwd = fixture('get-vercel-directory');
const dir = getVercelDirectory(cwd);
t.is(basename(dir), '.vercel');
});
test('`getVercelDirectory()` returns ".now"', t => {
const cwd = fixture('get-vercel-directory-legacy');
const dir = getVercelDirectory(cwd);
t.is(basename(dir), '.now');
});
test('`getVercelDirectory()` throws an error if ".vercel" and ".now" exist', t => {
let err;
const cwd = fixture('get-vercel-directory-error');
try {
getVercelDirectory(cwd);
} catch (_err) {
err = _err;
}
t.is(
err.message,
'Both `.vercel` and `.now` directories exist. Please remove the `.now` directory.'
);
});

View File

@@ -3,4 +3,6 @@ lib
node_modules node_modules
*.log *.log
!tests/fixtures/nowignore/node_modules !tests/fixtures/nowignore/node_modules
!tests/fixtures/vercelignore-allow-nodemodules/node_modules
!tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/client", "name": "@vercel/client",
"version": "8.0.2-canary.1", "version": "8.1.0",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"homepage": "https://vercel.com", "homepage": "https://vercel.com",
@@ -27,7 +27,7 @@
"@types/node-fetch": "2.5.4", "@types/node-fetch": "2.5.4",
"@types/recursive-readdir": "2.2.0", "@types/recursive-readdir": "2.2.0",
"@zeit/ncc": "0.18.5", "@zeit/ncc": "0.18.5",
"typescript": "3.5.1" "typescript": "3.9.3"
}, },
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",

View File

@@ -93,6 +93,16 @@ export async function* checkDeploymentStatus(
} }
if (isAliasAssigned(deploymentUpdate)) { if (isAliasAssigned(deploymentUpdate)) {
if (
deploymentUpdate.aliasWarning &&
deploymentUpdate.aliasWarning.message
) {
yield {
type: 'warning',
payload: deploymentUpdate.aliasWarning.message,
};
}
debug('Deployment alias assigned'); debug('Deployment alias assigned');
return yield { type: 'alias-assigned', payload: deploymentUpdate }; return yield { type: 'alias-assigned', payload: deploymentUpdate };
} }

View File

@@ -3,7 +3,7 @@ import { readdir as readRootFolder, lstatSync } from 'fs-extra';
import { relative, isAbsolute, basename } from 'path'; import { relative, isAbsolute, basename } from 'path';
import hashes, { mapToObject } from './utils/hashes'; import hashes, { mapToObject } from './utils/hashes';
import { upload } from './upload'; import { upload } from './upload';
import { buildFileTree, createDebug, parseNowJSON } from './utils'; import { buildFileTree, createDebug, parseVercelConfig } from './utils';
import { DeploymentError } from './errors'; import { DeploymentError } from './errors';
import { import {
NowConfig, NowConfig,
@@ -85,10 +85,24 @@ export default function buildCreateDeployment(version: number) {
let configPath: string | undefined; let configPath: string | undefined;
if (!nowConfig) { if (!nowConfig) {
// If the user did not provide a config file, use the one in the root directory. // If the user did not provide a config file, use the one in the root directory.
configPath = fileList const relativePaths = fileList.map(f => relative(cwd, f));
.map(f => relative(cwd, f)) const hasVercelConfig = relativePaths.includes('vercel.json');
.find(f => f === 'vercel.json' || f === 'now.json'); const hasNowConfig = relativePaths.includes('now.json');
nowConfig = await parseNowJSON(configPath);
if (hasVercelConfig) {
if (hasNowConfig) {
throw new DeploymentError({
code: 'conflicting_config',
message:
'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.',
});
}
configPath = 'vercel.json';
} else if (hasNowConfig) {
configPath = 'now.json';
}
nowConfig = await parseVercelConfig(configPath);
} }
if ( if (

View File

@@ -7,6 +7,7 @@ import qs from 'querystring';
import ignore from 'ignore'; import ignore from 'ignore';
type Ignore = ReturnType<typeof ignore>; type Ignore = ReturnType<typeof ignore>;
import { pkgVersion } from '../pkg'; import { pkgVersion } from '../pkg';
import { NowBuildError } from '@vercel/build-utils';
import { NowClientOptions, DeploymentOptions, NowConfig } from '../types'; import { NowClientOptions, DeploymentOptions, NowConfig } from '../types';
import { Sema } from 'async-sema'; import { Sema } from 'async-sema';
import { readFile } from 'fs-extra'; import { readFile } from 'fs-extra';
@@ -48,10 +49,10 @@ export function getApiDeploymentsUrl(
return '/v10/now/deployments'; return '/v10/now/deployments';
} }
return '/v12/now/deployments'; return '/v13/now/deployments';
} }
export async function parseNowJSON(filePath?: string): Promise<NowConfig> { export async function parseVercelConfig(filePath?: string): Promise<NowConfig> {
if (!filePath) { if (!filePath) {
return {}; return {};
} }
@@ -76,43 +77,22 @@ const maybeRead = async function<T>(path: string, default_: T) {
} }
}; };
export async function readdirRelative(
path: string,
ignores: string[],
cwd: string
): Promise<string[]> {
const preprocessedIgnores = ignores.map(ignore => {
if (ignore.endsWith('/')) {
return ignore.slice(0, -1);
}
return ignore;
});
const dirContents = await readdir(path, preprocessedIgnores);
return dirContents.map(filePath => relative(cwd, filePath));
}
export async function buildFileTree( export async function buildFileTree(
path: string | string[], path: string | string[],
isDirectory: boolean, isDirectory: boolean,
debug: Debug debug: Debug
): Promise<string[]> { ): Promise<string[]> {
// Get .nowignore
let { ig, ignores } = await getVercelIgnore(path);
debug(`Found ${ig.ignores.length} rules in .nowignore`);
let fileList: string[]; let fileList: string[];
let { ig } = await getVercelIgnore(path);
debug(`Found ${ig.ignores.length} rules in .vercelignore`);
debug('Building file tree...'); debug('Building file tree...');
if (isDirectory && !Array.isArray(path)) { if (isDirectory && !Array.isArray(path)) {
// Directory path // Directory path
const cwd = process.cwd(); const cwd = process.cwd();
const relativeFileList = await readdirRelative(path, ignores, cwd); const ignores = (absPath: string) => ig.ignores(relative(cwd, absPath));
fileList = ig fileList = await readdir(path, [ignores]);
.filter(relativeFileList)
.map((relativePath: string) => join(cwd, relativePath));
debug(`Read ${fileList.length} files in the specified directory`); debug(`Read ${fileList.length} files in the specified directory`);
} else if (Array.isArray(path)) { } else if (Array.isArray(path)) {
// Array of file paths // Array of file paths
@@ -166,9 +146,12 @@ export async function getVercelIgnore(
maybeRead(join(cwd, '.nowignore'), ''), maybeRead(join(cwd, '.nowignore'), ''),
]); ]);
if (vercelignore && nowignore) { if (vercelignore && nowignore) {
throw new Error( throw new NowBuildError({
'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.' code: 'CONFLICTING_IGNORE_FILES',
); message:
'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.',
link: 'https://vercel.link/combining-old-and-new-config',
});
} }
return vercelignore || nowignore; return vercelignore || nowignore;
}) })

View File

@@ -0,0 +1 @@
EXCLUDE="this file should be ignored"

View File

@@ -0,0 +1,6 @@
# literally dont ignore the node_modules directory
# so basically include the node_modules directory recursively
!node_modules/
# ignore this file in addition to the defaults
exclude.txt

View File

@@ -0,0 +1 @@
Should be ignored

View File

@@ -0,0 +1 @@
Hello World

View File

@@ -0,0 +1 @@
Should be included

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