Compare commits

...

53 Commits

Author SHA1 Message Date
Steven
621b53bc49 Publish Stable
- @vercel/build-utils@5.0.0
 - vercel@26.0.0
 - @vercel/client@12.0.4
 - @vercel/fs-detectors@1.0.0
 - @vercel/go@2.0.4
 - @vercel/next@3.1.3
 - @vercel/node@2.4.0
 - @vercel/python@3.0.4
 - @vercel/redwood@1.0.5
 - @vercel/remix@1.0.5
 - @vercel/ruby@1.3.12
 - @vercel/static-build@1.0.4
2022-07-05 08:47:49 -04:00
JJ Kasper
728b620355 Update trailing slash handling for _next/data resolving (#8080) 2022-07-04 09:52:08 -05:00
Luc Leray
7d16395038 [build-utils] Stricter getNodeBinPath return type (#8082)
In https://github.com/vercel/vercel/pull/8058, I made `getNodeBinPath` return type too broad. The function can never return `undefined` so we can make it more strict.
2022-07-04 13:22:35 +00:00
Lee Robinson
59e1259688 [examples] Remove stray console.log. (#8070) 2022-07-01 22:47:36 +00:00
Nathan Rajlich
169242157e [cli] Fix Middleware "matchers" with query string in vc dev (#8069)
`matchers` config in Middleware was not being properly… matched… in `vc dev` when a query string was present.
2022-07-01 20:38:02 +00:00
Steven
db10ffd679 [build-utils][next][redwood][remix][static-build] Fix corepack path prepend (#8065)
This PR fixes a bug where corepack (#7871) was not correctly setup because the lockfile autodetection and node version autodetection was overriding the PATH.

It also fixes a bug where the log output was printed twice because we incorrectly prepended the PATH twice.
2022-07-01 19:33:21 +00:00
Steven
c0d0744c4e [tests] Add yarn.lock to saber test fixture (#8068)
The Saber example had a lock file but the test did not.

This PR copies the lock file since the latest version of Saber doesn't work with the latest Vue.

```
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package' is not defined by "exports" in /vercel/path0/node_modules/vue/package.json
  at new NodeError (node:internal/errors:372:5)
  at throwExportsNotFound (node:internal/modules/esm/resolve:472:9)
  at packageExportsResolve (node:internal/modules/esm/resolve:753:3)
  at resolveExports (node:internal/modules/cjs/loader:482:36)
  at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
  at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
  at Function.resolve (node:internal/modules/cjs/helpers:108:19)
  at module.exports (/vercel/path0/node_modules/saber/dist/webpack/webpack.config.js:30:58)
  at Saber.getWebpackConfig (/vercel/path0/node_modules/saber/dist/index.js:195:58)
  at VueRenderer.build (/vercel/path0/node_modules/saber/dist/vue-renderer/index.js:232:39)
```
2022-07-01 19:04:44 +00:00
Steven
9da67423a5 [node] Fix public TypeScript types (#8064)
There was a regression some time between [1.12.2-canary.6](https://unpkg.com/browse/@vercel/node@1.12.2-canary.6/dist/index.d.ts) and [1.12.2-canary.7](https://unpkg.com/browse/@vercel/node@1.12.2-canary.7/dist/index.d.ts) where the `index.d.ts` types changed.

This PR reverts back to the old behavior so users can properly use

```ts
import { VercelRequest, VercelResponse } from '@vercel/node';
```

The existing test was also changed to use the result of the build rather than copy at the beginning

cfae7ec3c2/packages/node/test/fixtures/15-helpers/ts/index.ts (L1)

- Fixes https://github.com/vercel/vercel/issues/7951
2022-06-30 22:45:49 +00:00
Steven
51fe09d5e9 [build-utils][fs-detectors][cli] MAJOR: move some of build-utils into new fs-detectors (#8054)
The `@vercel/build-utils` package was meant be shared functions necessary for writing a Vercel Builder (aka Runtime).

This package has since bloated into the catch-all package for anything that wasn't a Builder.

This PR removes the bloat in favor of a new package, `@vercel/fs-detectors`. It also removes the need for `@vercel/build-utils` to have a dependency on `@vercel/frameworks`.

- Related to #7951
2022-06-30 21:14:07 +00:00
Nathan Rajlich
695bfbdd60 [cli] Add full stdio mockability for unit tests (#8052)
This PR is a follow-up to #8039, which provides an intuitive syntax for writing unit tests for interactive CLI commands.

The heart of this is the new `await expect(stream).toOutput(test)` custom Jest matcher, which is intended for use with the mock Client `stdout` and `stderr` stream properties. The `test` is a string that will wait for the stream to output via "data" events until a match is found, or it will timeout (after 3 seconds by default). The timeout error has nice Jest-style formatting so that you can easily identify what was output:

<img width="553" alt="Screen Shot 2022-06-29 at 10 33 06 PM" src="https://user-images.githubusercontent.com/71256/176600324-cb1ebecb-e891-42d9-bdc9-4864d3594a8c.png">

Below is an example of a unit test that was added for an interactive `vc login` session:

```typescript
it('should allow login via email', async () => {
  const user = useUser();

  const exitCodePromise = login(client);

  // Wait for login interactive prompt
  await expect(client.stderr).toOutput(`> Log in to Vercel`);

  // Move down to "Email" option
  client.stdin.write('\x1B[B'); // Down arrow
  client.stdin.write('\x1B[B'); // Down arrow
  client.stdin.write('\x1B[B'); // Down arrow
  client.stdin.write('\r'); // Return key

  // Wait for email input prompt
  await expect(client.stderr).toOutput('> Enter your email address:');

  // Write user email address into prompt
  client.stdin.write(`${user.email}\n`);

  // Wait for login success message
  await expect(client.stderr).toOutput(
    `Success! Email authentication complete for ${user.email}`
  );

  // Assert that the `login()` command returned 0 exit code
  await expect(exitCodePromise).resolves.toEqual(0);
});
```

**Note:**  as a consequence of this PR, prompts are now written to stderr instead of stdout, so this change _may_ be considered a breaking change, in which case we should tag it for major release.
2022-06-30 20:17:22 +00:00
Nathan Rajlich
547e88228e Publish Stable
- @vercel/build-utils@4.2.1
 - vercel@25.2.3
 - @vercel/client@12.0.3
 - @vercel/go@2.0.3
 - @vercel/next@3.1.2
 - @vercel/node@2.3.3
 - @vercel/python@3.0.3
 - @vercel/redwood@1.0.4
 - @vercel/remix@1.0.4
 - @vercel/ruby@1.3.11
 - @vercel/static-build@1.0.3
2022-06-30 12:24:13 -07:00
Luc Leray
9bfb5dd535 [build-utils] Handle npm bin exit code 7 (#8058)
In some rare cases, `npm bin` exits with code 7, but still outputs the right bin path.

To reproduce, try:
```
npm init -y
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
vc
# enter "echo build" for the build command, leave the other configuration as default
```

The build will fail with `Error: Command exited with 7` because `npm bin` fails with code 7, for some reason.

In this PR, we do 2 things:
(1) Ignore exit codes from `npm bin`. It still outputs the right path when it exits with code 7 so we just read the output and check if it's a valid path.
(2) Throw a more specific error message when `npm bin` fails to give us the bin path. The current error was hard to debug because it looked like it was coming from the install commmand. We can do better by emitting a custom error.

Alternative considered for (2): Do not throw errors. If `npm bin` fails, emit a warning and let the build continue.

Related Issues:
- https://github.com/vercel/customer-issues/issues/585 (internal)
2022-06-30 17:27:52 +00:00
Nathan Rajlich
81ea84fae8 [cli] Fix vc build lambda serialization when there's a broken symlink (#8050)
There's some cleanup directory walking logic that was choking when
a Lambda outputs a file with a broken symlink. We shouldn't need to
traverse into those directories in the case of a symlink anyways, so use
`lstat()` instead of `stat()` to prevent that filesystem call from
throwing an error.
2022-06-29 16:04:31 -07:00
Nathan Rajlich
fa8bf07be4 [cli] Add Client#stdin / Client#stdout (#8039)
This will allow for mockability of the input streams (i.e. prompts)
for CLI commands in unit tests.

**Example:**

```typescript
import confirm from '../../src/util/input/confirm';
import { client } from '../mocks/client';

describe('MockClient', () => {
  it('should mock `confirm()`', async () => {
    const confirmedPromise = confirm(client, 'Do the thing?', false);

    client.stdin.write('yes\n');

    const confirmed = await confirmedPromise;
    expect(confirmed).toEqual(true);
  });
});
```
2022-06-29 16:03:56 -07:00
JJ Kasper
cc9dce73ad [next] Ensure uncompressed limit is correct (#8049)
* Revert "Revert "[next] Update max size warning to handle initial layer better" (#8047)"

This reverts commit 8c62de16ce.

* Ensure uncompressed limit is correct

* apply suggestion
2022-06-29 15:13:15 -05:00
Sean Massa
bba7cbd411 [cli][dev] fix: creating "api/some-func.js" after "vc dev" now works (#8041)
If there is no `api` directory, then you run `vc dev`, then you create a new function `api/some-func.js`, then this file would not be served as a new function.

This was being caused by incomplete "new file" handling logic. This PR ensures that the proper detection is done in each new file (`getVercelConfig`) that populates key properties (`apiDir`, `apiExtensions`, and extensionless `files`) for determining when a file is used to serve a request.
2022-06-29 18:37:48 +00:00
Steven
9a3739bebd Publish Stable
- vercel@25.2.2
 - @vercel/next@3.1.1
 - @vercel/node@2.3.2
 - @vercel/redwood@1.0.3
 - @vercel/remix@1.0.3
2022-06-29 09:27:34 -04:00
Gal Schlezinger
8c62de16ce Revert "[next] Update max size warning to handle initial layer better" (#8047)
Revert "[next] Update max size warning to handle initial layer better (#8013)"

This reverts commit f20703b15d.
2022-06-29 09:26:29 -04:00
Steven
e9333988d7 [next][node][redwood][remix] Bump @vercel/nft to 0.20.1 (#8042)
- https://github.com/vercel/nft/releases/tag/0.20.0
- https://github.com/vercel/nft/releases/tag/0.20.1
2022-06-29 00:21:14 +00:00
Nathan Rajlich
fb001ce7eb [tests] Remove TODO comments from Middleware matchers vc dev test (#8037) 2022-06-28 18:26:56 +00:00
Sean Massa
b399fe7037 Publish Stable
- vercel@25.2.1
 - @vercel/node@2.3.1
2022-06-28 12:04:57 -05:00
Sean Massa
88385b3c84 [cli][node] switch to esbuild for compiling edge functions (#8032) 2022-06-28 11:27:56 -05:00
Steven
eed39913e1 Publish Stable
- @vercel/build-utils@4.2.0
 - vercel@25.2.0
 - @vercel/client@12.0.2
 - @vercel/edge@0.0.1
 - @vercel/frameworks@1.0.2
 - @vercel/go@2.0.2
 - @vercel/next@3.1.0
 - @vercel/node@2.3.0
 - @vercel/python@3.0.2
 - @vercel/redwood@1.0.2
 - @vercel/remix@1.0.2
 - @vercel/routing-utils@1.13.5
 - @vercel/ruby@1.3.10
 - @vercel/static-build@1.0.2
2022-06-28 10:15:33 -04:00
Gal Schlezinger
03e9047bc9 [@vercel/edge] add header helpers (#8036)
* [@vercel/edge] add header helpers

* rename getIp => ipAddress, getGeo => geolocation, as suggested by @kikobeats
2022-06-28 11:26:18 +02:00
Nathan Rajlich
0e35205bf1 [cli][dev][node] Support matchers config for Middleware in vc dev (#8033)
Adds support for `config.matchers` exported property in Middleware during `vc dev`.
2022-06-28 08:34:48 +00:00
Steven
e42fe34c4a [tests] Bump turbo to 1.3.1 (#8011)
https://turborepo.org/blog/turbo-1-3-0
2022-06-28 04:48:35 +00:00
Sean Massa
3ece7ac969 [cli][node] make error handling of edge functions consistent with serverless functions in vc dev (#8007)
When edge functions error, they were showing a basic text response instead of the error template. They were also sending a 502 status code instead of a 500.

This PR makes the error handling of Edge Functions consistent with Serverless Functions when executed through `vc dev`. If we want to update the error templates themselves, we can do that in a separate PR.

**Note:** Production currently treats Edge Function errors differently from Serverless Function errors, but that's a known issue that will be resolved.

---

*I deleted the original outputs (terminal and browser screenshots) because they are out of date and added a lot of content to this page. See my latest comment for updated examples.*
2022-06-28 04:12:21 +00:00
Nathan Rajlich
4f832acf90 [remix] Don't depend on @remix-run/vercel (#8029)
Instead, just add it to the project's `package.json` file before installing the dependencies.

Fixes warning about missing peer dependencies when installing CLI.

<img width="409" alt="CleanShot 2022-05-22 at 09 40 09@2x" src="https://user-images.githubusercontent.com/71256/176084428-79e964b3-8b20-416d-bf3f-c5bd36f4b0ff.png">

Now, a warning is shown in the Deployment build logs, saying that the dep was added, but that the user should commit the change:

<img width="931" alt="Screen Shot 2022-06-27 at 8 15 19 PM" src="https://user-images.githubusercontent.com/71256/176084377-dab5f7d3-4e9f-4bf6-baee-63708b65f218.png">
2022-06-28 03:29:50 +00:00
Matthew Stanciu
918726e01d [cli] Support "http:" scheme in vc bisect (#8023)
`vc bisect` currently prepends `https://` to a passed-in url if it doesn't begin with https—which means that if someone passes in a url that begins with `http://`, it'll turn the url into `https://http://url.com`. This PR fixes this.

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-28 02:15:59 +00:00
JJ Kasper
dc2ddf867b Publish Stable
- @vercel/next@3.0.5
2022-06-27 15:37:09 -05:00
Nathan Rajlich
ee1211416f [cli] Support root-level Middleware file in vc dev (#7973)
Adds initial support for a root-level `middleware.js` / `middleware.ts` file in the `vercel dev` CLI command. This leverages the existing Edge Function invoking logic in `@vercel/node`'s `startDevServer()` function and applies the necessary response / rewrites / mutations to the HTTP request based on the result of the middleware invocation.
2022-06-27 19:56:32 +00:00
JJ Kasper
570fd24e29 Publish Canary
- vercel@25.1.1-canary.11
 - @vercel/edge@0.0.1-canary.0
 - @vercel/next@3.0.5-canary.1
2022-06-27 12:35:09 -05:00
Gal Schlezinger
40681ad0f4 [next] allow to declare edge functions outside of /api/ in Next.js (#7997) 2022-06-27 11:55:39 -05:00
JJ Kasper
f20703b15d [next] Update max size warning to handle initial layer better (#8013)
* Update max size warning to handle initial layer better

* update test
2022-06-27 10:22:48 -05:00
Gal Schlezinger
68eb197112 Add @vercel/edge with helpers for Middleware (#8022) 2022-06-27 17:37:46 +03:00
JJ Kasper
b8b87b96da Publish Canary
- vercel@25.1.1-canary.10
 - @vercel/next@3.0.5-canary.0
2022-06-25 15:04:32 -05:00
JJ Kasper
967c24f1bb [next] Ensure trailing slash is handled while resolving _next/data rewrites (#8015)
* Ensure trailing slash is handled while resolving _next/data rewrites

* update regex
2022-06-25 12:39:30 -05:00
JJ Kasper
609f781234 Publish Stable
- @vercel/next@3.0.4
2022-06-24 23:16:40 -05:00
JJ Kasper
998f6bf6e6 Publish Canary
- vercel@25.1.1-canary.9
 - @vercel/next@3.0.4-canary.3
 - @vercel/node@2.2.1-canary.1
2022-06-23 15:45:31 -05:00
JJ Kasper
7511c2ef85 [next] Fix normalizing for _next/data/index.json route with middleware (#8005)
* Fix normalizing for _next/data/index.json route with middleware

* ensure / -> /index.json denormalizes as well

* add continue/override
2022-06-23 14:57:07 -05:00
Michaël De Boey
71425fac1f [examples] Update remix template (#7917) 2022-06-21 12:20:01 -07:00
Lee Robinson
6973cd5989 [examples] Address follow up from SvelteKit example update (#8002) 2022-06-21 12:10:55 -07:00
Nathan Rajlich
24785ff50a [node] Implement matcher config support for Middleware (#8001)
Allows to specify a string or array of paths/globs for when a root-level
Middleware should be invoked.

```javascript
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}
```
2022-06-21 12:10:13 -07:00
Nathan Rajlich
aa3ad4478c [cli] Only show "Removing .vercel/output" log when directory exists in vc dev (#8004)
Conditionally show the "Removing…" message, instead of always rendering it.
2022-06-21 19:04:24 +00:00
JJ Kasper
f0d73049ca Publish Canary
- vercel@25.1.1-canary.8
 - @vercel/next@3.0.4-canary.2
2022-06-21 10:09:24 -05:00
JJ Kasper
6cef07354a [next] Ensure basePath is matched correctly for _next/data resolving (#7999)
This ensures we correctly match `basePath` for the `_next/data` resolving routes. The tests in the below referenced PR already cover this change so no new fixtures have been added here as they would rely on those changes landing first. 

### Related Issues

x-ref: https://github.com/vercel/next.js/pull/37854

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-20 22:29:01 +00:00
Nathan Rajlich
50af9f5b75 Publish Canary
- vercel@25.1.1-canary.7
 - @vercel/next@3.0.4-canary.1
2022-06-20 00:19:54 -07:00
Seiya Nuta
af76b134d8 [next] Check the size of WASM files in the Edge Functions size validation (#7936)
This is the last missing piece in the size validation of edge functions. Since WASM binaries are not bundled in the user JavaScript file, we also need to count their sizes in the validation.

### Related Issues

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-20 04:58:36 +00:00
Nathan Rajlich
c7640005fd [cli] Implement vc build --output parameter (#7995)
Use `--output` parameter to output the Build Output API build artifacts to a different location than the default `.vercel/output` directory.
2022-06-18 23:10:33 +00:00
Nathan Rajlich
3deed977ba [cli] Add requirePath to "builds.json" in vc build (#7990)
Include a `requirePath` property to each "build" in the `builds.json` file which is the absolute path to the Builder entrypoint that was executed.

This gives context as to which Builder was invoked by `vc build` which helps with introspection / debugging.
2022-06-18 15:07:46 +00:00
Nathan Rajlich
b38c360e36 [cli] Include "argv" in builds.json produced by vc build (#7988)
It will be useful for debugging purposes to have access to the arguments that were passed to `vc build`.
2022-06-17 22:18:33 +00:00
JJ Kasper
1595e48414 Publish Canary
- vercel@25.1.1-canary.6
 - @vercel/next@3.0.4-canary.0
2022-06-17 16:32:59 -05:00
JJ Kasper
e6b0ee3e3c [next] Update data regex to be less specific (#7994)
Update data regex to be less specific
2022-06-17 16:32:33 -05:00
325 changed files with 19359 additions and 10212 deletions

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
# https://prettier.io/docs/en/ignore.html
# ignore this file with an intentional syntax error
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js

View File

@@ -24,7 +24,6 @@ export function sendToVercelAnalytics(metric) {
speed: getConnectionSpeed(),
};
console.log({ body });
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
};

View File

@@ -1,7 +1,11 @@
node_modules
.cache
.env
.vercel
.output
public/build
api/_build
/build/
/public/build
/api/index.js
/api/index.js.map

View File

@@ -1,5 +0,0 @@
const { createRequestHandler } = require("@remix-run/vercel");
module.exports = createRequestHandler({
build: require("./_build")
});

View File

@@ -1,4 +1,4 @@
import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
hydrate(<RemixBrowser />, document);

View File

@@ -1,6 +1,6 @@
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default function handleRequest(
request: Request,
@@ -16,6 +16,6 @@ export default function handleRequest(
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders
headers: responseHeaders,
});
}

View File

@@ -1,3 +1,4 @@
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import {
Link,
Links,
@@ -6,14 +7,13 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useCatch
} from "remix";
import type { LinksFunction } from "remix";
useCatch,
} from "@remix-run/react";
import globalStylesUrl from "~/styles/global.css";
import darkStylesUrl from "~/styles/dark.css";
import globalStylesUrl from "~/styles/global.css";
// https://remix.run/api/app#links
// https://remix.run/api/conventions#links
export let links: LinksFunction = () => {
return [
{ rel: "stylesheet", href: globalStylesUrl },
@@ -25,6 +25,12 @@ export let links: LinksFunction = () => {
];
};
// https://remix.run/api/conventions#meta
export let meta: MetaFunction = () => ({
charset: "utf-8",
viewport: "width=device-width,initial-scale=1",
});
// https://remix.run/api/conventions#default-export
// https://remix.run/api/conventions#route-filenames
export default function App() {
@@ -37,7 +43,7 @@ export default function App() {
);
}
// https://remix.run/docs/en/v1/api/conventions#errorboundary
// https://remix.run/api/conventions#errorboundary
export function ErrorBoundary({ error }: { error: Error }) {
console.error(error);
return (
@@ -57,7 +63,7 @@ export function ErrorBoundary({ error }: { error: Error }) {
);
}
// https://remix.run/docs/en/v1/api/conventions#catchboundary
// https://remix.run/api/conventions#catchboundary
export function CatchBoundary() {
let caught = useCatch();
@@ -103,8 +109,6 @@ function Document({
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{title ? <title>{title}</title> : null}
<Meta />
<Links />
@@ -113,7 +117,7 @@ function Document({
{children}
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === "development" && <LiveReload />}
<LiveReload />
</body>
</html>
);

View File

@@ -1,5 +1,5 @@
import { Outlet } from "remix";
import type { MetaFunction, LinksFunction } from "remix";
import type { MetaFunction, LinksFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";
import stylesUrl from "~/styles/demos/about.css";

View File

@@ -1,4 +1,4 @@
import { Link } from "remix";
import { Link } from "@remix-run/react";
export default function AboutIndex() {
return (

View File

@@ -1,4 +1,4 @@
import { Link } from "remix";
import { Link } from "@remix-run/react";
export default function AboutIndex() {
return (

View File

@@ -1,6 +1,7 @@
import type { ActionFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
import { useEffect, useRef } from "react";
import type { ActionFunction } from "remix";
import { Form, json, useActionData, redirect } from "remix";
export function meta() {
return { title: "Actions Demo" };

View File

@@ -1,8 +1,8 @@
import { useCatch, Link, json, useLoaderData, Outlet } from "remix";
import type { MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link, Outlet, useCatch, useLoaderData } from "@remix-run/react";
export function meta() {
return { title: "Boundaries Demo" };
}
export let meta: MetaFunction = () => ({ title: "Boundaries Demo" });
export default function Boundaries() {
return (

View File

@@ -1,5 +1,6 @@
import { useCatch, Link, json, useLoaderData } from "remix";
import type { LoaderFunction, MetaFunction } from "remix";
import type { LoaderFunction, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link, useCatch, useLoaderData } from "@remix-run/react";
// The `$` in route filenames becomes a pattern that's parsed from the URL and
// passed to your loaders so you can look up data.

View File

@@ -1,5 +1,6 @@
import { useCatch, Link, json, useLoaderData, Outlet } from "remix";
import type { LoaderFunction } from "remix";
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link, Outlet, useCatch, useLoaderData } from "@remix-run/react";
export default function Boundaries() {
return (

View File

@@ -1,5 +1,6 @@
import type { MetaFunction, LoaderFunction } from "remix";
import { useLoaderData, json, Link } from "remix";
import type { MetaFunction, LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
type IndexData = {
resources: Array<{ name: string; url: string }>;

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,28 @@
{
"private": true,
"name": "remix-app-template",
"description": "",
"license": "",
"sideEffects": false,
"scripts": {
"build": "remix build",
"dev": "remix dev",
"postinstall": "remix setup node"
"dev": "remix dev"
},
"dependencies": {
"@remix-run/react": "^1.0.6",
"@remix-run/node": "^1.5.1",
"@remix-run/react": "^1.5.1",
"@remix-run/vercel": "^1.5.1",
"@vercel/node": "^2.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remix": "^1.0.6",
"@remix-run/serve": "^1.0.6",
"@remix-run/vercel": "^1.0.6"
"react-dom": "^17.0.2"
},
"devDependencies": {
"@remix-run/dev": "^1.0.6",
"@types/react": "^17.0.24",
"@types/react-dom": "^17.0.9",
"typescript": "^4.1.2"
"@remix-run/dev": "^1.5.1",
"@remix-run/eslint-config": "^1.5.1",
"@remix-run/serve": "^1.5.1",
"@types/react": "^17.0.45",
"@types/react-dom": "^17.0.17",
"eslint": "^8.15.0",
"typescript": "^4.6.4"
},
"sideEffects": false
"engines": {
"node": ">=14"
}
}

View File

@@ -1,9 +1,15 @@
/**
* @type {import('@remix-run/dev/config').AppConfig}
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
appDirectory: "app",
browserBuildDirectory: "public/build",
publicPath: "/build/",
serverBuildDirectory: "api/_build"
serverBuildTarget: "vercel",
// When running locally in development mode, we use the built in remix
// server. This does not understand the vercel lambda module format,
// so we default back to the standard build output.
server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
ignoredRouteFiles: ["**/.*"],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "api/index.js",
// publicPath: "/build/",
};

4
examples/remix/server.js Normal file
View File

@@ -0,0 +1,4 @@
import * as build from "@remix-run/dev/server-build";
import { createRequestHandler } from "@remix-run/vercel";
export default createRequestHandler({ build, mode: process.env.NODE_ENV });

View File

@@ -19,7 +19,6 @@
"prettier-plugin-svelte": "^2.5.0",
"svelte": "^3.46.0",
"svelte-check": "^2.2.6",
"svelte-preprocess": "^4.10.6",
"typescript": "~4.6.2"
},
"type": "module",

View File

@@ -7,14 +7,12 @@
let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID;
if (browser && analyticsId) {
page.subscribe(({ url, params }) =>
webVitals({
path: url.pathname,
params,
analyticsId
})
);
$: if (browser && analyticsId) {
webVitals({
path: $page.url.pathname,
params: $page.params,
analyticsId
})
}
</script>

View File

@@ -1,19 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: preprocess({
replace: [
['import.meta.env.VERCEL_ANALYTICS_ID', JSON.stringify(process.env.VERCEL_ANALYTICS_ID)]
]
}),
kit: {
adapter: adapter(),
// Override http methods in the Todo forms
methodOverride: {
allowed: ['PATCH', 'DELETE']
},
vite: {
define: {
'import.meta.env.VERCEL_ANALYTICS_ID': JSON.stringify(process.env.VERCEL_ANALYTICS_ID)
}
}
}
};

View File

@@ -31,7 +31,7 @@
"prettier": "2.6.2",
"ts-eager": "2.0.2",
"ts-jest": "28.0.5",
"turbo": "1.2.14"
"turbo": "1.3.1"
},
"scripts": {
"lerna": "lerna",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "4.1.1-canary.1",
"version": "5.0.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -31,7 +31,6 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "2.4.2",
"@vercel/frameworks": "1.0.2-canary.0",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -61,6 +61,13 @@ export interface SpawnOptionsExtended extends SpawnOptions {
* Pretty formatted command that is being spawned for logging purposes.
*/
prettyCommand?: string;
/**
* Returns instead of throwing an error when the process exits with a
* non-0 exit code. When relevant, the returned object will include
* the error code, stdout and stderr.
*/
ignoreNon0Exit?: boolean;
}
export function spawnAsync(
@@ -79,7 +86,7 @@ export function spawnAsync(
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) {
if (code === 0 || opts.ignoreNon0Exit) {
return resolve();
}
@@ -123,24 +130,24 @@ export function execAsync(
child.on('error', reject);
child.on('close', (code, signal) => {
if (code !== 0) {
const cmd = opts.prettyCommand
? `Command "${opts.prettyCommand}"`
: 'Command';
return reject(
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
);
if (code === 0 || opts.ignoreNon0Exit) {
return resolve({
code,
stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
});
}
return resolve({
code,
stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
});
const cmd = opts.prettyCommand
? `Command "${opts.prettyCommand}"`
: 'Command';
return reject(
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
);
});
}
);
@@ -166,9 +173,30 @@ export async function execCommand(command: string, options: SpawnOptions = {}) {
return true;
}
export async function getNodeBinPath({ cwd }: { cwd: string }) {
const { stdout } = await execAsync('npm', ['bin'], { cwd });
return stdout.trim();
export async function getNodeBinPath({
cwd,
}: {
cwd: string;
}): Promise<string> {
const { code, stdout, stderr } = await execAsync('npm', ['bin'], {
cwd,
prettyCommand: 'npm bin',
// in some rare cases, we saw `npm bin` exit with a non-0 code, but still
// output the right bin path, so we ignore the exit code
ignoreNon0Exit: true,
});
const nodeBinPath = stdout.trim();
if (path.isAbsolute(nodeBinPath)) {
return nodeBinPath;
}
throw new NowBuildError({
code: `BUILD_UTILS_GET_NODE_BIN_PATH`,
message: `Running \`npm bin\` failed to return a valid bin path (code=${code}, stdout=${stdout}, stderr=${stderr})`,
});
}
async function chmodPlusX(fsPath: string) {
@@ -205,10 +233,23 @@ export function getSpawnOptions(
};
if (!meta.isDev) {
// Ensure that the selected Node version is at the beginning of the `$PATH`
opts.env.PATH = `/node${nodeVersion.major}/bin${path.delimiter}${
opts.env.PATH || process.env.PATH
}`;
let found = false;
const oldPath = opts.env.PATH || process.env.PATH || '';
const pathSegments = oldPath.split(path.delimiter).map(segment => {
if (/^\/node[0-9]+\/bin/.test(segment)) {
found = true;
return `/node${nodeVersion.major}/bin`;
}
return segment;
});
if (!found) {
// If we didn't find & replace, prepend at beginning of PATH
pathSegments.unshift(`/node${nodeVersion.major}/bin`);
}
opts.env.PATH = pathSegments.filter(Boolean).join(path.delimiter);
}
return opts;
@@ -446,20 +487,31 @@ export function getEnvForPackageManager({
env: { [x: string]: string | undefined };
}) {
const newEnv: { [x: string]: string | undefined } = { ...env };
const oldPath = env.PATH + '';
const npm7 = '/node16/bin-npm7';
const pnpm7 = '/pnpm7/node_modules/.bin';
const corepackEnabled = env.ENABLE_EXPERIMENTAL_COREPACK === '1';
if (cliType === 'npm') {
if (
typeof lockfileVersion === 'number' &&
lockfileVersion >= 2 &&
(nodeVersion?.major || 0) < 16
(nodeVersion?.major || 0) < 16 &&
!oldPath.includes(npm7) &&
!corepackEnabled
) {
// Ensure that npm 7 is at the beginning of the `$PATH`
newEnv.PATH = `/node16/bin-npm7${path.delimiter}${env.PATH}`;
console.log('Detected `package-lock.json` generated by npm 7...');
newEnv.PATH = `${npm7}${path.delimiter}${oldPath}`;
console.log('Detected `package-lock.json` generated by npm 7+...');
}
} else if (cliType === 'pnpm') {
if (typeof lockfileVersion === 'number' && lockfileVersion === 5.4) {
if (
typeof lockfileVersion === 'number' &&
lockfileVersion === 5.4 &&
!oldPath.includes(pnpm7) &&
!corepackEnabled
) {
// Ensure that pnpm 7 is at the beginning of the `$PATH`
newEnv.PATH = `/pnpm7/node_modules/.bin${path.delimiter}${env.PATH}`;
newEnv.PATH = `${pnpm7}${path.delimiter}${oldPath}`;
console.log('Detected `pnpm-lock.yaml` generated by pnpm 7...');
}
} else {

View File

@@ -80,16 +80,6 @@ export {
};
export { EdgeFunction } from './edge-function';
export {
detectBuilders,
detectOutputDirectory,
detectApiDirectory,
detectApiExtensions,
} from './detect-builders';
export { detectFileSystemAPI } from './detect-file-system-api';
export { detectFramework } from './detect-framework';
export { getProjectPaths } from './get-project-paths';
export { DetectorFilesystem } from './detectors/filesystem';
export { readConfigFile } from './fs/read-config-file';
export { normalizePath } from './fs/normalize-path';
@@ -97,35 +87,3 @@ export * from './should-serve';
export * from './schemas';
export * from './types';
export * from './errors';
/**
* Helper function to support both `@vercel` and legacy `@now` official Runtimes.
*/
export const isOfficialRuntime = (desired: string, name?: string): boolean => {
if (typeof name !== 'string') {
return false;
}
return (
name === `@vercel/${desired}` ||
name === `@now/${desired}` ||
name.startsWith(`@vercel/${desired}@`) ||
name.startsWith(`@now/${desired}@`)
);
};
export const isStaticRuntime = (name?: string): boolean => {
return isOfficialRuntime('static', name);
};
export { workspaceManagers } from './workspaces/workspace-managers';
export {
getWorkspaces,
GetWorkspaceOptions,
Workspace,
WorkspaceType,
} from './workspaces/get-workspaces';
export {
getWorkspacePackagePaths,
GetWorkspacePackagePathsOptions,
} from './workspaces/get-workspace-package-paths';
export { monorepoManagers } from './monorepos/monorepo-managers';

View File

@@ -1,10 +0,0 @@
{
"functions": {
"api/users.rb": {
"memory": 3008
},
"api/doesnt-exist.rb": {
"memory": 768
}
}
}

View File

@@ -1 +0,0 @@
# project/[aid]/[bid]/index.py

View File

@@ -1 +0,0 @@
This file should also be included

View File

@@ -1,7 +0,0 @@
{
"functions": {
"api/users/post.py": {
"memory": 3008
}
}
}

View File

@@ -5,7 +5,6 @@ import {
testDeployment,
// @ts-ignore
} from '../../../test/lib/deployment/test-deployment';
import { glob, detectBuilders } from '../src';
jest.setTimeout(4 * 60 * 1000);
@@ -32,11 +31,6 @@ const skipFixtures: string[] = [
'08-zero-config-middleman',
'21-npm-workspaces',
'23-pnpm-workspaces',
'27-yarn-workspaces',
'28-turborepo-with-yarn-workspaces',
'29-nested-workspaces',
'30-double-nested-workspaces',
'31-turborepo-in-package-json',
];
// eslint-disable-next-line no-restricted-syntax
@@ -83,145 +77,3 @@ for (const builder of buildersToTestWith) {
}
}
}
it('Test `detectBuilders` and `detectRoutes`', async () => {
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/my-endpoint',
mustContain: 'my-endpoint',
status: 200,
},
{
path: '/api/other-endpoint',
mustContain: 'other-endpoint',
status: 200,
},
{
path: '/api/team/zeit',
mustContain: 'team/zeit',
status: 200,
},
{
path: '/api/user/myself',
mustContain: 'user/myself',
status: 200,
},
{
path: '/api/not-okay/',
status: 404,
},
{
path: '/api',
status: 404,
},
{
path: '/api/',
status: 404,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/not-okay',
status: 404,
},
{
path: '/api',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index.js',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/date.js',
mustContain: 'hello from api/date.js',
status: 200,
},
{
// Someone might expect this to be `date.js`,
// but I doubt that there is any case were both
// `date/index.js` and `date.js` exists,
// so it is not special cased
path: '/api/date',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});

View File

@@ -0,0 +1,29 @@
import { execAsync, NowBuildError } from '../src';
it('should execute a command', async () => {
const { code, stdout, stderr } = await execAsync('echo', ['hello']);
expect(code).toBe(0);
expect(stdout).toContain('hello');
expect(stderr).toBe('');
});
it('should throw if the command exits with non-0 code', async () => {
await expect(execAsync('find', ['unknown-file'])).rejects.toBeInstanceOf(
NowBuildError
);
});
it('should return if the command exits with non-0 code and ignoreNon0Exit=true', async () => {
const { code, stdout, stderr } = await execAsync('find', ['unknown-file'], {
ignoreNon0Exit: true,
});
expect(code).toBe(process.platform === 'win32' ? 2 : 1);
expect(stdout).toBe('');
expect(stderr).toContain(
process.platform === 'win32'
? 'Parameter format not correct'
: 'No such file or directory'
);
});

View File

@@ -38,6 +38,38 @@ describe('Test `getEnvForPackageManager()`', () => {
PATH: `/node16/bin-npm7${delimiter}foo`,
},
},
{
name: 'should not set npm path if corepack enabled',
args: {
cliType: 'npm',
nodeVersion: { major: 14, range: '14.x', runtime: 'nodejs14.x' },
lockfileVersion: 2,
env: {
FOO: 'bar',
ENABLE_EXPERIMENTAL_COREPACK: '1',
},
},
want: {
FOO: 'bar',
ENABLE_EXPERIMENTAL_COREPACK: '1',
},
},
{
name: 'should not prepend npm path again if already detected',
args: {
cliType: 'npm',
nodeVersion: { major: 14, range: '14.x', runtime: 'nodejs14.x' },
lockfileVersion: 2,
env: {
FOO: 'bar',
PATH: `/node16/bin-npm7${delimiter}foo`,
},
},
want: {
FOO: 'bar',
PATH: `/node16/bin-npm7${delimiter}foo`,
},
},
{
name: 'should not set path if node is 16 and npm 7+ is detected',
args: {
@@ -101,6 +133,38 @@ describe('Test `getEnvForPackageManager()`', () => {
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
},
},
{
name: 'should not set pnpm path if corepack is enabled',
args: {
cliType: 'pnpm',
nodeVersion: { major: 16, range: '16.x', runtime: 'nodejs16.x' },
lockfileVersion: 5.4,
env: {
FOO: 'bar',
ENABLE_EXPERIMENTAL_COREPACK: '1',
},
},
want: {
FOO: 'bar',
ENABLE_EXPERIMENTAL_COREPACK: '1',
},
},
{
name: 'should not prepend pnpm path again if already detected',
args: {
cliType: 'pnpm',
nodeVersion: { major: 16, range: '16.x', runtime: 'nodejs16.x' },
lockfileVersion: 5.4,
env: {
FOO: 'bar',
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
},
},
want: {
FOO: 'bar',
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
},
},
{
name: 'should not set path if pnpm 6 is detected',
args: {

View File

@@ -0,0 +1,111 @@
import { delimiter } from 'path';
import { getSpawnOptions } from '../src';
describe('Test `getSpawnOptions()`', () => {
const origProcessEnvPath = process.env.PATH;
beforeEach(() => {
process.env.PATH = undefined;
});
afterEach(() => {
process.env.PATH = origProcessEnvPath;
});
const cases: Array<{
name: string;
args: Parameters<typeof getSpawnOptions>;
envPath: string | undefined;
want: string | undefined;
}> = [
{
name: 'should do nothing when isDev and node14',
args: [
{ isDev: true },
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
],
envPath: '/foo',
want: '/foo',
},
{
name: 'should do nothing when isDev and node16',
args: [
{ isDev: true },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: '/foo',
want: '/foo',
},
{
name: 'should replace 14 with 16 when only path',
args: [
{ isDev: false },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: '/node14/bin',
want: '/node16/bin',
},
{
name: 'should replace 14 with 16 at beginning',
args: [
{ isDev: false },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: `/node14/bin${delimiter}/foo`,
want: `/node16/bin${delimiter}/foo`,
},
{
name: 'should replace 14 with 16 at end',
args: [
{ isDev: false },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: `/foo${delimiter}/node14/bin`,
want: `/foo${delimiter}/node16/bin`,
},
{
name: 'should replace 14 with 16 in middle',
args: [
{ isDev: false },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: `/foo${delimiter}/node14/bin${delimiter}/bar`,
want: `/foo${delimiter}/node16/bin${delimiter}/bar`,
},
{
name: 'should prepend 16 at beginning when nothing to replace',
args: [
{ isDev: false },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: `/foo`,
want: `/node16/bin${delimiter}/foo`,
},
{
name: 'should prepend 16 at beginning no path input',
args: [
{ isDev: false },
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
],
envPath: '',
want: `/node16/bin`,
},
{
name: 'should replace 12 with 14 when only path',
args: [
{ isDev: false },
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
],
envPath: '/node12/bin',
want: '/node14/bin',
},
];
for (const { name, args, envPath, want } of cases) {
it(name, () => {
process.env.PATH = envPath;
const opts = getSpawnOptions(...args);
expect(opts.env?.PATH).toBe(want);
});
}
});

View File

@@ -0,0 +1,21 @@
import { spawnAsync, NowBuildError } from '../src';
it('should execute a command', async () => {
// should resolve (it doesn't return anything, so it resolves with "undefined")
await expect(spawnAsync('echo', ['hello'])).resolves.toBeUndefined();
});
it('should throw if the command exits with non-0 code', async () => {
await expect(spawnAsync('find', ['unknown-file'])).rejects.toBeInstanceOf(
NowBuildError
);
});
it('should return if the command exits with non-0 code and ignoreNon0Exit=true', async () => {
// should resolve (it doesn't return anything, so it resolves with "undefined")
await expect(
spawnAsync('find', ['unknown-file'], {
ignoreNon0Exit: true,
})
).resolves.toBeUndefined();
});

View File

@@ -18,6 +18,8 @@ import {
Meta,
} from '../src';
jest.setTimeout(7 * 1000);
async function expectBuilderError(promise: Promise<any>, pattern: string) {
let result;
try {

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "25.1.1-canary.5",
"version": "26.0.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -42,15 +42,15 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/go": "2.0.2-canary.1",
"@vercel/next": "3.0.3",
"@vercel/node": "2.2.1-canary.0",
"@vercel/python": "3.0.2-canary.1",
"@vercel/redwood": "1.0.2-canary.1",
"@vercel/remix": "1.0.2-canary.1",
"@vercel/ruby": "1.3.10-canary.1",
"@vercel/static-build": "1.0.2-canary.1",
"@vercel/build-utils": "5.0.0",
"@vercel/go": "2.0.4",
"@vercel/next": "3.1.3",
"@vercel/node": "2.4.0",
"@vercel/python": "3.0.4",
"@vercel/redwood": "1.0.5",
"@vercel/remix": "1.0.5",
"@vercel/ruby": "1.3.12",
"@vercel/static-build": "1.0.4",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -95,8 +95,9 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.0.2-canary.1",
"@vercel/frameworks": "1.0.2-canary.0",
"@vercel/client": "12.0.4",
"@vercel/frameworks": "1.0.2",
"@vercel/fs-detectors": "1.0.0",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",

View File

@@ -10,7 +10,6 @@ import confirm from '../../util/input/confirm';
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
import { Alias } from '../../types';
import { Output } from '../../util/output';
import { isValidName } from '../../util/is-valid-name';
import { getCommandName } from '../../util/pkg-name';
@@ -71,7 +70,7 @@ export default async function rm(
}
const removeStamp = stamp();
if (!opts['--yes'] && !(await confirmAliasRemove(output, alias))) {
if (!opts['--yes'] && !(await confirmAliasRemove(client, alias))) {
output.log('Aborted');
return 0;
}
@@ -85,7 +84,7 @@ export default async function rm(
return 0;
}
async function confirmAliasRemove(output: Output, alias: Alias) {
async function confirmAliasRemove(client: Client, alias: Alias) {
const srcUrl = alias.deployment
? chalk.underline(alias.deployment.url)
: null;
@@ -104,7 +103,7 @@ async function confirmAliasRemove(output: Output, alias: Alias) {
}
);
output.log(`The following alias will be removed permanently`);
output.print(` ${tbl}\n`);
return confirm(chalk.red('Are you sure?'), false);
client.output.log(`The following alias will be removed permanently`);
client.output.print(` ${tbl}\n`);
return confirm(client, chalk.red('Are you sure?'), false);
}

View File

@@ -127,7 +127,7 @@ export default async function ({ creditCards, clear = false, contextName }) {
}
console.log(''); // New line
const stopSpinner = wait('Saving card');
const stopSpinner = wait(process.stderr, 'Saving card');
try {
const res = await creditCards.add({

View File

@@ -174,7 +174,7 @@ export default async client => {
)} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards);
cardId = await listInput({
cardId = await listInput(client, {
message,
choices,
separator: true,
@@ -187,6 +187,7 @@ export default async client => {
if (cardId) {
const label = `Are you sure that you to set this card as the default?`;
const confirmation = await promptBool(label, {
...client,
trailing: '\n',
});
@@ -250,7 +251,7 @@ export default async client => {
)} under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards);
cardId = await listInput({
cardId = await listInput(client, {
message,
choices,
separator: true,
@@ -262,7 +263,7 @@ export default async client => {
// typed `vercel billing rm <some-id>`) is valid
if (cardId) {
const label = `Are you sure that you want to remove this card?`;
const confirmation = await promptBool(label);
const confirmation = await promptBool(label, client);
if (!confirmation) {
console.log('Aborted');
break;

View File

@@ -14,8 +14,8 @@ import logo from '../../util/output/logo';
import getArgs from '../../util/get-args';
import Client from '../../util/client';
import { getPkgName } from '../../util/pkg-name';
import { Output } from '../../util/output';
import { Deployment, PaginationOptions } from '../../types';
import { normalizeURL } from '../../util/bisect/normalize-url';
interface DeploymentV6
extends Pick<
@@ -85,10 +85,10 @@ export default async function main(client: Client): Promise<number> {
let bad =
argv['--bad'] ||
(await prompt(output, `Specify a URL where the bug occurs:`));
(await prompt(client, `Specify a URL where the bug occurs:`));
let good =
argv['--good'] ||
(await prompt(output, `Specify a URL where the bug does not occur:`));
(await prompt(client, `Specify a URL where the bug does not occur:`));
let subpath = argv['--path'] || '';
let run = argv['--run'] || '';
const openEnabled = argv['--open'] || false;
@@ -97,9 +97,7 @@ export default async function main(client: Client): Promise<number> {
run = resolve(run);
}
if (!bad.startsWith('https://')) {
bad = `https://${bad}`;
}
bad = normalizeURL(bad);
let parsed = parse(bad);
if (!parsed.hostname) {
output.error('Invalid input: no hostname provided');
@@ -120,9 +118,7 @@ export default async function main(client: Client): Promise<number> {
const badDeploymentPromise = getDeployment(client, bad).catch(err => err);
if (!good.startsWith('https://')) {
good = `https://${good}`;
}
good = normalizeURL(good);
parsed = parse(good);
if (!parsed.hostname) {
output.error('Invalid input: no hostname provided');
@@ -146,7 +142,7 @@ export default async function main(client: Client): Promise<number> {
if (!subpath) {
subpath = await prompt(
output,
client,
`Specify the URL subpath where the bug occurs:`
);
}
@@ -394,10 +390,10 @@ function getCommit(deployment: DeploymentV6) {
return { sha, message };
}
async function prompt(output: Output, message: string): Promise<string> {
async function prompt(client: Client, message: string): Promise<string> {
// eslint-disable-next-line no-constant-condition
while (true) {
const { val } = await inquirer.prompt({
const { val } = await client.prompt({
type: 'input',
name: 'val',
message,
@@ -405,7 +401,7 @@ async function prompt(output: Output, message: string): Promise<string> {
if (val) {
return val;
} else {
output.error('A value must be specified');
client.output.error('A value must be specified');
}
}
}

View File

@@ -1,9 +1,8 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import dotenv from 'dotenv';
import { join, normalize, relative } from 'path';
import { join, normalize, relative, resolve } from 'path';
import {
detectBuilders,
normalizePath,
Files,
FileFsRef,
@@ -17,6 +16,7 @@ import {
BuildResultV3,
NowBuildError,
} from '@vercel/build-utils';
import { detectBuilders } from '@vercel/fs-detectors';
import minimatch from 'minimatch';
import {
appendRoutesToPhase,
@@ -65,6 +65,7 @@ const help = () => {
'DIR'
)} Path to the global ${'`.vercel`'} directory
--cwd [path] The current working directory
--output [path] Directory where built assets should be written to
--prod Build a production deployment
-d, --debug Debug mode [off]
-y, --yes Skip the confirmation prompt
@@ -101,6 +102,7 @@ export default async function main(client: Client): Promise<number> {
// Parse CLI args
const argv = getArgs(client.argv.slice(2), {
'--cwd': String,
'--output': String,
'--prod': Boolean,
'--yes': Boolean,
});
@@ -138,6 +140,7 @@ export default async function main(client: Client): Promise<number> {
}
confirmed = await confirm(
client,
`No Project Settings found locally. Run ${cli.getCommandName(
'pull'
)} for retrieving them?`,
@@ -280,7 +283,9 @@ export default async function main(client: Client): Promise<number> {
}
// Delete output directory from potential previous build
const outputDir = join(cwd, OUTPUT_DIR);
const outputDir = argv['--output']
? resolve(argv['--output'])
: join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
const buildStamp = stamp();
@@ -297,6 +302,7 @@ export default async function main(client: Client): Promise<number> {
{
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
target,
argv: process.argv,
builds: builds.map(build => {
const builderWithPkg = buildersWithPkgs.get(build.use);
if (!builderWithPkg) {
@@ -305,6 +311,7 @@ export default async function main(client: Client): Promise<number> {
const { builder, pkg: builderPkg } = builderWithPkg;
return {
require: builderPkg.name,
requirePath: builderWithPkg.path,
apiVersion: builder.version,
...build,
};
@@ -463,11 +470,12 @@ export default async function main(client: Client): Promise<number> {
};
await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
const relOutputDir = relative(cwd, outputDir);
output.print(
`${prependEmoji(
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray(
buildStamp()
)}`,
`Build Completed in ${chalk.bold(
relOutputDir.startsWith('..') ? outputDir : relOutputDir
)} ${chalk.gray(buildStamp())}`,
emoji('success')
)}\n`
);

View File

@@ -160,12 +160,12 @@ export default async (client: Client) => {
}
}
const { log, debug, error, prettyError, isTTY } = output;
const { log, debug, error, prettyError } = output;
const quiet = !isTTY;
const quiet = !client.stdout.isTTY;
// check paths
const pathValidation = await validatePaths(output, paths);
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
@@ -243,6 +243,7 @@ export default async (client: Client) => {
const shouldStartSetup =
autoConfirm ||
(await confirm(
client,
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
@@ -287,7 +288,7 @@ export default async (client: Client) => {
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
@@ -521,7 +522,7 @@ export default async (client: Client) => {
}
const settings = await editProjectSettings(
output,
client,
projectSettings,
framework,
false,

View File

@@ -110,9 +110,11 @@ export default async function dev(
// v3 Build Output because it will incorrectly be detected by
// @vercel/static-build in BuildOutputV3.getBuildOutputDirectory()
if (!devCommand) {
output.log(`Removing ${OUTPUT_DIR}`);
const outputDir = join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
if (await fs.pathExists(outputDir)) {
output.log(`Removing ${OUTPUT_DIR}`);
await fs.remove(outputDir);
}
}
const devServer = new DevServer(cwd, {

View File

@@ -46,7 +46,7 @@ export default async function add(
const addStamp = stamp();
const { domain, data: argData } = parsedParams;
const data = await getDNSData(output, argData);
const data = await getDNSData(client, argData);
if (!data) {
output.log(`Aborted`);
return 1;

View File

@@ -87,7 +87,8 @@ export default async function buy(
!(await promptBool(
`Buy now for ${chalk.bold(`$${price}`)} (${`${period}yr${
period > 1 ? 's' : ''
}`})?`
}`})?`,
client
))
) {
return 0;
@@ -99,7 +100,7 @@ export default async function buy(
: `Auto renew every ${renewalPrice.period} years for ${chalk.bold(
`$${price}`
)}?`,
{ defaultValue: true }
{ ...client, defaultValue: true }
);
let buyResult;

View File

@@ -77,7 +77,8 @@ export default async function move(
!(await promptBool(
`Are you sure you want to move ${param(domainName)} to ${param(
destination
)}?`
)}?`,
client
))
) {
output.log('Aborted');
@@ -95,7 +96,8 @@ export default async function move(
);
if (
!(await promptBool(
`Are you sure you want to move ${param(domainName)}?`
`Are you sure you want to move ${param(domainName)}?`,
client
))
) {
output.log('Aborted');

View File

@@ -92,7 +92,10 @@ export default async function rm(
const skipConfirmation = opts['--yes'] || false;
if (
!skipConfirmation &&
!(await promptBool(`Are you sure you want to remove ${param(domainName)}?`))
!(await promptBool(
`Are you sure you want to remove ${param(domainName)}?`,
client
))
) {
output.log('Aborted');
return 0;
@@ -230,7 +233,7 @@ async function removeDomain(
if (
!skipConfirmation &&
!(await promptBool(`Remove conflicts associated with domain?`))
!(await promptBool(`Remove conflicts associated with domain?`, client))
) {
output.log('Aborted');
return 0;

View File

@@ -81,7 +81,8 @@ export default async function transferIn(
const shouldTransfer = await promptBool(
transferPolicy === 'no-change'
? `Transfer now for ${chalk.bold(`$${price}`)}?`
: `Transfer now with 1yr renewal for ${chalk.bold(`$${price}`)}?`
: `Transfer now with 1yr renewal for ${chalk.bold(`$${price}`)}?`,
client
);
if (!shouldTransfer) {
return 0;

View File

@@ -31,7 +31,7 @@ export default async function add(
// improve the way we show inquirer prompts
require('../../util/input/patch-inquirer');
const stdInput = await readStandardInput();
const stdInput = await readStandardInput(client.stdin);
let [envName, envTargetArg, envGitBranch] = args;
if (args.length > 3) {

View File

@@ -74,6 +74,7 @@ export default async function pull(
exists &&
!skipConfirmation &&
!(await confirm(
client,
`Found existing file ${param(filename)}. Do you want to overwrite?`,
false
))

View File

@@ -104,6 +104,7 @@ export default async function rm(
if (
!skipConfirmation &&
!(await confirm(
client,
`Removing Environment Variable ${param(env.key)} from ${formatEnvTarget(
env
)} in Project ${chalk.bold(project.name)}. Are you sure?`,

View File

@@ -46,7 +46,11 @@ export default async function init(
const exampleList = examples.filter(x => x.visible).map(x => x.name);
if (!name) {
const chosen = await chooseFromDropdown('Select example:', exampleList);
const chosen = await chooseFromDropdown(
client,
'Select example:',
exampleList
);
if (!chosen) {
output.log('Aborted');
@@ -65,7 +69,7 @@ export default async function init(
return extractExample(client, name, dir, force, 'v1');
}
const found = await guess(exampleList, name);
const found = await guess(client, exampleList, name);
if (typeof found === 'string') {
return extractExample(client, found, dir, force);
@@ -90,14 +94,18 @@ async function fetchExampleList(client: Client) {
/**
* Prompt user for choosing which example to init
*/
async function chooseFromDropdown(message: string, exampleList: string[]) {
async function chooseFromDropdown(
client: Client,
message: string,
exampleList: string[]
) {
const choices = exampleList.map(name => ({
name,
value: name,
short: name,
}));
return listInput({
return listInput(client, {
message,
choices,
});
@@ -194,7 +202,7 @@ function prepareFolder(cwd: string, folder: string, force?: boolean) {
/**
* Guess which example user try to init
*/
async function guess(exampleList: string[], name: string) {
async function guess(client: Client, exampleList: string[], name: string) {
const GuessError = new Error(
`No example found for ${chalk.bold(name)}, run ${getCommandName(
`init`
@@ -208,7 +216,7 @@ async function guess(exampleList: string[], name: string) {
const found = didYouMean(name, exampleList, 0.7);
if (typeof found === 'string') {
if (await promptBool(`Did you mean ${chalk.bold(found)}?`)) {
if (await promptBool(`Did you mean ${chalk.bold(found)}?`, client)) {
return found;
}
} else {

View File

@@ -83,7 +83,7 @@ export default async function main(client: Client, desiredSlug?: string) {
];
output.stopSpinner();
desiredSlug = await listInput({
desiredSlug = await listInput(client, {
message: 'Switch to:',
choices,
eraseFinalAnswer: true,

View File

@@ -54,12 +54,12 @@ export default async (client: Client): Promise<number> => {
throw err;
}
if (output.isTTY) {
if (client.stdout.isTTY) {
output.log(contextName);
} else {
// If stdout is not a TTY, then only print the username
// to support piping the output to another file / exe
output.print(`${contextName}\n`, { w: process.stdout });
client.stdout.write(`${contextName}\n`);
}
return 0;

View File

@@ -23,7 +23,7 @@ import * as Sentry from '@sentry/node';
import hp from './util/humanize-path';
import commands from './commands';
import pkg from './util/pkg';
import createOutput from './util/output';
import { Output } from './util/output';
import cmd from './util/output/cmd';
import info from './util/output/info';
import error from './util/output/error';
@@ -109,7 +109,7 @@ const main = async () => {
}
const isDebugging = argv['--debug'];
const output = createOutput({ debug: isDebugging });
const output = new Output(process.stderr, { debug: isDebugging });
debug = output.debug;
@@ -387,6 +387,9 @@ const main = async () => {
// Shared API `Client` instance for all sub-commands to utilize
client = new Client({
apiUrl,
stdin: process.stdin,
stdout: process.stdout,
stderr: output.stream,
output,
config,
authConfig,
@@ -796,7 +799,5 @@ process.on('uncaughtException', handleUnexpected);
main()
.then(exitCode => {
process.exitCode = exitCode;
// @ts-ignore - "nowExit" is a non-standard event name
process.emit('nowExit');
})
.catch(handleUnexpected);

View File

@@ -1,3 +1,5 @@
import type { Readable, Writable } from 'stream';
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
export type Primitive =
@@ -442,3 +444,19 @@ export interface BuildOutput {
layers?: string[];
} | null;
}
export interface ReadableTTY extends Readable {
isTTY?: boolean;
isRaw?: boolean;
setRawMode?: (mode: boolean) => void;
}
export interface WritableTTY extends Writable {
isTTY?: boolean;
}
export interface Stdio {
stdin: ReadableTTY;
stdout: WritableTTY;
stderr: WritableTTY;
}

View File

@@ -0,0 +1,7 @@
function hasScheme(url: string): Boolean {
return url.startsWith('http://') || url.startsWith('https://');
}
export function normalizeURL(url: string): string {
return hasScheme(url) ? url : `https://${url}`;
}

View File

@@ -385,8 +385,14 @@ export async function* findDirs(
}
for (const path of paths) {
const abs = join(dir, path);
const s = await fs.stat(abs);
if (s.isDirectory()) {
let stat: fs.Stats;
try {
stat = await fs.lstat(abs);
} catch (err: any) {
if (err.code === 'ENOENT') continue;
throw err;
}
if (stat.isDirectory()) {
if (path === name) {
yield relative(root, abs);
} else {

View File

@@ -1,3 +1,5 @@
import { bold } from 'chalk';
import inquirer from 'inquirer';
import { EventEmitter } from 'events';
import { URLSearchParams } from 'url';
import { parse as parseUrl } from 'url';
@@ -11,10 +13,16 @@ import printIndications from './print-indications';
import reauthenticate from './login/reauthenticate';
import { SAMLError } from './login/types';
import { writeToAuthConfigFile } from './config/files';
import { AuthConfig, GlobalConfig, JSONObject } from '../types';
import type {
AuthConfig,
GlobalConfig,
JSONObject,
Stdio,
ReadableTTY,
WritableTTY,
} from '../types';
import { sharedPromise } from './promise';
import { APIError } from './errors-ts';
import { bold } from 'chalk';
const isSAMLError = (v: any): v is SAMLError => {
return v && v.saml;
@@ -28,7 +36,7 @@ export interface FetchOptions extends Omit<RequestInit, 'body'> {
accountId?: string;
}
export interface ClientOptions {
export interface ClientOptions extends Stdio {
argv: string[];
apiUrl: string;
authConfig: AuthConfig;
@@ -41,13 +49,17 @@ export const isJSONObject = (v: any): v is JSONObject => {
return v && typeof v == 'object' && v.constructor === Object;
};
export default class Client extends EventEmitter {
export default class Client extends EventEmitter implements Stdio {
argv: string[];
apiUrl: string;
authConfig: AuthConfig;
stdin: ReadableTTY;
stdout: WritableTTY;
stderr: WritableTTY;
output: Output;
config: GlobalConfig;
localConfig?: VercelConfig;
prompt!: inquirer.PromptModule;
private requestIdCounter: number;
constructor(opts: ClientOptions) {
@@ -55,10 +67,14 @@ export default class Client extends EventEmitter {
this.argv = opts.argv;
this.apiUrl = opts.apiUrl;
this.authConfig = opts.authConfig;
this.stdin = opts.stdin;
this.stdout = opts.stdout;
this.stderr = opts.stderr;
this.output = opts.output;
this.config = opts.config;
this.localConfig = opts.localConfig;
this.requestIdCounter = 1;
this._createPromptModule();
}
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
@@ -124,7 +140,7 @@ export default class Client extends EventEmitter {
return this.retry(async bail => {
const res = await this._fetch(url, opts);
printIndications(res);
printIndications(this, res);
if (!res.ok) {
const error = await responseError(res);
@@ -180,4 +196,11 @@ export default class Client extends EventEmitter {
_onRetry = (error: Error) => {
this.output.debug(`Retrying: ${error}\n${error.stack}`);
};
_createPromptModule() {
this.prompt = inquirer.createPromptModule({
input: this.stdin as NodeJS.ReadStream,
output: this.stderr as NodeJS.WriteStream,
});
}
}

View File

@@ -13,8 +13,8 @@ import {
Lambda,
FileBlob,
FileFsRef,
isOfficialRuntime,
} from '@vercel/build-utils';
import { isOfficialRuntime } from '@vercel/fs-detectors';
import plural from 'pluralize';
import minimatch from 'minimatch';
@@ -425,10 +425,6 @@ export async function getBuildMatches(
src = extensionless;
}
// We need to escape brackets since `glob` will
// try to find a group otherwise
src = src.replace(/(\[|\])/g, '[$1]');
const files = fileList
.filter(name => name === src || minimatch(name, src, { dot: true }))
.map(name => join(cwd, name));

View File

@@ -0,0 +1,18 @@
import { Headers } from 'node-fetch';
import { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http';
export function nodeHeadersToFetchHeaders(
nodeHeaders: IncomingHttpHeaders | OutgoingHttpHeaders
): Headers {
const headers = new Headers();
for (const [name, value] of Object.entries(nodeHeaders)) {
if (Array.isArray(value)) {
for (const val of value) {
headers.append(name, val);
}
} else if (typeof value !== 'undefined') {
headers.set(name, String(value));
}
}
return headers;
}

View File

@@ -1,12 +1,13 @@
import ms from 'ms';
import url, { URL } from 'url';
import http from 'http';
import fs from 'fs-extra';
import chalk from 'chalk';
import fetch from 'node-fetch';
import plural from 'pluralize';
import rawBody from 'raw-body';
import listen from 'async-listen';
import minimatch from 'minimatch';
import ms from 'ms';
import httpProxy from 'http-proxy';
import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler';
@@ -16,11 +17,11 @@ import path, { isAbsolute, basename, dirname, extname, join } from 'path';
import once from '@tootallnate/once';
import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port';
import { ChildProcess } from 'child_process';
import isPortReachable from 'is-port-reachable';
import deepEqual from 'fast-deep-equal';
import which from 'which';
import npa from 'npm-package-arg';
import type { ChildProcess } from 'child_process';
import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
import {
@@ -35,12 +36,14 @@ import {
StartDevServerResult,
FileFsRef,
PackageJson,
spawnCommand,
} from '@vercel/build-utils';
import {
detectBuilders,
detectApiDirectory,
detectApiExtensions,
spawnCommand,
isOfficialRuntime,
} from '@vercel/build-utils';
} from '@vercel/fs-detectors';
import frameworkList from '@vercel/frameworks';
import cmd from '../output/cmd';
@@ -90,6 +93,7 @@ import {
import { ProjectEnvVariable, ProjectSettings } from '../../types';
import exposeSystemEnvs from './expose-system-envs';
import { treeKill } from '../tree-kill';
import { nodeHeadersToFetchHeaders } from './headers';
const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
@@ -327,6 +331,8 @@ export default class DevServer {
): Promise<void> {
const name = relative(this.cwd, fsPath);
try {
await this.getVercelConfig();
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
const extensionless = this.getExtensionlessFile(name);
if (extensionless) {
@@ -593,7 +599,7 @@ export default class DevServer {
await this.exit();
}
if (warnings && warnings.length > 0) {
if (warnings?.length > 0) {
warnings.forEach(warning =>
this.output.warn(warning.message, null, warning.link, warning.action)
);
@@ -1106,6 +1112,7 @@ export default class DevServer {
view = errorTemplate({
http_status_code: statusCode,
http_status_description,
error_code,
request_id: requestId,
});
}
@@ -1337,32 +1344,6 @@ export default class DevServer {
return false;
};
/*
runDevMiddleware = async (
req: http.IncomingMessage,
res: http.ServerResponse
) => {
const { devMiddlewarePlugins } = await loadCliPlugins(
this.cwd,
this.output
);
try {
for (let plugin of devMiddlewarePlugins) {
const result = await plugin.plugin.runDevMiddleware(req, res, this.cwd);
if (result.finished) {
return result;
}
}
return { finished: false };
} catch (e) {
return {
finished: true,
error: e,
};
}
};
*/
/**
* Serve project directory as a v2 deployment.
*/
@@ -1429,13 +1410,143 @@ export default class DevServer {
let statusCode: number | undefined;
let prevUrl = req.url;
let prevHeaders: HttpHeadersConfig = {};
let middlewarePid: number | undefined;
/*
const middlewareResult = await this.runDevMiddleware(req, res);
// Run the middleware file, if present, and apply any
// mutations to the incoming request based on the
// result of the middleware invocation.
const middleware = [...this.buildMatches.values()].find(
m => m.config?.middleware === true
);
if (middleware) {
let startMiddlewareResult: StartDevServerResult | undefined;
// TODO: can we add some caching to prevent (re-)starting
// the middleware server for every HTTP request?
const { envConfigs, files, devCacheDir, cwd: workPath } = this;
try {
startMiddlewareResult =
await middleware.builderWithPkg.builder.startDevServer?.({
files,
entrypoint: middleware.entrypoint,
workPath,
repoRootPath: this.cwd,
config: middleware.config || {},
meta: {
isDev: true,
devCacheDir,
requestUrl: req.url,
env: { ...envConfigs.runEnv },
buildEnv: { ...envConfigs.buildEnv },
},
});
if (middlewareResult) {
if (middlewareResult.error) {
this.sendError(
if (startMiddlewareResult) {
const { port, pid } = startMiddlewareResult;
middlewarePid = pid;
this.devServerPids.add(pid);
const middlewareReqHeaders = nodeHeadersToFetchHeaders(req.headers);
// Add the Vercel platform proxy request headers
const proxyHeaders = this.getProxyHeaders(req, requestId, true);
for (const [name, value] of nodeHeadersToFetchHeaders(proxyHeaders)) {
middlewareReqHeaders.set(name, value);
}
const middlewareRes = await fetch(
`http://127.0.0.1:${port}${parsed.path}`,
{
headers: middlewareReqHeaders,
method: req.method,
redirect: 'manual',
}
);
if (middlewareRes.status === 500) {
await this.sendError(
req,
res,
requestId,
'EDGE_FUNCTION_INVOCATION_FAILED',
500
);
return;
}
// Apply status code from middleware invocation,
// for i.e. redirects or a custom 404 page
res.statusCode = middlewareRes.status;
let rewritePath = '';
let contentType = '';
let shouldContinue = false;
const skipMiddlewareHeaders = new Set([
'date',
'connection',
'content-length',
'transfer-encoding',
]);
for (const [name, value] of middlewareRes.headers) {
if (name === 'x-middleware-next') {
shouldContinue = value === '1';
} else if (name === 'x-middleware-rewrite') {
rewritePath = value;
shouldContinue = true;
} else if (name === 'content-type') {
contentType = value;
} else if (!skipMiddlewareHeaders.has(name)) {
// Any other kind of response header should be included
// on both the incoming HTTP request (for when proxying
// to another function) and the outgoing HTTP response.
res.setHeader(name, value);
req.headers[name] = value;
}
}
if (!shouldContinue) {
const middlewareBody = await middlewareRes.buffer();
this.setResponseHeaders(res, requestId);
if (middlewareBody.length > 0) {
res.setHeader('content-length', middlewareBody.length);
if (contentType) {
res.setHeader('content-type', contentType);
}
res.end(middlewareBody);
} else {
res.end();
}
return;
}
if (rewritePath) {
// TODO: add validation?
debug(`Detected rewrite path from middleware: "${rewritePath}"`);
prevUrl = rewritePath;
// Retain orginal pathname, but override query parameters from the rewrite
const beforeRewriteUrl = req.url || '/';
const rewriteUrlParsed = url.parse(beforeRewriteUrl, true);
delete rewriteUrlParsed.search;
rewriteUrlParsed.query = url.parse(rewritePath, true).query;
req.url = url.format(rewriteUrlParsed);
debug(
`Rewrote incoming HTTP URL from "${beforeRewriteUrl}" to "${req.url}"`
);
}
}
} catch (err) {
// `startDevServer()` threw an error. Most likely this means the dev
// server process exited before sending the port information message
// (missing dependency at runtime, for example).
if (err.code === 'ENOENT') {
err.message = `Command not found: ${chalk.cyan(
err.path,
...err.spawnargs
)}\nPlease ensure that ${cmd(err.path)} is properly installed`;
err.link = 'https://vercel.link/command-not-found';
}
await this.sendError(
req,
res,
requestId,
@@ -1443,24 +1554,12 @@ export default class DevServer {
500
);
return;
}
if (middlewareResult.finished) {
return;
}
if (middlewareResult.pathname) {
const origUrl = url.parse(req.url || '/', true);
origUrl.pathname = middlewareResult.pathname;
prevUrl = url.format(origUrl);
}
if (middlewareResult.query && prevUrl) {
const origUrl = url.parse(req.url || '/', true);
delete origUrl.search;
Object.assign(origUrl.query, middlewareResult.query);
prevUrl = url.format(origUrl);
} finally {
if (middlewarePid) {
this.killBuilderDevServer(middlewarePid);
}
}
}
*/
for (const phase of phases) {
statusCode = undefined;
@@ -1740,7 +1839,10 @@ export default class DevServer {
isDev: true,
requestPath,
devCacheDir,
env: { ...envConfigs.runEnv },
env: {
...envConfigs.runEnv,
VERCEL_BUILDER_DEBUG: this.output.debugEnabled ? '1' : undefined,
},
buildEnv: { ...envConfigs.buildEnv },
},
});
@@ -2185,13 +2287,7 @@ function proxyPass(
`Failed to complete request to ${req.url}: ${error}`
);
if (!res.headersSent) {
devServer.sendError(
req,
res,
requestId,
'NO_RESPONSE_FROM_FUNCTION',
502
);
devServer.sendError(req, res, requestId, 'FUNCTION_INVOCATION_FAILED');
}
}
);
@@ -2269,11 +2365,12 @@ async function findBuildMatch(
if (!isIndex(match.src)) {
return match;
} else {
// if isIndex === true and ends in .html, we're done. Otherwise, keep searching
bestIndexMatch = match;
// If isIndex === true and ends in `.html`, we're done.
// Otherwise, keep searching.
if (extname(match.src) === '.html') {
return bestIndexMatch;
return match;
}
bestIndexMatch = match;
}
}
}
@@ -2295,6 +2392,13 @@ async function shouldServe(
config,
builderWithPkg: { builder },
} = match;
// "middleware" file is not served as a regular asset,
// instead it gets invoked as part of the routing logic.
if (config?.middleware === true) {
return false;
}
const cleanSrc = src.endsWith('.html') ? src.slice(0, -5) : src;
const trimmedPath = requestPath.endsWith('/')
? requestPath.slice(0, -1)

View File

@@ -2,26 +2,29 @@ import chalk from 'chalk';
import { DNSRecordData } from '../../types';
import textInput from '../input/text';
import promptBool from '../input/prompt-bool';
import { Output } from '../output';
import Client from '../client';
const RECORD_TYPES = ['A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'SRV', 'TXT'];
export default async function getDNSData(
output: Output,
client: Client,
data: null | DNSRecordData
): Promise<DNSRecordData | null> {
if (data) {
return data;
}
const { output } = client;
try {
// first ask for type, branch from there
const possibleTypes = new Set(RECORD_TYPES);
const type = (await textInput({
label: `- Record type (${RECORD_TYPES.join(', ')}): `,
validateValue: (v: string) =>
Boolean(v && possibleTypes.has(v.trim().toUpperCase()))
}))
const type = (
await textInput({
label: `- Record type (${RECORD_TYPES.join(', ')}): `,
validateValue: (v: string) =>
Boolean(v && possibleTypes.has(v.trim().toUpperCase())),
})
)
.trim()
.toUpperCase();
@@ -39,7 +42,7 @@ export default async function getDNSData(
target
)}.`
);
return (await verifyData())
return (await verifyData(client))
? {
name,
type,
@@ -47,8 +50,8 @@ export default async function getDNSData(
priority,
weight,
port,
target
}
target,
},
}
: null;
}
@@ -61,23 +64,23 @@ export default async function getDNSData(
`${mxPriority}`
)} ${chalk.cyan(value)}`
);
return (await verifyData())
return (await verifyData(client))
? {
name,
type,
value,
mxPriority
mxPriority,
}
: null;
}
const value = await getTrimmedString(`- ${type} value: `);
output.log(`${chalk.cyan(name)} ${chalk.bold(type)} ${chalk.cyan(value)}`);
return (await verifyData())
return (await verifyData(client))
? {
name,
type,
value
value,
}
: null;
} catch (error) {
@@ -85,13 +88,13 @@ export default async function getDNSData(
}
}
async function verifyData() {
return promptBool('Is this correct?');
async function verifyData(client: Client) {
return promptBool('Is this correct?', client);
}
async function getRecordName(type: string) {
const input = await textInput({
label: `- ${type} name: `
label: `- ${type} name: `,
});
return input === '@' ? '' : input;
}
@@ -100,14 +103,14 @@ async function getNumber(label: string) {
return Number(
await textInput({
label,
validateValue: v => Boolean(v && Number(v))
validateValue: v => Boolean(v && Number(v)),
})
);
}
async function getTrimmedString(label: string) {
const res = await textInput({
label,
validateValue: v => Boolean(v && v.trim().length > 0)
validateValue: v => Boolean(v && v.trim().length > 0),
});
return res.trim();
}

View File

@@ -54,7 +54,8 @@ export default async function purchaseDomainIfAvailable(
!(await promptBool(
`Buy ${chalk.underline(domain)} for ${chalk.bold(
`$${price}`
)} (${plural('yr', period, true)})?`
)} (${plural('yr', period, true)})?`,
client
))
) {
output.print(eraseLines(1));

View File

@@ -1,5 +1,5 @@
import { resolve } from 'path';
import fs from 'fs-extra';
import { resolve } from 'path';
import { getVercelIgnore } from '@vercel/client';
import uniqueStrings from './unique-strings';
import { Output } from './output/create-output';

View File

@@ -530,7 +530,7 @@ export default class Now extends EventEmitter {
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
fetch(`${this._apiUrl}${_url}`, { ...opts, body })
);
printIndications(res);
printIndications(this._client, res);
return res;
}

View File

@@ -1,12 +1,13 @@
import inquirer from 'inquirer';
import Client from '../client';
export default async function confirm(
client: Client,
message: string,
preferred: boolean
): Promise<boolean> {
require('./patch-inquirer');
const answers = await inquirer.prompt({
const answers = await client.prompt({
type: 'confirm',
name: 'value',
message,

View File

@@ -1,8 +1,8 @@
import inquirer from 'inquirer';
import confirm from './confirm';
import chalk from 'chalk';
import { Output } from '../output';
import frameworkList, { Framework } from '@vercel/frameworks';
import Client from '../client';
import { isSettingValue } from '../is-setting-value';
import { ProjectSettings } from '../../types';
@@ -22,12 +22,14 @@ const settingKeys = Object.keys(settingMap).sort() as unknown as readonly [
export type PartialProjectSettings = Pick<ProjectSettings, ConfigKeys>;
export default async function editProjectSettings(
output: Output,
client: Client,
projectSettings: PartialProjectSettings | null,
framework: Framework | null,
autoConfirm: boolean,
localConfigurationOverrides: PartialProjectSettings | null
): Promise<ProjectSettings> {
const { output } = client;
// Create initial settings object defaulting everything to `null` and assigning what may exist in `projectSettings`
const settings: ProjectSettings = Object.assign(
{
@@ -118,7 +120,7 @@ export default async function editProjectSettings(
// Prompt the user if they want to modify any settings not defined by local configuration.
if (
autoConfirm ||
!(await confirm('Want to modify these settings?', false))
!(await confirm(client, 'Want to modify these settings?', false))
) {
return settings;
}

View File

@@ -1,5 +1,4 @@
import Client from '../client';
import inquirer from 'inquirer';
import confirm from './confirm';
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
import chalk from 'chalk';
@@ -47,11 +46,16 @@ export default async function inputProject(
if (!detectedProject) {
// did not auto-detect a project to link
shouldLinkProject = await confirm(`Link to existing project?`, false);
shouldLinkProject = await confirm(
client,
`Link to existing project?`,
false
);
} else {
// auto-detected a project to link
if (
await confirm(
client,
`Found project ${chalk.cyan(
`${org.slug}/${detectedProject.name}`
)}. Link to it?`,
@@ -63,6 +67,7 @@ export default async function inputProject(
// user doesn't want to link the auto-detected project
shouldLinkProject = await confirm(
client,
`Link to different existing project?`,
true
);
@@ -73,7 +78,7 @@ export default async function inputProject(
let project: Project | ProjectNotFound | null = null;
while (!project || project instanceof ProjectNotFound) {
const answers = await inquirer.prompt({
const answers = await client.prompt({
type: 'input',
name: 'existingProjectName',
message: `Whats the name of your existing project?`,
@@ -104,7 +109,7 @@ export default async function inputProject(
let newProjectName: string | null = null;
while (!newProjectName) {
const answers = await inquirer.prompt({
const answers = await client.prompt({
type: 'input',
name: 'newProjectName',
message: `Whats your projects name?`,

View File

@@ -1,12 +1,11 @@
import path from 'path';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { Output } from '../output';
import { validateRootDirectory } from '../validate-paths';
import Client from '../client';
export async function inputRootDirectory(
client: Client,
cwd: string,
output: Output,
autoConfirm = false
) {
if (autoConfirm) {
@@ -15,7 +14,7 @@ export async function inputRootDirectory(
// eslint-disable-next-line no-constant-condition
while (true) {
const { rootDirectory } = await inquirer.prompt({
const { rootDirectory } = await client.prompt({
type: 'input',
name: 'rootDirectory',
message: `In which directory is your code located?`,
@@ -38,7 +37,7 @@ export async function inputRootDirectory(
if (
(await validateRootDirectory(
output,
client.output,
cwd,
fullPath,
'Please choose a different one.'

View File

@@ -1,5 +1,6 @@
import inquirer from 'inquirer';
import stripAnsi from 'strip-ansi';
import Client from '../client';
import eraseLines from '../output/erase-lines';
interface ListEntry {
@@ -35,21 +36,24 @@ function getLength(input: string): number {
return biggestLength;
}
export default async function list({
message = 'the question',
// eslint-disable-line no-unused-vars
choices: _choices = [
{
name: 'something\ndescription\ndetails\netc',
value: 'something unique',
short: 'generally the first line of `name`',
},
],
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
separator = false, // Puts a blank separator between each choice
abort = 'end', // Whether the `abort` option will be at the `start` or the `end`,
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
}: ListOptions): Promise<string> {
export default async function list(
client: Client,
{
message = 'the question',
// eslint-disable-line no-unused-vars
choices: _choices = [
{
name: 'something\ndescription\ndetails\netc',
value: 'something unique',
short: 'generally the first line of `name`',
},
],
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
separator = false, // Puts a blank separator between each choice
abort = 'end', // Whether the `abort` option will be at the `start` or the `end`,
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
}: ListOptions
): Promise<string> {
require('./patch-inquirer-legacy');
let biggestLength = 0;
@@ -106,7 +110,7 @@ export default async function list({
choices.push(abortSeparator, _abort);
}
const answer = await inquirer.prompt({
const answer = await client.prompt({
name: 'value',
type: 'list',
default: selected,

View File

@@ -1,25 +1,26 @@
import chalk from 'chalk';
import type { ReadableTTY, WritableTTY } from '../../types';
type Options = {
abortSequences?: Set<string>;
defaultValue?: boolean;
noChar?: string;
resolveChars?: Set<string>;
stdin?: NodeJS.ReadStream;
stdout?: NodeJS.WriteStream;
stdin: ReadableTTY;
stdout: WritableTTY;
trailing?: string;
yesChar?: string;
};
export default async function promptBool(label: string, options: Options = {}) {
export default async function promptBool(label: string, options: Options) {
const {
stdin,
stdout,
defaultValue = false,
abortSequences = new Set(['\u0003']),
resolveChars = new Set(['\r']),
yesChar = 'y',
noChar = 'n',
stdin = process.stdin,
stdout = process.stdout,
trailing = '',
} = options;

View File

@@ -1,13 +1,17 @@
export default async function readStandardInput(): Promise<string> {
import type { ReadableTTY } from '../../types';
export default async function readStandardInput(
stdin: ReadableTTY
): Promise<string> {
return new Promise<string>(resolve => {
setTimeout(() => resolve(''), 500);
if (process.stdin.isTTY) {
if (stdin.isTTY) {
// found tty so we know there is nothing piped to stdin
resolve('');
} else {
process.stdin.setEncoding('utf8');
process.stdin.once('data', resolve);
stdin.setEncoding('utf8');
stdin.once('data', resolve);
}
});
}

View File

@@ -56,7 +56,7 @@ export default async function setupAndLink(
return { status: 'error', exitCode: 1, reason: 'PATH_IS_FILE' };
}
const link = await getLinkedProject(client, path);
const isTTY = process.stdout.isTTY;
const isTTY = client.stdin.isTTY;
const quiet = !isTTY;
let rootDirectory: string | null = null;
let sourceFilesOutsideRootDirectory = true;
@@ -80,6 +80,7 @@ export default async function setupAndLink(
const shouldStartSetup =
autoConfirm ||
(await confirm(
client,
`${setupMsg} ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
@@ -120,7 +121,7 @@ export default async function setupAndLink(
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
} else {
const project = projectOrNewProjectName;
@@ -224,7 +225,7 @@ export default async function setupAndLink(
const { projectSettings, framework } = deployment;
settings = await editProjectSettings(
output,
client,
projectSettings,
framework,
autoConfirm,

View File

@@ -176,7 +176,7 @@ async function getVerificationTokenOutOfBand(client: Client, url: URL) {
output.log(
`After login is complete, enter the verification code printed in your browser.`
);
const verificationToken = await readInput('Verification code:');
const verificationToken = await readInput(client, 'Verification code:');
output.print(eraseLines(6));
// If the pasted token begins with "saml_", then the `ssoUserId` was returned.

View File

@@ -1,4 +1,3 @@
import inquirer from 'inquirer';
import Client from '../client';
import error from '../output/error';
import listInput from '../input/list';
@@ -32,7 +31,7 @@ export default async function prompt(
choices.pop();
}
const choice = await listInput({
const choice = await listInput(client, {
message: 'Log in to Vercel',
choices,
});
@@ -44,22 +43,26 @@ export default async function prompt(
} else if (choice === 'bitbucket') {
result = await doBitbucketLogin(client, outOfBand, ssoUserId);
} else if (choice === 'email') {
const email = await readInput('Enter your email address:');
const email = await readInput(client, 'Enter your email address:');
result = await doEmailLogin(client, email, ssoUserId);
} else if (choice === 'saml') {
const slug = error?.teamId || (await readInput('Enter your Team slug:'));
const slug =
error?.teamId || (await readInput(client, 'Enter your Team slug:'));
result = await doSamlLogin(client, slug, outOfBand, ssoUserId);
}
return result;
}
export async function readInput(message: string): Promise<string> {
export async function readInput(
client: Client,
message: string
): Promise<string> {
let input;
while (!input) {
try {
const { val } = await inquirer.prompt({
const { val } = await client.prompt({
type: 'input',
name: 'val',
message,

View File

@@ -14,7 +14,7 @@ export default async function reauthenticate(
client.output.log(
`You must re-authenticate with SAML to use ${bold(error.scope)} scope.`
);
if (await confirm(`Log in with SAML?`, true)) {
if (await confirm(client, `Log in with SAML?`, true)) {
return doSamlLogin(client, error.teamId);
}
} else {

View File

@@ -1,41 +1,39 @@
import chalk from 'chalk';
import renderLink from './link';
import wait, { StopSpinner } from './wait';
import { Writable } from 'stream';
import type { WritableTTY } from '../../types';
export interface OutputOptions {
debug?: boolean;
}
export interface PrintOptions {
w?: Writable;
}
export interface LogOptions extends PrintOptions {
export interface LogOptions {
color?: typeof chalk;
}
export class Output {
stream: WritableTTY;
debugEnabled: boolean;
private spinnerMessage: string;
private _spinner: StopSpinner | null;
isTTY: boolean;
constructor({ debug: debugEnabled = false }: OutputOptions = {}) {
constructor(
stream: WritableTTY,
{ debug: debugEnabled = false }: OutputOptions = {}
) {
this.stream = stream;
this.debugEnabled = debugEnabled;
this.spinnerMessage = '';
this._spinner = null;
this.isTTY = process.stdout.isTTY || false;
}
isDebugEnabled = () => {
return this.debugEnabled;
};
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
print = (str: string) => {
this.stopSpinner();
const stream: Writable = w || process.stderr;
stream.write(str);
this.stream.write(str);
};
log = (str: string, color = chalk.grey) => {
@@ -111,11 +109,17 @@ export class Output {
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
return;
}
if (this.isTTY) {
if (this.stream.isTTY) {
if (this._spinner) {
this._spinner.text = message;
} else {
this._spinner = wait(message, delay);
this._spinner = wait(
{
text: message,
stream: this.stream,
},
delay
);
}
} else {
this.print(`${message}\n`);
@@ -157,7 +161,3 @@ export class Output {
return promise;
};
}
export default function createOutput(opts?: OutputOptions) {
return new Output(opts);
}

View File

@@ -1,2 +1,2 @@
export { default, Output } from './create-output';
export { Output } from './create-output';
export { StopSpinner } from './wait';

View File

@@ -8,14 +8,19 @@ export interface StopSpinner {
}
export default function wait(
msg: string,
delay: number = 300,
_ora = ora
opts: ora.Options,
delay: number = 300
): StopSpinner {
let spinner: ReturnType<typeof _ora> | null = null;
let text = opts.text;
let spinner: ora.Ora | null = null;
if (typeof text !== 'string') {
throw new Error(`"text" is required for Ora spinner`);
}
const timeout = setTimeout(() => {
spinner = _ora(chalk.gray(msg));
spinner = ora(opts);
spinner.text = chalk.gray(text);
spinner.color = 'gray';
spinner.start();
}, delay);
@@ -29,23 +34,21 @@ export default function wait(
}
};
stop.text = msg;
stop.text = text;
// Allow `text` property to update the text while the spinner is in action
Object.defineProperty(stop, 'text', {
get() {
return msg;
return text;
},
set(v: string) {
msg = v;
text = v;
if (spinner) {
spinner.text = chalk.gray(v);
}
},
});
// @ts-ignore
process.once('nowExit', stop);
return stop;
}

View File

@@ -1,11 +1,10 @@
import chalk from 'chalk';
import { Response } from 'node-fetch';
import { emoji, EmojiLabel, prependEmoji } from './emoji';
import createOutput from './output';
import Client from './client';
import linkStyle from './output/link';
import { emoji, EmojiLabel, prependEmoji } from './emoji';
export default function printIndications(res: Response) {
const _output = createOutput();
export default function printIndications(client: Client, res: Response) {
const indications = new Set(['warning', 'notice', 'tip']);
const regex = /^x-(?:vercel|now)-(warning|notice|tip)-(.*)$/;
@@ -25,7 +24,7 @@ export default function printIndications(res: Response) {
chalk.dim(`${action || 'Learn More'}: ${linkStyle(link)}`) +
newline;
}
_output.print(message + finalLink);
client.output.print(message + finalLink);
}
}
}

View File

@@ -5,6 +5,7 @@ import chalk from 'chalk';
import { homedir } from 'os';
import confirm from './input/confirm';
import toHumanPath from './humanize-path';
import Client from './client';
const stat = promisify(lstatRaw);
@@ -51,9 +52,11 @@ export async function validateRootDirectory(
}
export default async function validatePaths(
output: Output,
client: Client,
paths: string[]
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
const { output } = client;
// can't deploy more than 1 path
if (paths.length > 1) {
output.print(`${chalk.red('Error!')} Can't deploy more than one path.\n`);
@@ -85,6 +88,7 @@ export default async function validatePaths(
// ask confirmation if the directory is home
if (path === homedir()) {
const shouldDeployHomeDirectory = await confirm(
client,
`You are deploying your home directory. Do you want to continue?`,
false
);

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'invalid-runtime-value',
};
export default async function edge(request, event) {
throw new Error('intentional runtime error');
}

View File

@@ -0,0 +1,8 @@
export const config = {
runtime: 'experimental-edge',
};
export async function notTheDefaultExport(request, event) {
// this will never be run
return new Response('some response body');
}

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
throw new Error('intentional runtime error');
}

View File

@@ -0,0 +1,10 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
// this should never be executed
return new Response('some response body');
}
throw new Error('intentional startup error');

View File

@@ -0,0 +1,9 @@
export const config = {
runtime: 'experimental-edge'
}
export default async function edge(request, event) {
return new Response('some response body');
// intentional missing closing bracket to produce syntax error
// }

View File

@@ -0,0 +1,9 @@
import unknownModule from 'unknown-module-893427589372458934795843';
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
return new Response(unknownModule('some response body'));
}

View File

@@ -0,0 +1,9 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
return new Response('responding with intentional 500 from user code', {
status: 500,
});
}

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