Compare commits

..

119 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
JJ Kasper
a247e31688 Publish Stable
- @vercel/next@3.0.3
2022-06-17 14:49:54 -05:00
JJ Kasper
dc02e763a4 Publish Canary
- vercel@25.1.1-canary.5
 - @vercel/next@3.0.3-canary.2
2022-06-17 14:24:39 -05:00
JJ Kasper
8567fc0de6 [next] Optimize _next/data route regex (#7992)
Optimize _next/data route regex
2022-06-17 14:19:37 -05:00
JJ Kasper
4f8f3d373f Publish Canary
- vercel@25.1.1-canary.4
 - @vercel/next@3.0.3-canary.1
 - @vercel/node@2.2.1-canary.0
2022-06-16 18:00:29 -05:00
JJ Kasper
debb85b690 [next] Update error for internal missing page (#7987)
Update error for internal missing page
2022-06-16 16:44:12 -05:00
Thai Pangsakulyanont
bfef989ada [examples] Delete pnpm-lock.yaml from examples/sveltekit because it already contains yarn.lock (#7983)
Delete pnpm-lock.yaml

Co-authored-by: Lee Robinson <lrobinson2011@gmail.com>
2022-06-16 10:24:18 -07:00
JJ Kasper
4e0b6c5eaf [next] Update to skip middleware for on-demand revalidate (#7978)
Update to skip middleware for on-demand revalidate
2022-06-16 11:23:30 -05:00
Nathan Rajlich
0ace69ef75 Publish Stable
- @vercel/node@2.2.0
2022-06-15 13:01:57 -07:00
Nathan Rajlich
b7b7923f92 Publish Canary
- vercel@25.1.1-canary.3
 - @vercel/next@3.0.3-canary.0
 - @vercel/node@2.1.1-canary.2
2022-06-15 11:56:32 -07:00
Nathan Rajlich
8167233c56 [tests] Create tarballs of all packages in Vercel deployment (#7967)
For the Vercel deployment, run `yarn pack` in each of the packages in the monorepo and place them in the "public/tarballs" directory so that we can have npm-installable URLs of each package for every commit. The `package.json` of each package is also updated to reference the tarball when a package depends on other packages within the monorepo.

Try it out like:

```
$ npm i -g https://vercel-biww73ffq.vercel.sh/tarballs/vercel.tgz

# Notice how the package.json has the monorepo dependencies
# updated to point to the related tarballs from the same deployment:

$ cat /usr/local/lib/node_modules/vercel/package.json | grep biww
    "@vercel/build-utils": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/build-utils.tgz",
    "@vercel/go": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/go.tgz",
    "@vercel/next": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/next.tgz",
    "@vercel/node": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/node.tgz",
    "@vercel/python": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/python.tgz",
    "@vercel/redwood": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/redwood.tgz",
    "@vercel/remix": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/remix.tgz",
    "@vercel/ruby": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/ruby.tgz",
    "@vercel/static-build": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/static-build.tgz",
    "@vercel/client": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/client.tgz",
    "@vercel/frameworks": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/frameworks.tgz",

# Also notice that the "version" in `package.json` gets updated to include the commit
# SHA so that we can easily identify which commit a given tarball was generated from:

$ vercel --version
25.1.1-canary.1-727b290
```
2022-06-15 17:44:23 +00:00
Michaël De Boey
32ee6aba92 [node] Move @vercel/build-utils to "dependencies" (#7934)
fix(node): move `@vercel/build-utils` to `dependencies`

Co-authored-by: Steven <steven@ceriously.com>
2022-06-15 13:18:02 -04:00
Nathan Rajlich
b48f7a7e6e [node] Add missing dependencies for dev-server.ts (#7969)
Some new imports were introduced recently but are missing from the `"dependencies"` object, so they were missing when the package was installed from the npm registry.
2022-06-15 01:38:17 +00:00
JJ Kasper
a961c9b992 Publish Stable
- @vercel/next@3.0.2
2022-06-14 17:35:24 -05:00
JJ Kasper
cf7c50d691 Publish Canary
- vercel@25.1.1-canary.2
 - @vercel/next@3.0.2-canary.2
2022-06-14 14:57:54 -05:00
JJ Kasper
f4be388a1f [next] Fix _next/data resolving priority with afterFiles (#7968)
* Fix _next/data resolving priority with afterFiles

* ensure x-nextjs-matched-path header is set
2022-06-14 14:52:49 -05:00
Nathan Rajlich
c1bc53dea8 Publish Canary
- @vercel/build-utils@4.1.1-canary.1
 - vercel@25.1.1-canary.1
 - @vercel/client@12.0.2-canary.1
 - @vercel/frameworks@1.0.2-canary.0
 - @vercel/go@2.0.2-canary.1
 - @vercel/next@3.0.2-canary.1
 - @vercel/node@2.1.1-canary.1
 - @vercel/python@3.0.2-canary.1
 - @vercel/redwood@1.0.2-canary.1
 - @vercel/remix@1.0.2-canary.1
 - @vercel/routing-utils@1.13.5-canary.0
 - @vercel/ruby@1.3.10-canary.1
 - @vercel/static-build@1.0.2-canary.1
2022-06-14 09:15:20 -07:00
Nathan Rajlich
6855e3df54 [tests] Allow for multiple probes (#7957)
The `probes` in the vercel.json files in tests fixtures weren't allowing both `mustContain` _and_ `responseHeaders` probes due to an `else` block. This PR allows to combine those checks into the same probe test.
2022-06-14 06:54:16 +00:00
Nathan Rajlich
0d39dbd1d9 [routing-utils] Convert tests to TypeScript (#7959)
Allows for the tests to be run without rebuilding the source code.
2022-06-13 23:29:00 -07:00
Nathan Rajlich
509c85182a [tests] Update ts-jest to v28.0.5 (#7962)
Gets rid of the annoying ts-jest update warning:

```
ts-jest[ts-jest-transformer] (WARN) Use 'ts-jest' entry point in v28 will resolve in refactored transformer. If you wish to use legacy transformer, please adjust your Jest 'transform' config. For example:
     module.exports = {
        transform: {
           '^.+\\.tsx?$': 'ts-jest/legacy'
        }
     }
```
2022-06-13 23:18:23 -07:00
Sean Massa
ae801e563d [cli] add support for Edge Functions in vercel dev (#7925)
Add support for Edge Functions when using `vercel dev`. This uses the [`edge-runtime`](https://edge-runtime.vercel.app/) to emulate the proper environment, [`ncc`](https://github.com/vercel/ncc) to parse the entrypoint (supporting TypeScript and importing node modules), and some custom marshaling to pass the request/response values through.
2022-06-13 22:50:16 +00:00
Steven
0e8278f490 [cli] Fix vc dev when user has ipv6 configured (#7956)
Running tests from #7455 

- Fixes #7306
- Closes #7455
2022-06-13 21:04:00 +00:00
Steven
0d302a6f48 [cli] Add alias for vc develop => vc dev (#7955)
This will make it easier for users who are used to `gatsby develop` or other CLIs that abbreviate to [`develop`](https://twitter.com/DiverseCauses/status/1536358138676486146).

Fixes this:
![image](https://user-images.githubusercontent.com/229881/173387083-7afd01db-00ab-4670-a07d-fa6c8cecea5d.png)
2022-06-13 17:01:46 +00:00
Lee Robinson
4e4f5f28a2 [examples] Add Vercel Analytics for CRA/SvelteKit templates. (#7894)
Successfully tested with:

- [CRA](https://front-git-analytics-for-all-frameworks.vercel.sh/now-examples/create-react-app/analytics?device=desktop&timePeriod=1d&percentile=75&timeInterval=3h&sort=datapoints&search=)
- [SvelteKit](https://front-git-analytics-for-all-frameworks.vercel.sh/now-examples/sveltekit/analytics?device=desktop&timePeriod=1d&percentile=75&timeInterval=3h&sort=datapoints&search=)

Dependencies have also been updated 👍
2022-06-13 16:30:14 +00:00
Steven
5205a4ec4b [cli] Fix vc dev with dynamic paths and ESM (#7946)
In a previous PR, the entrypoint path extension was stripped in `vc dev` to match the behavior of production deployments (`/api/user.js` => `/api/user`). However, it was supposed to map back to the original file extension before invoking the matching builder. This PR fixes the mapping for dynamic paths with ESM, such as `/api/[id].mjs` => `/api/[id]`.
2022-06-12 17:54:21 +00:00
Nathan Rajlich
2c15e496ed [tests] Remove as any casting in unit tests (#7945)
Maybe this casting was necessary at some point, but it doesn't seem to be anymore.
2022-06-10 22:30:51 +00:00
Matthew Stanciu
1f0ca46626 [cli] Add git metadata to vc deploy (#7910)
Currently, git metadata is not included on the Vercel dashboard when a project is deployed via the CLI. This PR populates the git metadata.

### 📋 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-10 20:53:47 +00:00
Steven
17cb5f1bc6 [cli] Remove boxen for warnings (#7944)
This never worked correctly and its too verbose. Lets remove boxen in favor of yellow text.

## Before

<img width="1432" alt="before" src="https://user-images.githubusercontent.com/229881/173138338-1cd1c81c-f294-4752-a08f-f80466fbd21c.png" />

## After

<img width="1432" alt="after" src="https://user-images.githubusercontent.com/229881/173138813-f9ff4de1-5bb7-4b60-8609-6abdd0e86fa0.png">
2022-06-10 20:28:00 +00:00
Nathan Rajlich
b095031292 [cli] Normalize "src" property in vc build (#7943)
This matches the production behavior.
2022-06-10 18:53:01 +00:00
Ethan Arrowood
f50bcbc0ba [cli] Implement vercel.json configuration overrides (#7811)
### Related Issues

This PR adds vercel.json configuration overrides support for both new and existing project flows through the CLI. It includes integration and unit tests.

### 📋 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-10 16:46:46 +00:00
JJ Kasper
4bf6295d7a Publish Canary
- @vercel/build-utils@4.1.1-canary.0
 - vercel@25.1.1-canary.0
 - @vercel/client@12.0.2-canary.0
 - @vercel/go@2.0.2-canary.0
 - @vercel/next@3.0.2-canary.0
 - @vercel/node@2.1.1-canary.0
 - @vercel/python@3.0.2-canary.0
 - @vercel/redwood@1.0.2-canary.0
 - @vercel/remix@1.0.2-canary.0
 - @vercel/ruby@1.3.10-canary.0
 - @vercel/static-build@1.0.2-canary.0
2022-06-10 09:37:52 -05:00
JJ Kasper
a4001ce10b [next] Add handling for resolving _next/data URLs (#7935)
* Add handling for resolving _next/data URLs

* update check

* update routes

* remove extra routes

* Update resolving
2022-06-10 08:50:59 -05:00
Nathan Rajlich
2df3432d88 [node] Add zero-config support for root-level middleware.js/middleware.ts file (#7928)
Adds support for `config.middleware` property in `@vercel/node` to output an Edge Middleware (by including a catch-all route in the output).

Also updates the zero-config detection logic to schedule a build of root-level `middleware.js`/`middleware.ts` files with `@vercel/node` with this middleware option enabled.
2022-06-09 23:04:39 +00:00
chloetedder
bcfc19de12 [build-utils] Add getProjectPaths that will evaluate if there is a monorepo (#7826)
<img width="1174" alt="Screen Shot 2022-05-19 at 3 53 55 PM" src="https://user-images.githubusercontent.com/35947020/169402845-4eb70a24-9954-44ac-8726-a6b56161d58c.png">

If there is no workspace detected, we still want to check if there are potential projects in the monorepo. An example of this case would be:

<img width="202" alt="Screen Shot 2022-05-19 at 10 29 12 AM" src="https://user-images.githubusercontent.com/35947020/169336400-e69845e3-616e-4857-80fe-c2614a65352a.png">

The directories `backend/app-three`, `frontend/app-one`, `frontend/app-two` all have definitions within their `package.json` files that define a framework assosiated with the project. This is what distinguishes them from the `package.json` files within `backend` and `frontend`

`getProjectPaths` loops through a depth of 3 (this can be changed but in most cases of this it won't be more than 3). For each directory it recursively calls itself until it finds a `package.json` file. It then checks if the directory has a framework defined and if so it adds the path to the final return value of all projects.

If any `skipPaths` are sent into the function, no projects will be looked for within those path(s). This would include the case for a hybrid monorepo where a workspace has already been detected

### Related Issues

> Fixes #1
> Related to #2

### 📋 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-09 15:15:25 +00:00
Matthew Stanciu
04381c669b [cli] Pull correct env variables in vc build command (#7933)
`vc build` runs `vc pull` if no project settings are found locally. By default, `vc pull` pulls development environment variables, but we want either preview or production environment variables in the context of `vc build`. This PR adds a flag to the internal `vc pull` call to pull the correct environment variables.

### 📋 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-09 06:15:30 +00:00
Steven
0c7b54edad Publish Stable
- @vercel/build-utils@4.1.0
 - vercel@25.1.0
 - @vercel/client@12.0.1
 - @vercel/go@2.0.1
 - @vercel/next@3.0.1
 - @vercel/node@2.1.0
 - @vercel/python@3.0.1
 - @vercel/redwood@1.0.1
 - @vercel/remix@1.0.1
 - @vercel/ruby@1.3.9
 - @vercel/static-build@1.0.1
 - @vercel/static-config@2.0.1
2022-06-08 10:44:17 -04:00
Steven
6d42816395 Publish Canary
- vercel@25.0.2-canary.1
 - @vercel/next@3.0.1-canary.1
 - @vercel/static-build@1.0.1-canary.1
2022-06-08 08:48:04 -04:00
Seiya Nuta
6fe6d05a42 Increase the maximum size of an edge function to 1MiB (#7903) 2022-06-08 16:30:49 +09:00
Steven
50a201f145 [cli][static-build] Fix case when vc dev is throwing the wrong error w/ BOAv3 (#7927)
Fixes an issue brought up in https://github.com/vercel/og-image/issues/207

The problem is that we allow a corner case with `vc dev` to run the build script, but this can break if the user ran `vc build` first due to the BOA V3 detection.

The workaround is to delete the BOA V3 directory when there is no Development Command provided since that will run the Build Command during development.

We also add a nicer error in the case when someone actually intended to emit BOA V3.
2022-06-07 21:31:43 +00:00
Gal Schlezinger
701a02ae9d [next] Support Edge API Endpoints (#7905)
* Add regression test for nested middleware

* [next] Allow edge api endpoints in Next.js

This reverts commit d4cef69cc9 (#7898)

* delete `functions`, not `middleware`

* Add an assertion

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-06-07 23:36:46 +03:00
agadzik
39f7586621 Publish Canary
- @vercel/build-utils@4.0.1-canary.0
 - vercel@25.0.2-canary.0
 - @vercel/client@12.0.1-canary.0
 - @vercel/go@2.0.1-canary.0
 - @vercel/next@3.0.1-canary.0
 - @vercel/node@2.0.2-canary.0
 - @vercel/python@3.0.1-canary.0
 - @vercel/redwood@1.0.1-canary.0
 - @vercel/remix@1.0.1-canary.0
 - @vercel/ruby@1.3.9-canary.0
 - @vercel/static-build@1.0.1-canary.0
 - @vercel/static-config@2.0.1-canary.0
2022-06-07 12:33:07 -04:00
Andrew Gadzik
c4a39c8d29 [build-utils] Add getWorkspacePackagePaths function (#7859)
Gets the package paths from the workspace definition for a given git repository

```ts
/** 
 *  Gets the list of workspace package paths based on the configuration 
 * 
 * @example
 *
 * my-repo
 * |-- packages
 * |    |-- api-1
 * |    |-- api-2
 * |-- package.json
 * |-- pnpm-workspaces.yaml // packages: ["packages/*"]
 *
 * const fs = new ... // based on 'my-repo'
 * const workspace = { ..., type: "pnpm" }
 * getWorkspacePackagePaths({ fs, workspace }) => [
 *   "my-repo/packages/api-1",
 *   "my-repo/packages/api-2"
 * ]
 */
function getWorkspacePackagePaths(fs: DetectorFilesystem, workspace: Workspace): string[]
```

### Related Issues

Closes https://github.com/vercel/vercel/issues/7749

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-06-07 15:55:23 +00:00
Nathan Rajlich
3ac238cf08 [node] Add support for EdgeFunction output type (#7877)
Adds support for `@vercel/node` to return an `EdgeFunction` output when it detects `runtime: 'experimental-edge'` from the static `config` object. This is implemented in `@vercel/node` so that it can share the same file extension (`.js`/`.ts`) as Node.js Serverless Functions without modifying the zero-config detection logic.

**Example:**

```js
export const config = {
  runtime: 'experimental-edge',
};

export default req => {
  return new Response(`Hi, from an Edge Function!`);
};
```
2022-06-07 02:20:08 +00:00
Nathan Rajlich
8384813a0d [cli] Output EdgeFunctions from version 3 Builders in vc build (#7924)
Serializes `EdgeFunction` outputs in `vc build` when provided by a version 3 Builder.
2022-06-07 01:42:33 +00:00
Nathan Rajlich
c4587de439 [cli] Add initial vc build unit tests (#7869)
Adds some initial unit tests for the `vc build` command which includes some fixes for Windows.
2022-06-06 22:16:18 +00:00
Nathan Rajlich
d997dc4fbc [static-config] Use "runtime" instead of "use" (#7922)
The static configuration property name has been decided to be `runtime`.

This is a precursor for #7877.
2022-06-06 19:02:32 +00:00
Nathan Rajlich
d15b90bd4d [build-utils] Allow EdgeFunction as a valid output type for version 3 Builders (#7923)
A version 3 Builder is allowed to output an `EdgeFunction` output type. This already is handled correctly by the Vercel build infrastructure.
2022-06-06 18:42:14 +00:00
JJ Kasper
5b31297f0c [next] Ensure middleware route comes before beforeFiles rewrites (#7912)
* Ensure middleware route comes before beforeFiles rewrites

* update test

* add version lock and more tests

* update nested middleware test for version lock
2022-06-06 13:03:31 -04:00
Steven
e232566cbe Publish Stable
- vercel@25.0.1
 - @vercel/node@2.0.1
2022-06-05 21:38:44 -04:00
Steven
592689cad1 Publish Canary
- vercel@25.0.1-canary.0
 - @vercel/node@2.0.1-canary.0
2022-06-05 20:58:34 -04:00
Steven
9b08e72f76 [node] Skip TS compile for .d.ts assets (#7918)
Fixes an issue where `.d.ts` assets were incorrectly being compiled after a fix to nft in https://github.com/vercel/nft/pull/289

- [Repro Steps](https://github.com/cheapsteak/repro-vercel-cli-build-dts-error)
- [Incident](https://app.kintaba.com/incident/755531679765890512)
- [Slack](https://vercel.slack.com/archives/C03JNHA844R/p1654334373086879)

<img src="https://user-images.githubusercontent.com/229881/172055403-d2edfe1d-3a60-459f-bf40-506fea1d1ca4.png" height=500 />
2022-06-06 00:36:41 +00:00
Sean Massa
bd0e10cfe7 [cli] update vc env pull --help (#7911)
* update `vc env pull --help`

* update `vc env pull` help documentation

* try longer timeout

* remove dupe "env"

* remove unused --env option

* Update packages/cli/src/commands/env/index.ts

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

Co-authored-by: Steven <steven@ceriously.com>
2022-06-05 16:22:32 -05:00
Sean Massa
28436ade60 [tests] increase test timeout; it has been failing recently (#7913) 2022-06-03 10:06:36 -05:00
Sean Massa
de0d2fba0b Publish Stable
- @vercel/build-utils@4.0.0
 - vercel@25.0.0
 - @vercel/client@12.0.0
 - @vercel/frameworks@1.0.1
 - @vercel/go@2.0.0
 - @vercel/next@3.0.0
 - @vercel/node-bridge@3.0.0
 - @vercel/node@2.0.0
 - @vercel/python@3.0.0
 - @vercel/redwood@1.0.0
 - @vercel/remix@1.0.0
 - @vercel/routing-utils@1.13.4
 - @vercel/ruby@1.3.8
 - @vercel/static-build@1.0.0
 - @vercel/static-config@2.0.0
2022-06-02 14:17:21 -05:00
Sean Massa
e0900128d6 [cli][client][tests] update to node14-compatible target (drop support for node12) (#7865)
Node 12 is EOL and we're starting to reference packages that don't install on node lower than 14, such as nuxt@3 and remix. Let's update to 14 for now.

---

Reimplements: https://github.com/vercel/vercel/pull/7819
2022-06-02 19:01:34 +00:00
Nathan Rajlich
8d15f30579 [tests] Remove ava compileEnhancements property from package.json (#7904)
VS Code was complaining about this property, so I guess it's not doing anything:

> Property `compileEnhancements` is not allowed.
2022-06-01 21:14:01 +00:00
Nathan Rajlich
960c66584c [build-utils] Make repoRootPath non-optional (#7909)
This value is always set in the Vercel build infra, so mark the type as non-optional.
2022-06-01 19:39:45 +00:00
JJ Kasper
1c8f91031a [next] Add regression test for nested middleware (#7901)
Add regression test for nested middleware
2022-06-01 10:04:16 -05:00
Steven
68cb23c3cc [cli] Add experimental corepack support (#7871)
This PR adds support for experimental [corepack](https://nodejs.org/api/corepack.html) to `vc build`.

Since this is still experimental, we only enable if the env var `ENABLE_EXPERIMENTAL_COREPACK=1` is set.

#### 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`
2022-06-01 14:31:33 +00:00
Nathan Rajlich
94f6ae2595 [examples] Remove ENABLE_FILE_SYSTEM_API env var from "remix" template (#7886)
Since the `@vercel/remix` Builder is now being used, this environment variable is no longer necessary.
2022-06-01 01:16:24 +00:00
Steven
b92aeac84d [build-utils] Fix warning for package.json engines (#7900)
This PR updates the way we handle warning for engines.node in `package.json`.

- should not warn when the engines version satisfies the project settings (previously it was an exact match)
- should warn when engines version is exact instead of range since it cannot be satisfied exactly
- should warn when engines version is greater than since it might introduce breaking changes for a future node.js version
2022-06-01 00:15:00 +00:00
JJ Kasper
00420b7a01 Publish Canary
- vercel@24.2.6-canary.0
 - @vercel/next@2.9.1-canary.0
 - @vercel/static-config@1.0.2-canary.0
2022-05-31 18:52:50 -05:00
Steven
a5128790d0 [tests] Fix turbo cache invalidation for GH Actions RUNNER_OS (#7899)
This PR fixes an issue where turbo was caching the result regardless of OS by adding the env var `RUNNER_OS` to the cache key.

https://docs.github.com/en/actions/learn-github-actions/environment-variables#detecting-the-operating-system
2022-05-31 22:42:26 +00:00
Seiya Nuta
ae9aa91f4f [static-config] Support extracting export const config from swc's AST (#7791)
This PR adds support for extracting `config` from swc's AST. `static-config` supports parsing from the source program but in some cases we already have parsed an AST to do other static code analysis.

### Related Issues

None

### 📋 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-05-31 20:44:29 +00:00
JJ Kasper
d4cef69cc9 Revert "[next] Allow edge api endpoints in Next.js" (#7898)
Reverts vercel/vercel#7855

We can re-land after we ensure the cases we have found failing are resolved.
2022-05-31 20:07:46 +00:00
Sean Massa
323f67c31a [cli] update update-notifier to fix notification of non-latest (#7897)
Looks like this is a bug in update-notifier, but they [smoothed it over](https://github.com/yeoman/update-notifier/pull/192) with a change that was deployed in [`5.0.0`](https://github.com/yeoman/update-notifier/releases/tag/v5.0.0). We’re currently on `4.1.0`.

This can cause an update notification for a PREVIOUS version, like so:

```
$  vc --version
> UPDATE AVAILABLE Run `npm i -g vercel@latest` to install Vercel CLI 24.2.5-canary.3
> Changelog: https://github.com/vercel/vercel/releases/tag/vercel@24.2.5-canary.3
Vercel CLI 24.2.5
24.2.5
```

While we're here, also sets the [`updatecheckinterval`](https://github.com/yeoman/update-notifier#updatecheckinterval) to 1 week.

---

Previous PR: https://github.com/vercel/vercel/pull/4896
2022-05-31 19:14:39 +00:00
Steven
63c499a826 [tests] Update domain tests from .org to .com (#7896)
Use a regex matcher and change the .org to .com
2022-05-31 18:07:25 +00:00
600 changed files with 30912 additions and 12338 deletions

4
.gitattributes vendored
View File

@@ -8,3 +8,7 @@ packages/*/test/* linguist-vendored
# Go build fails with Windows line endings. # Go build fails with Windows line endings.
*.go text eol=lf *.go text eol=lf
go.mod text eol=lf go.mod text eol=lf
# Mark certain files as "binary" -- hide diffs
**/test/fixtures/**/git/**/* binary
**/test/fixtures/**/git/**/* linguist-generated

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

@@ -1,18 +1 @@
* packages/*/test/**
# general
!utils/
!utils/run.js
!.yarnrc
!yarn.lock
!package.json
!turbo.json
# api
!api/
!api/**
# packages
!packages/
!packages/frameworks
!packages/frameworks/**

View File

@@ -1,5 +1,6 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import { join } from 'path'; import { join, dirname } from 'path';
import execa from 'execa';
import { getExampleList } from '../examples/example-list'; import { getExampleList } from '../examples/example-list';
import { mapOldToNew } from '../examples/map-old-to-new'; import { mapOldToNew } from '../examples/map-old-to-new';
@@ -40,7 +41,32 @@ async function main() {
JSON.stringify([...existingExamples, ...oldExamples]) JSON.stringify([...existingExamples, ...oldExamples])
); );
const { stdout: sha } = await execa('git', ['rev-parse', '--short', 'HEAD'], {
cwd: repoRoot,
});
const tarballsDir = join(pubDir, 'tarballs');
const packagesDir = join(repoRoot, 'packages');
const packages = await fs.readdir(packagesDir);
for (const pkg of packages) {
const fullDir = join(packagesDir, pkg);
const packageJsonRaw = await fs.readFile(
join(fullDir, 'package.json'),
'utf-8'
);
const packageJson = JSON.parse(packageJsonRaw);
const tarballName = `${packageJson.name
.replace('@', '')
.replace('/', '-')}-v${packageJson.version}-${sha.trim()}.tgz`;
const destTarballPath = join(tarballsDir, `${packageJson.name}.tgz`);
await fs.mkdir(dirname(destTarballPath), { recursive: true });
await fs.copyFile(join(fullDir, tarballName), destTarballPath);
}
console.log('Completed building static frontend.'); console.log('Completed building static frontend.');
} }
main().catch(console.error); main().catch(err => {
console.log('error running build:', err);
process.exit(1);
});

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "ES2020",
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,

View File

@@ -0,0 +1,2 @@
# `REACT_APP` prefix is required to expose to client-side
REACT_APP_VERCEL_ANALYTICS_ID=$VERCEL_ANALYTICS_ID

View File

@@ -1,13 +1,13 @@
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.16.1", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.2", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^14.2.0",
"react": "^17.0.2", "react": "^18.1.0",
"react-dom": "^17.0.2", "react-dom": "^18.1.0",
"react-scripts": "5.0.0", "react-scripts": "5.0.1",
"web-vitals": "^2.1.3" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { sendToVercelAnalytics } from './vitals';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
@@ -11,7 +12,4 @@ ReactDOM.render(
document.getElementById('root') document.getElementById('root')
); );
// If you want to start measuring performance in your app, pass a function reportWebVitals(sendToVercelAnalytics);
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1,40 @@
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in navigator['connection']
? navigator['connection']['effectiveType']
: '';
}
export function sendToVercelAnalytics(metric) {
const analyticsId = process.env.REACT_APP_VERCEL_ANALYTICS_ID;
if (!analyticsId) {
return;
}
const body = {
dsn: analyticsId,
id: metric.id,
page: window.location.pathname,
href: window.location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed(),
};
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
});
}

View File

@@ -1484,10 +1484,10 @@
"@svgr/plugin-svgo" "^5.5.0" "@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0" loader-utils "^2.0.0"
"@testing-library/dom@^8.0.0": "@testing-library/dom@^8.5.0":
version "8.11.2" version "8.13.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.2.tgz#fc110c665a066c2287be765e4a35ba8dad737015" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5"
integrity sha512-idsS/cqbYudXcVWngc1PuWNmXs416oBy2g/7Q8QAUREt5Z3MUkAL2XJD7xazLJ6esDfqRDi/ZBxk+OPPXitHRw== integrity sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==
dependencies: dependencies:
"@babel/code-frame" "^7.10.4" "@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
@@ -1498,10 +1498,10 @@
lz-string "^1.4.4" lz-string "^1.4.4"
pretty-format "^27.0.2" pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.16.1": "@testing-library/jest-dom@^5.16.4":
version "5.16.1" version "5.16.4"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd"
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw== integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==
dependencies: dependencies:
"@babel/runtime" "^7.9.2" "@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1" "@types/testing-library__jest-dom" "^5.9.1"
@@ -1513,20 +1513,19 @@
lodash "^4.17.15" lodash "^4.17.15"
redent "^3.0.0" redent "^3.0.0"
"@testing-library/react@^12.1.2": "@testing-library/react@^13.3.0":
version "12.1.2" version "13.3.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.3.0.tgz#bf298bfbc5589326bbcc8052b211f3bb097a97c5"
integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g== integrity sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0" "@testing-library/dom" "^8.5.0"
"@types/react-dom" "^18.0.0"
"@testing-library/user-event@^13.5.0": "@testing-library/user-event@^14.2.0":
version "13.5.0" version "14.2.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.2.0.tgz#8293560f8f80a00383d6c755ec3e0b918acb1683"
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== integrity sha512-+hIlG4nJS6ivZrKnOP7OGsDu9Fxmryj9vCl8x0ZINtTJcCHs2zLsYif5GzuRiBF2ck5GZG2aQr7Msg+EHlnYVQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
@@ -1735,6 +1734,11 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf"
integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w== integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/q@^1.5.1": "@types/q@^1.5.1":
version "1.5.5" version "1.5.5"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
@@ -1750,6 +1754,22 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@^18.0.0":
version "18.0.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.5.tgz#330b2d472c22f796e5531446939eacef8378444a"
integrity sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.9"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/resolve@1.17.1": "@types/resolve@1.17.1":
version "1.17.1" version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -1762,6 +1782,11 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/serve-index@^1.9.1": "@types/serve-index@^1.9.1":
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
@@ -3175,6 +3200,11 @@ cssstyle@^2.3.0:
dependencies: dependencies:
cssom "~0.3.6" cssom "~0.3.6"
csstype@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
damerau-levenshtein@^1.0.7: damerau-levenshtein@^1.0.7:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -3622,10 +3652,10 @@ escodegen@^2.0.0:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-config-react-app@^7.0.0: eslint-config-react-app@^7.0.1:
version "7.0.0" version "7.0.1"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4"
integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g== integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==
dependencies: dependencies:
"@babel/core" "^7.16.0" "@babel/core" "^7.16.0"
"@babel/eslint-parser" "^7.16.3" "@babel/eslint-parser" "^7.16.3"
@@ -6841,10 +6871,10 @@ react-app-polyfill@^3.0.0:
regenerator-runtime "^0.13.9" regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2" whatwg-fetch "^3.6.2"
react-dev-utils@^12.0.0: react-dev-utils@^12.0.1:
version "12.0.0" version "12.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"
integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ== integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==
dependencies: dependencies:
"@babel/code-frame" "^7.16.0" "@babel/code-frame" "^7.16.0"
address "^1.1.2" address "^1.1.2"
@@ -6865,25 +6895,24 @@ react-dev-utils@^12.0.0:
open "^8.4.0" open "^8.4.0"
pkg-up "^3.1.0" pkg-up "^3.1.0"
prompts "^2.4.2" prompts "^2.4.2"
react-error-overlay "^6.0.10" react-error-overlay "^6.0.11"
recursive-readdir "^2.2.2" recursive-readdir "^2.2.2"
shell-quote "^1.7.3" shell-quote "^1.7.3"
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
text-table "^0.2.0" text-table "^0.2.0"
react-dom@^17.0.2: react-dom@^18.1.0:
version "17.0.2" version "18.1.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" scheduler "^0.22.0"
scheduler "^0.20.2"
react-error-overlay@^6.0.10: react-error-overlay@^6.0.11:
version "6.0.10" version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-is@^16.13.1: react-is@^16.13.1:
version "16.13.1" version "16.13.1"
@@ -6900,10 +6929,10 @@ react-refresh@^0.11.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-scripts@5.0.0: react-scripts@5.0.1:
version "5.0.0" version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg== integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==
dependencies: dependencies:
"@babel/core" "^7.16.0" "@babel/core" "^7.16.0"
"@pmmmwh/react-refresh-webpack-plugin" "^0.5.3" "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3"
@@ -6921,7 +6950,7 @@ react-scripts@5.0.0:
dotenv "^10.0.0" dotenv "^10.0.0"
dotenv-expand "^5.1.0" dotenv-expand "^5.1.0"
eslint "^8.3.0" eslint "^8.3.0"
eslint-config-react-app "^7.0.0" eslint-config-react-app "^7.0.1"
eslint-webpack-plugin "^3.1.1" eslint-webpack-plugin "^3.1.1"
file-loader "^6.2.0" file-loader "^6.2.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
@@ -6938,7 +6967,7 @@ react-scripts@5.0.0:
postcss-preset-env "^7.0.1" postcss-preset-env "^7.0.1"
prompts "^2.4.2" prompts "^2.4.2"
react-app-polyfill "^3.0.0" react-app-polyfill "^3.0.0"
react-dev-utils "^12.0.0" react-dev-utils "^12.0.1"
react-refresh "^0.11.0" react-refresh "^0.11.0"
resolve "^1.20.0" resolve "^1.20.0"
resolve-url-loader "^4.0.0" resolve-url-loader "^4.0.0"
@@ -6955,13 +6984,12 @@ react-scripts@5.0.0:
optionalDependencies: optionalDependencies:
fsevents "^2.3.2" fsevents "^2.3.2"
react@^17.0.2: react@^18.1.0:
version "17.0.2" version "18.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1"
readable-stream@^2.0.1: readable-stream@^2.0.1:
version "2.3.7" version "2.3.7"
@@ -7235,13 +7263,12 @@ saxes@^5.0.1:
dependencies: dependencies:
xmlchars "^2.2.0" xmlchars "^2.2.0"
scheduler@^0.20.2: scheduler@^0.22.0:
version "0.20.2" version "0.22.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@2.7.0: schema-utils@2.7.0:
version "2.7.0" version "2.7.0"
@@ -8156,10 +8183,10 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies: dependencies:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
web-vitals@^2.1.3: web-vitals@^2.1.4:
version "2.1.3" version "2.1.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.3.tgz#6dca59f41dbc3fcccdb889da06191b437b18f534" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
integrity sha512-+ijpniAzcnQicXaXIN0/eHQAiV/jMt1oHGHTmz7VdAJPPkzzDhmoYPSpLgJTuFtUh+jCjxCoeTZPg7Ic+g8o7w== integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
webidl-conversions@^4.0.2: webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"

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 node_modules
.cache .cache
.env
.vercel .vercel
.output
public/build /build/
api/_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 { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
hydrate(<RemixBrowser />, document); 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 { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default function handleRequest( export default function handleRequest(
request: Request, request: Request,
@@ -16,6 +16,6 @@ export default function handleRequest(
return new Response("<!DOCTYPE html>" + markup, { return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode, status: responseStatusCode,
headers: responseHeaders headers: responseHeaders,
}); });
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { Outlet } from "remix"; import type { MetaFunction, LinksFunction } from "@remix-run/node";
import type { MetaFunction, LinksFunction } from "remix"; import { Outlet } from "@remix-run/react";
import stylesUrl from "~/styles/demos/about.css"; 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() { export default function AboutIndex() {
return ( return (

View File

@@ -1,4 +1,4 @@
import { Link } from "remix"; import { Link } from "@remix-run/react";
export default function AboutIndex() { export default function AboutIndex() {
return ( 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 { useEffect, useRef } from "react";
import type { ActionFunction } from "remix";
import { Form, json, useActionData, redirect } from "remix";
export function meta() { export function meta() {
return { title: "Actions Demo" }; 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() { export let meta: MetaFunction = () => ({ title: "Boundaries Demo" });
return { title: "Boundaries Demo" };
}
export default function Boundaries() { export default function Boundaries() {
return ( return (

View File

@@ -1,5 +1,6 @@
import { useCatch, Link, json, useLoaderData } from "remix"; import type { LoaderFunction, MetaFunction } from "@remix-run/node";
import type { LoaderFunction, MetaFunction } from "remix"; 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 // The `$` in route filenames becomes a pattern that's parsed from the URL and
// passed to your loaders so you can look up data. // 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-run/node";
import type { LoaderFunction } from "remix"; import { json } from "@remix-run/node";
import { Link, Outlet, useCatch, useLoaderData } from "@remix-run/react";
export default function Boundaries() { export default function Boundaries() {
return ( return (

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,9 +1,15 @@
/** /**
* @type {import('@remix-run/dev/config').AppConfig} * @type {import('@remix-run/dev').AppConfig}
*/ */
module.exports = { module.exports = {
appDirectory: "app", serverBuildTarget: "vercel",
browserBuildDirectory: "public/build", // When running locally in development mode, we use the built in remix
publicPath: "/build/", // server. This does not understand the vercel lambda module format,
serverBuildDirectory: "api/_build" // 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

@@ -1,7 +0,0 @@
{
"build": {
"env": {
"ENABLE_FILE_SYSTEM_API": "1"
}
}
}

View File

@@ -12,8 +12,8 @@
"format": "prettier --write --plugin-search-dir=. ." "format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "1.0.0-next.50",
"@sveltejs/kit": "next", "@sveltejs/kit": "1.0.0-next.347",
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0", "prettier-plugin-svelte": "^2.5.0",
@@ -24,6 +24,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.0", "@fontsource/fira-mono": "^4.5.0",
"cookie": "^0.4.1" "cookie": "^0.4.1",
"web-vitals": "^2.1.4"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals';
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in navigator['connection']
? // @ts-ignore
navigator['connection']['effectiveType']
: '';
}
/**
* @param {import("web-vitals").Metric} metric
* @param {{ params: { [s: string]: any; } | ArrayLike<any>; path: string; analyticsId: string; debug: boolean; }} options
*/
function sendToAnalytics(metric, options) {
const page = Object.entries(options.params).reduce(
(acc, [key, value]) => acc.replace(value, `[${key}]`),
options.path
);
const body = {
dsn: options.analyticsId,
id: metric.id,
page,
href: location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed()
};
if (options.debug) {
console.log('[Analytics]', metric.name, JSON.stringify(body, null, 2));
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded'
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true
});
}
/**
* @param {any} options
*/
export function webVitals(options) {
try {
getFID((metric) => sendToAnalytics(metric, options));
getTTFB((metric) => sendToAnalytics(metric, options));
getLCP((metric) => sendToAnalytics(metric, options));
getCLS((metric) => sendToAnalytics(metric, options));
getFCP((metric) => sendToAnalytics(metric, options));
} catch (err) {
console.error('[Analytics]', err);
}
}

View File

@@ -1,6 +1,19 @@
<script> <script>
import Header from '$lib/header/Header.svelte'; import Header from '$lib/header/Header.svelte';
import { webVitals } from '$lib/vitals';
import { browser } from '$app/env';
import { page } from '$app/stores';
import '../app.css'; import '../app.css';
let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID;
$: if (browser && analyticsId) {
webVitals({
path: $page.url.pathname,
params: $page.params,
analyticsId
})
}
</script> </script>
<Header /> <Header />

View File

@@ -8,6 +8,11 @@ const config = {
// Override http methods in the Todo forms // Override http methods in the Todo forms
methodOverride: { methodOverride: {
allowed: ['PATCH', 'DELETE'] allowed: ['PATCH', 'DELETE']
},
vite: {
define: {
'import.meta.env.VERCEL_ANALYTICS_ID': JSON.stringify(process.env.VERCEL_ANALYTICS_ID)
}
} }
} }
}; };

1479
examples/sveltekit/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -30,8 +30,8 @@
"npm-package-arg": "6.1.0", "npm-package-arg": "6.1.0",
"prettier": "2.6.2", "prettier": "2.6.2",
"ts-eager": "2.0.2", "ts-eager": "2.0.2",
"ts-jest": "28.0.0-next.1", "ts-jest": "28.0.5",
"turbo": "1.2.14" "turbo": "1.3.1"
}, },
"scripts": { "scripts": {
"lerna": "lerna", "lerna": "lerna",
@@ -41,7 +41,7 @@
"publish-from-github": "./utils/publish.sh", "publish-from-github": "./utils/publish.sh",
"changelog": "node utils/changelog.js", "changelog": "node utils/changelog.js",
"build": "turbo run build", "build": "turbo run build",
"vercel-build": "yarn build && cd api && node -r ts-eager/register ./_lib/script/build.ts", "vercel-build": "yarn build && yarn run pack && cd api && node -r ts-eager/register ./_lib/script/build.ts",
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"test": "jest --rootDir=\"test\" --testPathPattern=\"\\.test.js\"", "test": "jest --rootDir=\"test\" --testPathPattern=\"\\.test.js\"",
"test-unit": "yarn test && turbo run test-unit", "test-unit": "yarn test && turbo run test-unit",
@@ -49,7 +49,8 @@
"test-integration-once": "turbo run test-integration-once", "test-integration-once": "turbo run test-integration-once",
"test-integration-dev": "turbo run test-integration-dev", "test-integration-dev": "turbo run test-integration-dev",
"lint": "eslint . --ext .ts,.js", "lint": "eslint . --ext .ts,.js",
"prepare": "husky install" "prepare": "husky install",
"pack": "cd utils && node -r ts-eager/register ./pack.ts"
}, },
"lint-staged": { "lint-staged": {
"./{*,{api,packages,test,utils}/**/*}.{js,ts}": [ "./{*,{api,packages,test,utils}/**/*}.{js,ts}": [

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/build-utils", "name": "@vercel/build-utils",
"version": "3.1.1", "version": "5.0.0",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",
@@ -23,7 +23,7 @@
"@types/cross-spawn": "6.0.0", "@types/cross-spawn": "6.0.0",
"@types/end-of-stream": "^1.4.0", "@types/end-of-stream": "^1.4.0",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/glob": "^7.1.1", "@types/glob": "7.2.0",
"@types/jest": "27.4.1", "@types/jest": "27.4.1",
"@types/js-yaml": "3.12.1", "@types/js-yaml": "3.12.1",
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
@@ -31,7 +31,6 @@
"@types/node-fetch": "^2.1.6", "@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@types/yazl": "2.4.2", "@types/yazl": "2.4.2",
"@vercel/frameworks": "1.0.0",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1", "aggregate-error": "3.0.1",
"async-retry": "1.2.3", "async-retry": "1.2.3",
@@ -40,7 +39,7 @@
"cross-spawn": "6.0.5", "cross-spawn": "6.0.5",
"end-of-stream": "1.4.1", "end-of-stream": "1.4.1",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"glob": "7.1.3", "glob": "8.0.3",
"into-stream": "5.0.0", "into-stream": "5.0.0",
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"minimatch": "3.0.4", "minimatch": "3.0.4",

View File

@@ -3,6 +3,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import Sema from 'async-sema'; import Sema from 'async-sema';
import spawn from 'cross-spawn'; import spawn from 'cross-spawn';
import { coerce, intersects, validRange } from 'semver';
import { SpawnOptions } from 'child_process'; import { SpawnOptions } from 'child_process';
import { deprecate } from 'util'; import { deprecate } from 'util';
import debug from '../debug'; import debug from '../debug';
@@ -60,6 +61,13 @@ export interface SpawnOptionsExtended extends SpawnOptions {
* Pretty formatted command that is being spawned for logging purposes. * Pretty formatted command that is being spawned for logging purposes.
*/ */
prettyCommand?: string; 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( export function spawnAsync(
@@ -78,7 +86,7 @@ export function spawnAsync(
child.on('error', reject); child.on('error', reject);
child.on('close', (code, signal) => { child.on('close', (code, signal) => {
if (code === 0) { if (code === 0 || opts.ignoreNon0Exit) {
return resolve(); return resolve();
} }
@@ -122,24 +130,24 @@ export function execAsync(
child.on('error', reject); child.on('error', reject);
child.on('close', (code, signal) => { child.on('close', (code, signal) => {
if (code !== 0) { if (code === 0 || opts.ignoreNon0Exit) {
const cmd = opts.prettyCommand return resolve({
? `Command "${opts.prettyCommand}"` code,
: 'Command'; stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
return reject( });
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
);
} }
return resolve({ const cmd = opts.prettyCommand
code, ? `Command "${opts.prettyCommand}"`
stdout: Buffer.concat(stdoutList).toString(), : 'Command';
stderr: Buffer.concat(stderrList).toString(),
}); return reject(
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
);
}); });
} }
); );
@@ -165,9 +173,30 @@ export async function execCommand(command: string, options: SpawnOptions = {}) {
return true; return true;
} }
export async function getNodeBinPath({ cwd }: { cwd: string }) { export async function getNodeBinPath({
const { stdout } = await execAsync('npm', ['bin'], { cwd }); cwd,
return stdout.trim(); }: {
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) { async function chmodPlusX(fsPath: string) {
@@ -204,10 +233,23 @@ export function getSpawnOptions(
}; };
if (!meta.isDev) { if (!meta.isDev) {
// Ensure that the selected Node version is at the beginning of the `$PATH` let found = false;
opts.env.PATH = `/node${nodeVersion.major}/bin${path.delimiter}${ const oldPath = opts.env.PATH || process.env.PATH || '';
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; return opts;
@@ -219,9 +261,9 @@ export async function getNodeVersion(
config: Config = {}, config: Config = {},
meta: Meta = {} meta: Meta = {}
): Promise<NodeVersion> { ): Promise<NodeVersion> {
const latest = getLatestNodeVersion();
if (meta && meta.isDev) { if (meta && meta.isDev) {
// Use the system-installed version of `node` in PATH for `vercel dev` // Use the system-installed version of `node` in PATH for `vercel dev`
const latest = getLatestNodeVersion();
return { ...latest, runtime: 'nodejs' }; return { ...latest, runtime: 'nodejs' };
} }
const { packageJson } = await scanParentDirs(destPath, true); const { packageJson } = await scanParentDirs(destPath, true);
@@ -229,10 +271,27 @@ export async function getNodeVersion(
let isAuto = true; let isAuto = true;
if (packageJson && packageJson.engines && packageJson.engines.node) { if (packageJson && packageJson.engines && packageJson.engines.node) {
const { node } = packageJson.engines; const { node } = packageJson.engines;
if (nodeVersion && nodeVersion !== node && !meta.isDev) { if (
nodeVersion &&
validRange(node) &&
!intersects(nodeVersion, node) &&
!meta.isDev
) {
console.warn( console.warn(
`Warning: Due to "engines": { "node": "${node}" } in your \`package.json\` file, the Node.js Version defined in your Project Settings ("${nodeVersion}") will not apply. Learn More: http://vercel.link/node-version` `Warning: Due to "engines": { "node": "${node}" } in your \`package.json\` file, the Node.js Version defined in your Project Settings ("${nodeVersion}") will not apply. Learn More: http://vercel.link/node-version`
); );
} else if (coerce(node)?.raw === node && !meta.isDev) {
console.warn(
`Warning: Detected "engines": { "node": "${node}" } in your \`package.json\` with major.minor.patch, but only major Node.js Version can be selected. Learn More: http://vercel.link/node-version`
);
} else if (
validRange(node) &&
intersects(`${latest.major + 1}.x`, node) &&
!meta.isDev
) {
console.warn(
`Warning: Detected "engines": { "node": "${node}" } in your \`package.json\` that will automatically upgrade when a new major Node.js Version is released. Learn More: http://vercel.link/node-version`
);
} }
nodeVersion = node; nodeVersion = node;
isAuto = false; isAuto = false;
@@ -428,20 +487,31 @@ export function getEnvForPackageManager({
env: { [x: string]: string | undefined }; env: { [x: string]: string | undefined };
}) { }) {
const newEnv: { [x: string]: string | undefined } = { ...env }; 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 (cliType === 'npm') {
if ( if (
typeof lockfileVersion === 'number' && typeof lockfileVersion === 'number' &&
lockfileVersion >= 2 && 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` // Ensure that npm 7 is at the beginning of the `$PATH`
newEnv.PATH = `/node16/bin-npm7${path.delimiter}${env.PATH}`; newEnv.PATH = `${npm7}${path.delimiter}${oldPath}`;
console.log('Detected `package-lock.json` generated by npm 7...'); console.log('Detected `package-lock.json` generated by npm 7+...');
} }
} else if (cliType === 'pnpm') { } 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` // 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...'); console.log('Detected `pnpm-lock.yaml` generated by pnpm 7...');
} }
} else { } else {

View File

@@ -80,15 +80,6 @@ export {
}; };
export { EdgeFunction } from './edge-function'; 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 { DetectorFilesystem } from './detectors/filesystem';
export { readConfigFile } from './fs/read-config-file'; export { readConfigFile } from './fs/read-config-file';
export { normalizePath } from './fs/normalize-path'; export { normalizePath } from './fs/normalize-path';
@@ -96,26 +87,3 @@ export * from './should-serve';
export * from './schemas'; export * from './schemas';
export * from './types'; export * from './types';
export * from './errors'; 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 } from './workspaces/get-workspaces';
export { monorepoManagers } from './monorepos/monorepo-managers';

View File

@@ -82,7 +82,7 @@ export interface BuildOptions {
* is the Git Repository Root. This is only relevant for Monorepos. * is the Git Repository Root. This is only relevant for Monorepos.
* See https://vercel.com/blog/monorepos * See https://vercel.com/blog/monorepos
*/ */
repoRootPath?: string; repoRootPath: string;
/** /**
* An arbitrary object passed by the user in the build definition defined * An arbitrary object passed by the user in the build definition defined
@@ -123,7 +123,7 @@ export interface PrepareCacheOptions {
* is the Git Repository Root. This is only relevant for Monorepos. * is the Git Repository Root. This is only relevant for Monorepos.
* See https://vercel.com/blog/monorepos * See https://vercel.com/blog/monorepos
*/ */
repoRootPath?: string; repoRootPath: string;
/** /**
* An arbitrary object passed by the user in the build definition defined * An arbitrary object passed by the user in the build definition defined
@@ -295,6 +295,7 @@ export interface PackageJson {
readonly preferGlobal?: boolean; readonly preferGlobal?: boolean;
readonly private?: boolean; readonly private?: boolean;
readonly publishConfig?: PackageJson.PublishConfig; readonly publishConfig?: PackageJson.PublishConfig;
readonly packageManager?: string;
} }
export interface NodeVersion { export interface NodeVersion {
@@ -427,7 +428,9 @@ export interface BuildResultV2Typical {
export type BuildResultV2 = BuildResultV2Typical | BuildResultBuildOutput; export type BuildResultV2 = BuildResultV2Typical | BuildResultBuildOutput;
export interface BuildResultV3 { export interface BuildResultV3 {
output: Lambda; // TODO: use proper `Route` type from `routing-utils` (perhaps move types to a common package)
routes?: any[];
output: Lambda | EdgeFunction;
} }
export type BuildV2 = (options: BuildOptions) => Promise<BuildResultV2>; export type BuildV2 = (options: BuildOptions) => Promise<BuildResultV2>;

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

@@ -1,19 +1,260 @@
lockfileVersion: 5.3 lockfileVersion: 5.4
specifiers: importers:
once: ^1.4.0
dependencies: .:
once: 1.4.0 specifiers:
once: ^1.4.0
dependencies:
once: 1.4.0
a:
specifiers:
debug: ^4.3.2
dependencies:
debug: 4.3.4
b:
specifiers:
cowsay: ^1.5.0
dependencies:
cowsay: 1.5.0
packages: packages:
/ansi-regex/3.0.1:
resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
engines: {node: '>=4'}
dev: false
/ansi-regex/5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
dev: false
/ansi-styles/4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
dev: false
/camelcase/5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
dev: false
/cliui/6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
dev: false
/color-convert/2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
dev: false
/color-name/1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: false
/cowsay/1.5.0:
resolution: {integrity: sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==}
engines: {node: '>= 4'}
hasBin: true
dependencies:
get-stdin: 8.0.0
string-width: 2.1.1
strip-final-newline: 2.0.0
yargs: 15.4.1
dev: false
/debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: false
/decamelize/1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
dev: false
/emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false
/find-up/4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
dev: false
/get-caller-file/2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
dev: false
/get-stdin/8.0.0:
resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==}
engines: {node: '>=10'}
dev: false
/is-fullwidth-code-point/2.0.0:
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
engines: {node: '>=4'}
dev: false
/is-fullwidth-code-point/3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
dev: false
/locate-path/5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
dependencies:
p-locate: 4.1.0
dev: false
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: false
/once/1.4.0: /once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
dev: false dev: false
/p-limit/2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
dependencies:
p-try: 2.2.0
dev: false
/p-locate/4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
dependencies:
p-limit: 2.3.0
dev: false
/p-try/2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
dev: false
/path-exists/4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
dev: false
/require-directory/2.1.1:
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=}
engines: {node: '>=0.10.0'}
dev: false
/require-main-filename/2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
dev: false
/set-blocking/2.0.0:
resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=}
dev: false
/string-width/2.1.1:
resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==}
engines: {node: '>=4'}
dependencies:
is-fullwidth-code-point: 2.0.0
strip-ansi: 4.0.0
dev: false
/string-width/4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
dev: false
/strip-ansi/4.0.0:
resolution: {integrity: sha1-qEeQIusaw2iocTibY1JixQXuNo8=}
engines: {node: '>=4'}
dependencies:
ansi-regex: 3.0.1
dev: false
/strip-ansi/6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
dev: false
/strip-final-newline/2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: false
/which-module/2.0.0:
resolution: {integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=}
dev: false
/wrap-ansi/6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
dev: false
/wrappy/1.0.2: /wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
dev: false dev: false
/y18n/4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
dev: false
/yargs-parser/18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
dev: false
/yargs/15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.0
y18n: 4.0.3
yargs-parser: 18.1.3
dev: false

View File

@@ -5,7 +5,6 @@ import {
testDeployment, testDeployment,
// @ts-ignore // @ts-ignore
} from '../../../test/lib/deployment/test-deployment'; } from '../../../test/lib/deployment/test-deployment';
import { glob, detectBuilders } from '../src';
jest.setTimeout(4 * 60 * 1000); jest.setTimeout(4 * 60 * 1000);
@@ -32,11 +31,6 @@ const skipFixtures: string[] = [
'08-zero-config-middleman', '08-zero-config-middleman',
'21-npm-workspaces', '21-npm-workspaces',
'23-pnpm-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 // 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,6 @@
{
"private": true,
"engines": {
"node": "16.14.0"
}
}

View File

@@ -0,0 +1,6 @@
{
"private": true,
"engines": {
"node": ">=16"
}
}

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`, 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', name: 'should not set path if node is 16 and npm 7+ is detected',
args: { args: {
@@ -101,6 +133,38 @@ describe('Test `getEnvForPackageManager()`', () => {
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`, 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', name: 'should not set path if pnpm 6 is detected',
args: { 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, Meta,
} from '../src'; } from '../src';
jest.setTimeout(7 * 1000);
async function expectBuilderError(promise: Promise<any>, pattern: string) { async function expectBuilderError(promise: Promise<any>, pattern: string) {
let result; let result;
try { try {
@@ -277,7 +279,45 @@ it('should prefer package.json engines over project setting from config and warn
]); ]);
}); });
it('should warn when package.json engines is exact version', async () => {
expect(
await getNodeVersion(
path.join(__dirname, 'pkg-engine-node-exact'),
undefined,
{},
{}
)
).toHaveProperty('range', '16.x');
expect(warningMessages).toStrictEqual([
'Warning: Detected "engines": { "node": "16.14.0" } in your `package.json` with major.minor.patch, but only major Node.js Version can be selected. Learn More: http://vercel.link/node-version',
]);
});
it('should warn when package.json engines is greater than', async () => {
expect(
await getNodeVersion(
path.join(__dirname, 'pkg-engine-node-greaterthan'),
undefined,
{},
{}
)
).toHaveProperty('range', '16.x');
expect(warningMessages).toStrictEqual([
'Warning: Detected "engines": { "node": ">=16" } in your `package.json` that will automatically upgrade when a new major Node.js Version is released. Learn More: http://vercel.link/node-version',
]);
});
it('should not warn when package.json engines matches project setting from config', async () => { it('should not warn when package.json engines matches project setting from config', async () => {
expect(
await getNodeVersion(
path.join(__dirname, 'pkg-engine-node'),
undefined,
{ nodeVersion: '14' },
{}
)
).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([]);
expect( expect(
await getNodeVersion( await getNodeVersion(
path.join(__dirname, 'pkg-engine-node'), path.join(__dirname, 'pkg-engine-node'),
@@ -287,6 +327,16 @@ it('should not warn when package.json engines matches project setting from confi
) )
).toHaveProperty('range', '14.x'); ).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([]); expect(warningMessages).toStrictEqual([]);
expect(
await getNodeVersion(
path.join(__dirname, 'pkg-engine-node'),
undefined,
{ nodeVersion: '<15' },
{}
)
).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([]);
}); });
it('should get latest node version', async () => { it('should get latest node version', async () => {

View File

@@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"declaration": true, "declaration": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["esnext"], "lib": ["ES2020"],
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"noEmitOnError": true, "noEmitOnError": true,
@@ -13,7 +13,7 @@
"outDir": "./dist", "outDir": "./dist",
"types": ["node", "jest"], "types": ["node", "jest"],
"strict": true, "strict": true,
"target": "es2019" "target": "ES2020"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel", "name": "vercel",
"version": "24.2.5", "version": "26.0.0",
"preferGlobal": true, "preferGlobal": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "The command-line interface for Vercel", "description": "The command-line interface for Vercel",
@@ -30,7 +30,6 @@
"scripts/preinstall.js" "scripts/preinstall.js"
], ],
"ava": { "ava": {
"compileEnhancements": false,
"extensions": [ "extensions": [
"ts" "ts"
], ],
@@ -40,19 +39,19 @@
] ]
}, },
"engines": { "engines": {
"node": ">= 12" "node": ">= 14"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "3.1.1", "@vercel/build-utils": "5.0.0",
"@vercel/go": "1.4.4", "@vercel/go": "2.0.4",
"@vercel/next": "2.9.0", "@vercel/next": "3.1.3",
"@vercel/node": "1.15.4", "@vercel/node": "2.4.0",
"@vercel/python": "2.3.4", "@vercel/python": "3.0.4",
"@vercel/redwood": "0.8.4", "@vercel/redwood": "1.0.5",
"@vercel/remix": "0.0.2", "@vercel/remix": "1.0.5",
"@vercel/ruby": "1.3.7", "@vercel/ruby": "1.3.12",
"@vercel/static-build": "0.26.0", "@vercel/static-build": "1.0.4",
"update-notifier": "4.1.0" "update-notifier": "5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@alex_neo/jest-expect-message": "1.0.5", "@alex_neo/jest-expect-message": "1.0.5",
@@ -72,6 +71,7 @@
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/glob": "7.1.1", "@types/glob": "7.1.1",
"@types/http-proxy": "1.16.2", "@types/http-proxy": "1.16.2",
"@types/ini": "1.3.31",
"@types/inquirer": "7.3.1", "@types/inquirer": "7.3.1",
"@types/jest": "27.4.1", "@types/jest": "27.4.1",
"@types/jest-expect-message": "1.0.3", "@types/jest-expect-message": "1.0.3",
@@ -95,8 +95,9 @@
"@types/which": "1.3.2", "@types/which": "1.3.2",
"@types/write-json-file": "2.2.1", "@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0", "@types/yauzl-promise": "2.1.0",
"@vercel/client": "11.0.4", "@vercel/client": "12.0.4",
"@vercel/frameworks": "1.0.0", "@vercel/frameworks": "1.0.2",
"@vercel/fs-detectors": "1.0.0",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2", "@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2", "@zeit/source-map-support": "0.6.2",
@@ -131,11 +132,13 @@
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"get-port": "5.1.1", "get-port": "5.1.1",
"git-last-commit": "1.0.1",
"glob": "7.1.2", "glob": "7.1.2",
"http-proxy": "1.18.1", "http-proxy": "1.18.1",
"ini": "3.0.0",
"inquirer": "7.0.4", "inquirer": "7.0.4",
"is-docker": "2.2.1", "is-docker": "2.2.1",
"is-port-reachable": "3.0.0", "is-port-reachable": "3.1.0",
"is-url": "1.2.2", "is-url": "1.2.2",
"jaro-winkler": "0.2.8", "jaro-winkler": "0.2.8",
"jsonlines": "0.1.1", "jsonlines": "0.1.1",

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import chalk from 'chalk'; import chalk from 'chalk';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { join, relative } from 'path'; import { join, normalize, relative, resolve } from 'path';
import { import {
detectBuilders, normalizePath,
Files, Files,
FileFsRef, FileFsRef,
PackageJson, PackageJson,
@@ -14,7 +14,9 @@ import {
BuildResultV2, BuildResultV2,
BuildResultV2Typical, BuildResultV2Typical,
BuildResultV3, BuildResultV3,
NowBuildError,
} from '@vercel/build-utils'; } from '@vercel/build-utils';
import { detectBuilders } from '@vercel/fs-detectors';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import { import {
appendRoutesToPhase, appendRoutesToPhase,
@@ -45,6 +47,7 @@ import {
writeBuildResult, writeBuildResult,
} from '../util/build/write-build-result'; } from '../util/build/write-build-result';
import { importBuilders, BuilderWithPkg } from '../util/build/import-builders'; import { importBuilders, BuilderWithPkg } from '../util/build/import-builders';
import { initCorepack, cleanupCorepack } from '../util/build/corepack';
type BuildResult = BuildResultV2 | BuildResultV3; type BuildResult = BuildResultV2 | BuildResultV3;
@@ -62,6 +65,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
--cwd [path] The current working directory --cwd [path] The current working directory
--output [path] Directory where built assets should be written to
--prod Build a production deployment --prod Build a production deployment
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-y, --yes Skip the confirmation prompt -y, --yes Skip the confirmation prompt
@@ -98,7 +102,9 @@ export default async function main(client: Client): Promise<number> {
// Parse CLI args // Parse CLI args
const argv = getArgs(client.argv.slice(2), { const argv = getArgs(client.argv.slice(2), {
'--cwd': String, '--cwd': String,
'--output': String,
'--prod': Boolean, '--prod': Boolean,
'--yes': Boolean,
}); });
if (argv['--help']) { if (argv['--help']) {
@@ -112,42 +118,54 @@ export default async function main(client: Client): Promise<number> {
} }
const cwd = process.cwd(); const cwd = process.cwd();
// Build `target` influences which environment variables will be used
const target = argv['--prod'] ? 'production' : 'preview';
const yes = Boolean(argv['--yes']);
// TODO: read project settings from the API, fall back to local `project.json` if that fails // TODO: read project settings from the API, fall back to local `project.json` if that fails
// Read project settings, and pull them from Vercel if necessary // Read project settings, and pull them from Vercel if necessary
let project = await readProjectSettings(join(cwd, VERCEL_DIR)); let project = await readProjectSettings(join(cwd, VERCEL_DIR));
const isTTY = process.stdin.isTTY; const isTTY = process.stdin.isTTY;
while (!project?.settings) { while (!project?.settings) {
if (!isTTY) { let confirmed = yes;
client.output.print( if (!confirmed) {
`No Project Settings found locally. Run ${cli.getCommandName( if (!isTTY) {
'pull --yes' client.output.print(
)} to retreive them.` `No Project Settings found locally. Run ${cli.getCommandName(
); 'pull --yes'
return 1; )} to retreive them.`
} );
return 1;
}
const confirmed = await confirm( confirmed = await confirm(
`No Project Settings found locally. Run ${cli.getCommandName( client,
'pull' `No Project Settings found locally. Run ${cli.getCommandName(
)} for retrieving them?`, 'pull'
true )} for retrieving them?`,
); true
);
}
if (!confirmed) { if (!confirmed) {
client.output.print(`Aborted. No Project Settings retrieved.\n`); client.output.print(`Aborted. No Project Settings retrieved.\n`);
return 0; return 0;
} }
client.argv = []; const { argv: originalArgv } = client;
client.argv = [
...originalArgv.slice(0, 2),
'pull',
`--environment`,
target,
];
const result = await pull(client); const result = await pull(client);
if (result !== 0) { if (result !== 0) {
return result; return result;
} }
client.argv = originalArgv;
project = await readProjectSettings(join(cwd, VERCEL_DIR)); project = await readProjectSettings(join(cwd, VERCEL_DIR));
} }
// Build `target` influences which environment variables will be used
const target = argv['--prod'] ? 'production' : 'preview';
// TODO: load env vars from the API, fall back to local files if that fails // TODO: load env vars from the API, fall back to local files if that fails
const envPath = await checkExists([ const envPath = await checkExists([
@@ -177,7 +195,7 @@ export default async function main(client: Client): Promise<number> {
// Get a list of source files // Get a list of source files
const files = (await getFiles(workPath, client)).map(f => const files = (await getFiles(workPath, client)).map(f =>
relative(workPath, f) normalizePath(relative(workPath, f))
); );
const routesResult = getTransformedRoutes({ nowConfig: vercelConfig || {} }); const routesResult = getTransformedRoutes({ nowConfig: vercelConfig || {} });
@@ -265,22 +283,26 @@ export default async function main(client: Client): Promise<number> {
} }
// Delete output directory from potential previous build // Delete output directory from potential previous build
await fs.remove(OUTPUT_DIR); const outputDir = argv['--output']
? resolve(argv['--output'])
: join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
const buildStamp = stamp(); const buildStamp = stamp();
// Create fresh new output directory // Create fresh new output directory
await fs.mkdirp(OUTPUT_DIR); await fs.mkdirp(outputDir);
const ops: Promise<Error | void>[] = []; const ops: Promise<Error | void>[] = [];
// Write the `detectedBuilders` result to output dir // Write the `detectedBuilders` result to output dir
ops.push( ops.push(
fs.writeJSON( fs.writeJSON(
join(OUTPUT_DIR, 'builds.json'), join(outputDir, 'builds.json'),
{ {
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.', '//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
target, target,
argv: process.argv,
builds: builds.map(build => { builds: builds.map(build => {
const builderWithPkg = buildersWithPkgs.get(build.use); const builderWithPkg = buildersWithPkgs.get(build.use);
if (!builderWithPkg) { if (!builderWithPkg) {
@@ -289,6 +311,7 @@ export default async function main(client: Client): Promise<number> {
const { builder, pkg: builderPkg } = builderWithPkg; const { builder, pkg: builderPkg } = builderWithPkg;
return { return {
require: builderPkg.name, require: builderPkg.name,
requirePath: builderWithPkg.path,
apiVersion: builder.version, apiVersion: builder.version,
...build, ...build,
}; };
@@ -312,6 +335,10 @@ export default async function main(client: Client): Promise<number> {
// TODO: parallelize builds // TODO: parallelize builds
const buildResults: Map<Builder, BuildResult> = new Map(); const buildResults: Map<Builder, BuildResult> = new Map();
const overrides: PathOverride[] = []; const overrides: PathOverride[] = [];
const repoRootPath = cwd;
const rootPackageJsonPath = repoRootPath || workPath;
const corepackShimDir = await initCorepack({ cwd, rootPackageJsonPath });
for (const build of builds) { for (const build of builds) {
if (typeof build.src !== 'string') continue; if (typeof build.src !== 'string') continue;
@@ -331,7 +358,6 @@ export default async function main(client: Client): Promise<number> {
framework: project.settings.framework, framework: project.settings.framework,
nodeVersion: project.settings.nodeVersion, nodeVersion: project.settings.nodeVersion,
}; };
const repoRootPath = cwd === workPath ? undefined : cwd;
const buildOptions: BuildOptions = { const buildOptions: BuildOptions = {
files: filesMap, files: filesMap,
entrypoint: build.src, entrypoint: build.src,
@@ -352,6 +378,7 @@ export default async function main(client: Client): Promise<number> {
// Start flushing the file outputs to the filesystem asynchronously // Start flushing the file outputs to the filesystem asynchronously
ops.push( ops.push(
writeBuildResult( writeBuildResult(
outputDir,
buildResult, buildResult,
build, build,
builder, builder,
@@ -366,6 +393,10 @@ export default async function main(client: Client): Promise<number> {
); );
} }
if (corepackShimDir) {
cleanupCorepack(corepackShimDir);
}
// Wait for filesystem operations to complete // Wait for filesystem operations to complete
// TODO render progress bar? // TODO render progress bar?
let hadError = false; let hadError = false;
@@ -379,7 +410,7 @@ export default async function main(client: Client): Promise<number> {
if (hadError) return 1; if (hadError) return 1;
// Merge existing `config.json` file into the one that will be produced // Merge existing `config.json` file into the one that will be produced
const configPath = join(OUTPUT_DIR, 'config.json'); const configPath = join(outputDir, 'config.json');
// TODO: properly type // TODO: properly type
const existingConfig = await readJSONFile<any>(configPath); const existingConfig = await readJSONFile<any>(configPath);
if (existingConfig instanceof CantParseJSONFile) { if (existingConfig instanceof CantParseJSONFile) {
@@ -437,13 +468,14 @@ export default async function main(client: Client): Promise<number> {
wildcard: mergedWildcard, wildcard: mergedWildcard,
overrides: mergedOverrides, overrides: mergedOverrides,
}; };
await fs.writeJSON(join(OUTPUT_DIR, 'config.json'), config, { spaces: 2 }); await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
const relOutputDir = relative(cwd, outputDir);
output.print( output.print(
`${prependEmoji( `${prependEmoji(
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray( `Build Completed in ${chalk.bold(
buildStamp() relOutputDir.startsWith('..') ? outputDir : relOutputDir
)}`, )} ${chalk.gray(buildStamp())}`,
emoji('success') emoji('success')
)}\n` )}\n`
); );
@@ -452,17 +484,33 @@ export default async function main(client: Client): Promise<number> {
} }
function expandBuild(files: string[], build: Builder): Builder[] { function expandBuild(files: string[], build: Builder): Builder[] {
if (!build.src) return []; if (!build.use) {
throw new NowBuildError({
code: `invalid_build_specification`,
message: 'Field `use` is missing in build specification',
link: 'https://vercel.com/docs/configuration#project/builds',
action: 'View Documentation',
});
}
let pattern = build.src; let src = normalize(build.src || '**');
if (pattern[0] === '/') { if (src === '.' || src === './') {
throw new NowBuildError({
code: `invalid_build_specification`,
message: 'A build `src` path resolves to an empty string',
link: 'https://vercel.com/docs/configuration#project/builds',
action: 'View Documentation',
});
}
if (src[0] === '/') {
// Remove a leading slash so that the globbing is relative // Remove a leading slash so that the globbing is relative
// to `cwd` instead of the root of the filesystem. // to `cwd` instead of the root of the filesystem.
pattern = pattern.substring(1); src = src.substring(1);
} }
const matches = files.filter( const matches = files.filter(
name => name === pattern || minimatch(name, pattern, { dot: true }) name => name === src || minimatch(name, src, { dot: true })
); );
return matches.map(m => { return matches.map(m => {

View File

@@ -43,7 +43,9 @@ import {
import { SchemaValidationFailed } from '../../util/errors'; import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available'; import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
import confirm from '../../util/input/confirm'; import confirm from '../../util/input/confirm';
import editProjectSettings from '../../util/input/edit-project-settings'; import editProjectSettings, {
PartialProjectSettings,
} from '../../util/input/edit-project-settings';
import { import {
getLinkedProject, getLinkedProject,
linkFolderToProject, linkFolderToProject,
@@ -63,6 +65,7 @@ import { help } from './args';
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks'; import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
import parseTarget from '../../util/deploy/parse-target'; import parseTarget from '../../util/deploy/parse-target';
import getPrebuiltJson from '../../util/deploy/get-prebuilt-json'; import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
import { createGitMeta } from '../../util/deploy/create-git-meta';
export default async (client: Client) => { export default async (client: Client) => {
const { output } = client; const { output } = client;
@@ -157,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 // check paths
const pathValidation = await validatePaths(output, paths); const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) { if (!pathValidation.valid) {
return pathValidation.exitCode; return pathValidation.exitCode;
@@ -240,6 +243,7 @@ export default async (client: Client) => {
const shouldStartSetup = const shouldStartSetup =
autoConfirm || autoConfirm ||
(await confirm( (await confirm(
client,
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`, `Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true true
)); ));
@@ -284,7 +288,7 @@ export default async (client: Client) => {
if (typeof projectOrNewProjectName === 'string') { if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName; newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm); rootDirectory = await inputRootDirectory(client, path, autoConfirm);
} else { } else {
project = projectOrNewProjectName; project = projectOrNewProjectName;
rootDirectory = project.rootDirectory; rootDirectory = project.rootDirectory;
@@ -415,6 +419,8 @@ export default async (client: Client) => {
parseMeta(argv['--meta']) parseMeta(argv['--meta'])
); );
const gitMetadata = await createGitMeta(path, output);
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments // Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
const deploymentEnv = Object.assign( const deploymentEnv = Object.assign(
{}, {},
@@ -453,6 +459,15 @@ export default async (client: Client) => {
let deployStamp = stamp(); let deployStamp = stamp();
let deployment = null; let deployment = null;
const localConfigurationOverrides: PartialProjectSettings = {
buildCommand: localConfig?.buildCommand,
devCommand: localConfig?.devCommand,
framework: localConfig?.framework,
commandForIgnoringBuildStep: localConfig?.ignoreCommand,
installCommand: localConfig?.installCommand,
outputDirectory: localConfig?.outputDirectory,
};
try { try {
const createArgs: any = { const createArgs: any = {
name: project ? project.name : newProjectName, name: project ? project.name : newProjectName,
@@ -468,6 +483,7 @@ export default async (client: Client) => {
nowConfig: localConfig, nowConfig: localConfig,
regions, regions,
meta, meta,
gitMetadata,
deployStamp, deployStamp,
target, target,
skipAutoDetectionConfirmation: autoConfirm, skipAutoDetectionConfirmation: autoConfirm,
@@ -475,7 +491,12 @@ export default async (client: Client) => {
if (!localConfig.builds || localConfig.builds.length === 0) { if (!localConfig.builds || localConfig.builds.length === 0) {
// Only add projectSettings for zero config deployments // Only add projectSettings for zero config deployments
createArgs.projectSettings = { sourceFilesOutsideRootDirectory }; createArgs.projectSettings =
status === 'not_linked'
? {
sourceFilesOutsideRootDirectory,
}
: { ...localConfigurationOverrides, sourceFilesOutsideRootDirectory };
} }
deployment = await createDeploy( deployment = await createDeploy(
@@ -501,9 +522,11 @@ export default async (client: Client) => {
} }
const settings = await editProjectSettings( const settings = await editProjectSettings(
output, client,
projectSettings, projectSettings,
framework framework,
false,
localConfigurationOverrides
); );
// deploy again, but send projectSettings this time // deploy again, but send projectSettings this time

View File

@@ -1,4 +1,5 @@
import { resolve, join } from 'path'; import { resolve, join } from 'path';
import fs from 'fs-extra';
import DevServer from '../../util/dev/server'; import DevServer from '../../util/dev/server';
import parseListen from '../../util/dev/parse-listen'; import parseListen from '../../util/dev/parse-listen';
@@ -12,6 +13,7 @@ import setupAndLink from '../../util/link/setup-and-link';
import getSystemEnvValues from '../../util/env/get-system-env-values'; import getSystemEnvValues from '../../util/env/get-system-env-values';
import { getCommandName } from '../../util/pkg-name'; import { getCommandName } from '../../util/pkg-name';
import param from '../../util/output/param'; import param from '../../util/output/param';
import { OUTPUT_DIR } from '../../util/build/write-build-result';
type Options = { type Options = {
'--listen': string; '--listen': string;
@@ -104,6 +106,17 @@ export default async function dev(
devCommand = process.env.VERCEL_DEV_COMMAND; devCommand = process.env.VERCEL_DEV_COMMAND;
} }
// If there is no Development Command, we must delete the
// v3 Build Output because it will incorrectly be detected by
// @vercel/static-build in BuildOutputV3.getBuildOutputDirectory()
if (!devCommand) {
const outputDir = join(cwd, OUTPUT_DIR);
if (await fs.pathExists(outputDir)) {
output.log(`Removing ${OUTPUT_DIR}`);
await fs.remove(outputDir);
}
}
const devServer = new DevServer(cwd, { const devServer = new DevServer(cwd, {
output, output,
devCommand, devCommand,

View File

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

View File

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

View File

@@ -150,17 +150,7 @@ export default async function inspect(
`This Domain is not configured properly. To configure it you should either:`, `This Domain is not configured properly. To configure it you should either:`,
null, null,
null, null,
null, null
{
boxen: {
margin: {
left: 2,
right: 0,
bottom: 0,
top: 0,
},
},
}
); );
output.print( output.print(
` ${chalk.grey('a)')} ` + ` ${chalk.grey('a)')} ` +

View File

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

View File

@@ -92,7 +92,10 @@ export default async function rm(
const skipConfirmation = opts['--yes'] || false; const skipConfirmation = opts['--yes'] || false;
if ( if (
!skipConfirmation && !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'); output.log('Aborted');
return 0; return 0;
@@ -230,7 +233,7 @@ async function removeDomain(
if ( if (
!skipConfirmation && !skipConfirmation &&
!(await promptBool(`Remove conflicts associated with domain?`)) !(await promptBool(`Remove conflicts associated with domain?`, client))
) { ) {
output.log('Aborted'); output.log('Aborted');
return 0; return 0;

View File

@@ -81,7 +81,8 @@ export default async function transferIn(
const shouldTransfer = await promptBool( const shouldTransfer = await promptBool(
transferPolicy === 'no-change' transferPolicy === 'no-change'
? `Transfer now for ${chalk.bold(`$${price}`)}?` ? `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) { if (!shouldTransfer) {
return 0; return 0;

View File

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

View File

@@ -42,6 +42,13 @@ const help = () => {
${chalk.dim('Examples:')} ${chalk.dim('Examples:')}
${chalk.gray(
''
)} Pull all Development Environment Variables down from the cloud
${chalk.cyan(`$ ${getPkgName()} env pull <file>`)}
${chalk.cyan(`$ ${getPkgName()} env pull .env.development.local`)}
${chalk.gray('')} Add a new variable to multiple Environments ${chalk.gray('')} Add a new variable to multiple Environments
${chalk.cyan(`$ ${getPkgName()} env add <name>`)} ${chalk.cyan(`$ ${getPkgName()} env add <name>`)}

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ export default new Map([
['certs', 'certs'], ['certs', 'certs'],
['deploy', 'deploy'], ['deploy', 'deploy'],
['dev', 'dev'], ['dev', 'dev'],
['develop', 'dev'],
['dns', 'dns'], ['dns', 'dns'],
['domain', 'domains'], ['domain', 'domains'],
['domains', 'domains'], ['domains', 'domains'],

View File

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

View File

@@ -25,7 +25,7 @@ import {
const help = () => { const help = () => {
return console.log(` return console.log(`
${chalk.bold(`${logo} ${getPkgName()} pull`)} [path] ${chalk.bold(`${logo} ${getPkgName()} pull`)} [project-path]
${chalk.dim('Options:')} ${chalk.dim('Options:')}
@@ -42,25 +42,29 @@ const help = () => {
${chalk.dim('Examples:')} ${chalk.dim('Examples:')}
${chalk.gray('')} Pull the latest Project Settings from the cloud ${chalk.gray(
''
)} Pull the latest Environment Variables and Project Settings from the cloud
and stores them in \`.vercel/.env.\${target}.local\` and \`.vercel/project.json\` respectively.
${chalk.cyan(`$ ${getPkgName()} pull`)} ${chalk.cyan(`$ ${getPkgName()} pull`)}
${chalk.cyan(`$ ${getPkgName()} pull ./path-to-project`)} ${chalk.cyan(`$ ${getPkgName()} pull ./path-to-project`)}
${chalk.cyan(`$ ${getPkgName()} pull --env .env.local`)}
${chalk.cyan(`$ ${getPkgName()} pull ./path-to-project --env .env.local`)}
${chalk.gray('')} Pull specific environment's Project Settings from the cloud ${chalk.gray('')} Pull for a specific environment
${chalk.cyan( ${chalk.cyan(
`$ ${getPkgName()} pull --environment=${getEnvTargetPlaceholder()}` `$ ${getPkgName()} pull --environment=${getEnvTargetPlaceholder()}`
)} )}
${chalk.gray(
'If you want to download environment variables to a specific file, use `vercel env pull` instead.'
)}
`); `);
}; };
function processArgs(client: Client) { function processArgs(client: Client) {
return getArgs(client.argv.slice(2), { return getArgs(client.argv.slice(2), {
'--yes': Boolean, '--yes': Boolean,
'--env': String, // deprecated
'--environment': String, '--environment': String,
'--debug': Boolean, '--debug': Boolean,
'-d': '--debug', '-d': '--debug',

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ import * as Sentry from '@sentry/node';
import hp from './util/humanize-path'; import hp from './util/humanize-path';
import commands from './commands'; import commands from './commands';
import pkg from './util/pkg'; import pkg from './util/pkg';
import createOutput from './util/output'; import { Output } from './util/output';
import cmd from './util/output/cmd'; import cmd from './util/output/cmd';
import info from './util/output/info'; import info from './util/output/info';
import error from './util/output/error'; import error from './util/output/error';
@@ -58,6 +58,7 @@ const isCanary = pkg.version.includes('canary');
const notifier = updateNotifier({ const notifier = updateNotifier({
pkg, pkg,
distTag: isCanary ? 'canary' : 'latest', distTag: isCanary ? 'canary' : 'latest',
updateCheckInterval: 1000 * 60 * 60 * 24 * 7, // 1 week
}); });
const VERCEL_DIR = getGlobalPathConfig(); const VERCEL_DIR = getGlobalPathConfig();
@@ -108,7 +109,7 @@ const main = async () => {
} }
const isDebugging = argv['--debug']; const isDebugging = argv['--debug'];
const output = createOutput({ debug: isDebugging }); const output = new Output(process.stderr, { debug: isDebugging });
debug = output.debug; debug = output.debug;
@@ -386,6 +387,9 @@ const main = async () => {
// Shared API `Client` instance for all sub-commands to utilize // Shared API `Client` instance for all sub-commands to utilize
client = new Client({ client = new Client({
apiUrl, apiUrl,
stdin: process.stdin,
stdout: process.stdout,
stderr: output.stream,
output, output,
config, config,
authConfig, authConfig,
@@ -795,7 +799,5 @@ process.on('uncaughtException', handleUnexpected);
main() main()
.then(exitCode => { .then(exitCode => {
process.exitCode = exitCode; process.exitCode = exitCode;
// @ts-ignore - "nowExit" is a non-standard event name
process.emit('nowExit');
}) })
.catch(handleUnexpected); .catch(handleUnexpected);

View File

@@ -1,3 +1,5 @@
import type { Readable, Writable } from 'stream';
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings; export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
export type Primitive = export type Primitive =
@@ -296,6 +298,15 @@ export interface Token {
teamId?: string; teamId?: string;
} }
export interface GitMetadata {
commitAuthorName?: string | undefined;
commitMessage?: string | undefined;
commitRef?: string | undefined;
commitSha?: string | undefined;
dirty?: boolean | undefined;
remoteUrl: string;
}
/** /**
* An object representing a Build on Vercel * An object representing a Build on Vercel
*/ */
@@ -433,3 +444,19 @@ export interface BuildOutput {
layers?: string[]; layers?: string[];
} | null; } | 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

@@ -0,0 +1,82 @@
import { delimiter, join } from 'path';
import { PackageJson, spawnAsync } from '@vercel/build-utils';
import fs from 'fs-extra';
import { CantParseJSONFile } from '../errors-ts';
import { VERCEL_DIR } from '../projects/link';
import readJSONFile from '../read-json-file';
export async function initCorepack({
cwd,
rootPackageJsonPath,
}: {
cwd: string;
rootPackageJsonPath: string;
}): Promise<string | null> {
if (process.env.ENABLE_EXPERIMENTAL_COREPACK !== '1') {
// Since corepack is experimental, we need to exit early
// unless the user explicitly enables it with the env var.
return null;
}
const pkg = await readJSONFile<PackageJson>(
join(rootPackageJsonPath, 'package.json')
);
if (pkg instanceof CantParseJSONFile) {
console.warn(
'Warning: Could not enable corepack because package.json is invalid JSON'
);
} else if (!pkg?.packageManager) {
console.warn(
'Warning: Could not enable corepack because package.json is missing "packageManager" property'
);
} else {
console.log(
`Detected ENABLE_EXPERIMENTAL_COREPACK=1 and "${pkg.packageManager}" in package.json`
);
const corepackRootDir = join(cwd, VERCEL_DIR, 'cache', 'corepack');
const corepackHomeDir = join(corepackRootDir, 'home');
const corepackShimDir = join(corepackRootDir, 'shim');
await fs.mkdirp(corepackHomeDir);
await fs.mkdirp(corepackShimDir);
process.env.COREPACK_HOME = corepackHomeDir;
process.env.PATH = `${corepackShimDir}${delimiter}${process.env.PATH}`;
process.env.DEBUG = process.env.DEBUG
? `corepack,${process.env.DEBUG}`
: 'corepack';
const pkgManagerName = pkg.packageManager.split('@')[0];
// We must explicitly call `corepack enable npm` since `corepack enable`
// doesn't work with npm. See https://github.com/nodejs/corepack/pull/24
// Also, `corepack enable` is too broad and will change the verison of
// yarn & pnpm even though those versions are not specified by the user.
// See https://github.com/nodejs/corepack#known-good-releases
// Finally, we use `--install-directory` so we can cache the result to
// reuse for subsequent builds. See `@vercel/vc-build` for `prepareCache`.
await spawnAsync(
'corepack',
['enable', pkgManagerName, '--install-directory', corepackShimDir],
{
prettyCommand: `corepack enable ${pkgManagerName}`,
}
);
return corepackShimDir;
}
return null;
}
export function cleanupCorepack(corepackShimDir: string) {
if (process.env.COREPACK_HOME) {
delete process.env.COREPACK_HOME;
}
if (process.env.PATH) {
process.env.PATH = process.env.PATH.replace(
`${corepackShimDir}${delimiter}`,
''
);
}
if (process.env.DEBUG) {
if (process.env.DEBUG === 'corepack') {
delete process.env.DEBUG;
} else {
process.env.DEBUG = process.env.DEBUG.replace('corepack,', '');
}
}
}

View File

@@ -106,9 +106,10 @@ export async function resolveBuilders(
// If `pkgPath` wasn't found in `.vercel/builders` then try as a CLI local // If `pkgPath` wasn't found in `.vercel/builders` then try as a CLI local
// dependency. `require.resolve()` will throw if the Builder is not a CLI // dependency. `require.resolve()` will throw if the Builder is not a CLI
// dep, in which case we'll install it into `.vercel/builders`. // dep, in which case we'll install it into `.vercel/builders`.
pkgPath = require.resolve(`${name}/package.json`, { // NOTE: `eval('require')` is necessary to avoid bad transpilation to `__webpack_require__`
pkgPath = eval('require').resolve(`${name}/package.json`, {
paths: [__dirname], paths: [__dirname],
}); }) as string;
builderPkg = await readJSON(pkgPath); builderPkg = await readJSON(pkgPath);
} }
@@ -148,7 +149,9 @@ export async function resolveBuilders(
// TODO: handle `parsed.type === 'tag'` ("latest" vs. anything else?) // TODO: handle `parsed.type === 'tag'` ("latest" vs. anything else?)
const path = join(dirname(pkgPath), builderPkg.main || 'index.js'); const path = join(dirname(pkgPath), builderPkg.main || 'index.js');
const builder = require(path);
// NOTE: `eval('require')` is necessary to avoid bad transpilation to `__webpack_require__`
const builder = eval('require')(path);
builders.set(spec, { builders.set(spec, {
builder, builder,

View File

@@ -23,6 +23,7 @@ import { VERCEL_DIR } from '../projects/link';
export const OUTPUT_DIR = join(VERCEL_DIR, 'output'); export const OUTPUT_DIR = join(VERCEL_DIR, 'output');
export async function writeBuildResult( export async function writeBuildResult(
outputDir: string,
buildResult: BuildResultV2 | BuildResultV3, buildResult: BuildResultV2 | BuildResultV3,
build: Builder, build: Builder,
builder: BuilderV2 | BuilderV3, builder: BuilderV2 | BuilderV3,
@@ -31,9 +32,13 @@ export async function writeBuildResult(
) { ) {
const { version } = builder; const { version } = builder;
if (version === 2) { if (version === 2) {
return writeBuildResultV2(buildResult as BuildResultV2, cleanUrls); return writeBuildResultV2(
outputDir,
buildResult as BuildResultV2,
cleanUrls
);
} else if (version === 3) { } else if (version === 3) {
return writeBuildResultV3(buildResult as BuildResultV3, build); return writeBuildResultV3(outputDir, buildResult as BuildResultV3, build);
} }
throw new Error( throw new Error(
`Unsupported Builder version \`${version}\` from "${builderPkg.name}"` `Unsupported Builder version \`${version}\` from "${builderPkg.name}"`
@@ -67,11 +72,12 @@ export interface PathOverride {
* the filesystem. * the filesystem.
*/ */
async function writeBuildResultV2( async function writeBuildResultV2(
outputDir: string,
buildResult: BuildResultV2, buildResult: BuildResultV2,
cleanUrls?: boolean cleanUrls?: boolean
) { ) {
if ('buildOutputPath' in buildResult) { if ('buildOutputPath' in buildResult) {
await mergeBuilderOutput(buildResult); await mergeBuilderOutput(outputDir, buildResult);
return; return;
} }
@@ -79,16 +85,16 @@ async function writeBuildResultV2(
const overrides: Record<string, PathOverride> = {}; const overrides: Record<string, PathOverride> = {};
for (const [path, output] of Object.entries(buildResult.output)) { for (const [path, output] of Object.entries(buildResult.output)) {
if (isLambda(output)) { if (isLambda(output)) {
await writeLambda(output, path, lambdas); await writeLambda(outputDir, output, path, lambdas);
} else if (isPrerender(output)) { } else if (isPrerender(output)) {
await writeLambda(output.lambda, path, lambdas); await writeLambda(outputDir, output.lambda, path, lambdas);
// Write the fallback file alongside the Lambda directory // Write the fallback file alongside the Lambda directory
let fallback = output.fallback; let fallback = output.fallback;
if (fallback) { if (fallback) {
const ext = getFileExtension(fallback); const ext = getFileExtension(fallback);
const fallbackName = `${path}.prerender-fallback${ext}`; const fallbackName = `${path}.prerender-fallback${ext}`;
const fallbackPath = join(OUTPUT_DIR, 'functions', fallbackName); const fallbackPath = join(outputDir, 'functions', fallbackName);
const stream = fallback.toStream(); const stream = fallback.toStream();
await pipe( await pipe(
stream, stream,
@@ -101,7 +107,7 @@ async function writeBuildResultV2(
} }
const prerenderConfigPath = join( const prerenderConfigPath = join(
OUTPUT_DIR, outputDir,
'functions', 'functions',
`${path}.prerender-config.json` `${path}.prerender-config.json`
); );
@@ -112,9 +118,9 @@ async function writeBuildResultV2(
}; };
await fs.writeJSON(prerenderConfigPath, prerenderConfig, { spaces: 2 }); await fs.writeJSON(prerenderConfigPath, prerenderConfig, { spaces: 2 });
} else if (isFile(output)) { } else if (isFile(output)) {
await writeStaticFile(output, path, overrides, cleanUrls); await writeStaticFile(outputDir, output, path, overrides, cleanUrls);
} else if (isEdgeFunction(output)) { } else if (isEdgeFunction(output)) {
await writeEdgeFunction(output, path); await writeEdgeFunction(outputDir, output, path);
} else { } else {
throw new Error( throw new Error(
`Unsupported output type: "${(output as any).type}" for ${path}` `Unsupported output type: "${(output as any).type}" for ${path}`
@@ -128,15 +134,24 @@ async function writeBuildResultV2(
* Writes the output from the `build()` return value of a v3 Builder to * Writes the output from the `build()` return value of a v3 Builder to
* the filesystem. * the filesystem.
*/ */
async function writeBuildResultV3(buildResult: BuildResultV3, build: Builder) { async function writeBuildResultV3(
outputDir: string,
buildResult: BuildResultV3,
build: Builder
) {
const { output } = buildResult; const { output } = buildResult;
const src = build.src;
if (typeof src !== 'string') {
throw new Error(`Expected "build.src" to be a string`);
}
const ext = extname(src);
const path = build.config?.zeroConfig
? src.substring(0, src.length - ext.length)
: src;
if (isLambda(output)) { if (isLambda(output)) {
const src = build.src!; await writeLambda(outputDir, output, path);
const ext = extname(src); } else if (isEdgeFunction(output)) {
const path = build.config?.zeroConfig await writeEdgeFunction(outputDir, output, path);
? src.substring(0, src.length - ext.length)
: src;
await writeLambda(output, path);
} else { } else {
throw new Error( throw new Error(
`Unsupported output type: "${(output as any).type}" for ${build.src}` `Unsupported output type: "${(output as any).type}" for ${build.src}`
@@ -154,6 +169,7 @@ async function writeBuildResultV3(buildResult: BuildResultV3, build: Builder) {
* @param overrides Record of override configuration when a File is renamed or has other metadata * @param overrides Record of override configuration when a File is renamed or has other metadata
*/ */
async function writeStaticFile( async function writeStaticFile(
outputDir: string,
file: File, file: File,
path: string, path: string,
overrides: Record<string, PathOverride>, overrides: Record<string, PathOverride>,
@@ -191,7 +207,7 @@ async function writeStaticFile(
overrides[fsPath] = override; overrides[fsPath] = override;
} }
const dest = join(OUTPUT_DIR, 'static', fsPath); const dest = join(outputDir, 'static', fsPath);
await fs.mkdirp(dirname(dest)); await fs.mkdirp(dirname(dest));
// TODO: handle (or skip) symlinks? // TODO: handle (or skip) symlinks?
@@ -205,8 +221,12 @@ async function writeStaticFile(
* @param edgeFunction The `EdgeFunction` instance * @param edgeFunction The `EdgeFunction` instance
* @param path The URL path where the `EdgeFunction` can be accessed from * @param path The URL path where the `EdgeFunction` can be accessed from
*/ */
async function writeEdgeFunction(edgeFunction: EdgeFunction, path: string) { async function writeEdgeFunction(
const dest = join(OUTPUT_DIR, 'functions', `${path}.func`); outputDir: string,
edgeFunction: EdgeFunction,
path: string
) {
const dest = join(outputDir, 'functions', `${path}.func`);
await fs.mkdirp(dest); await fs.mkdirp(dest);
const ops: Promise<any>[] = []; const ops: Promise<any>[] = [];
@@ -235,11 +255,12 @@ async function writeEdgeFunction(edgeFunction: EdgeFunction, path: string) {
* @param lambdas (optional) Map of `Lambda` instances that have previously been written * @param lambdas (optional) Map of `Lambda` instances that have previously been written
*/ */
async function writeLambda( async function writeLambda(
outputDir: string,
lambda: Lambda, lambda: Lambda,
path: string, path: string,
lambdas?: Map<Lambda, string> lambdas?: Map<Lambda, string>
) { ) {
const dest = join(OUTPUT_DIR, 'functions', `${path}.func`); const dest = join(outputDir, 'functions', `${path}.func`);
// If the `lambda` has already been written to the filesystem at a different // If the `lambda` has already been written to the filesystem at a different
// location then create a symlink to the previous location instead of copying // location then create a symlink to the previous location instead of copying
@@ -248,7 +269,7 @@ async function writeLambda(
if (existingLambdaPath) { if (existingLambdaPath) {
const destDir = dirname(dest); const destDir = dirname(dest);
const targetDest = join( const targetDest = join(
OUTPUT_DIR, outputDir,
'functions', 'functions',
`${existingLambdaPath}.func` `${existingLambdaPath}.func`
); );
@@ -312,14 +333,17 @@ async function writeLambda(
* `.vercel/output` directory that was specified by the Builder into the * `.vercel/output` directory that was specified by the Builder into the
* `vc build` output directory. * `vc build` output directory.
*/ */
async function mergeBuilderOutput(buildResult: BuildResultBuildOutput) { async function mergeBuilderOutput(
const absOutputDir = resolve(OUTPUT_DIR); outputDir: string,
buildResult: BuildResultBuildOutput
) {
const absOutputDir = resolve(outputDir);
if (absOutputDir === buildResult.buildOutputPath) { if (absOutputDir === buildResult.buildOutputPath) {
// `.vercel/output` dir is already in the correct location, // `.vercel/output` dir is already in the correct location,
// so no need to do anything // so no need to do anything
return; return;
} }
await fs.copy(buildResult.buildOutputPath, OUTPUT_DIR); await fs.copy(buildResult.buildOutputPath, outputDir);
} }
/** /**
@@ -361,8 +385,14 @@ export async function* findDirs(
} }
for (const path of paths) { for (const path of paths) {
const abs = join(dir, path); const abs = join(dir, path);
const s = await fs.stat(abs); let stat: fs.Stats;
if (s.isDirectory()) { try {
stat = await fs.lstat(abs);
} catch (err: any) {
if (err.code === 'ENOENT') continue;
throw err;
}
if (stat.isDirectory()) {
if (path === name) { if (path === name) {
yield relative(root, abs); yield relative(root, abs);
} else { } else {

View File

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

@@ -0,0 +1,79 @@
import fs from 'fs-extra';
import { join } from 'path';
import ini from 'ini';
import git from 'git-last-commit';
import { exec } from 'child_process';
import { GitMetadata } from '../../types';
import { Output } from '../output';
export function isDirty(directory: string): Promise<boolean> {
return new Promise((resolve, reject) => {
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
if (err) return reject(err);
if (stderr)
return reject(
new Error(
`Failed to determine if git repo has been modified: ${stderr.trim()}`
)
);
resolve(stdout.trim().length > 0);
});
});
}
function getLastCommit(directory: string): Promise<git.Commit> {
return new Promise((resolve, reject) => {
git.getLastCommit(
(err, commit) => {
if (err) return reject(err);
resolve(commit);
},
{ dst: directory }
);
});
}
export async function getRemoteUrl(
configPath: string,
output: Output
): Promise<string | null> {
let gitConfig;
try {
gitConfig = ini.parse(await fs.readFile(configPath, 'utf-8'));
} catch (error) {
output.debug(`Error while parsing repo data: ${error.message}`);
}
if (!gitConfig) {
return null;
}
const originUrl: string = gitConfig['remote "origin"']?.url;
if (originUrl) {
return originUrl;
}
return null;
}
export async function createGitMeta(
directory: string,
output: Output
): Promise<GitMetadata | undefined> {
const remoteUrl = await getRemoteUrl(join(directory, '.git/config'), output);
// If we can't get the repo URL, then don't return any metadata
if (!remoteUrl) {
return;
}
const [commit, dirty] = await Promise.all([
getLastCommit(directory),
isDirty(directory),
]);
return {
remoteUrl,
commitAuthorName: commit.author.name,
commitMessage: commit.subject,
commitRef: commit.branch,
commitSha: commit.hash,
dirty,
};
}

View File

@@ -13,8 +13,8 @@ import {
Lambda, Lambda,
FileBlob, FileBlob,
FileFsRef, FileFsRef,
isOfficialRuntime,
} from '@vercel/build-utils'; } from '@vercel/build-utils';
import { isOfficialRuntime } from '@vercel/fs-detectors';
import plural from 'pluralize'; import plural from 'pluralize';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
@@ -142,6 +142,7 @@ export async function executeBuild(
files, files,
entrypoint, entrypoint,
workPath, workPath,
repoRootPath: workPath,
config, config,
meta: { meta: {
isDev: true, isDev: true,
@@ -416,10 +417,6 @@ export async function getBuildMatches(
src = src.substring(1); src = src.substring(1);
} }
// We need to escape brackets since `glob` will
// try to find a group otherwise
src = src.replace(/(\[|\])/g, '[$1]');
// lambda function files are trimmed of their file extension // lambda function files are trimmed of their file extension
const mapToEntrypoint = new Map<string, string>(); const mapToEntrypoint = new Map<string, string>();
const extensionless = devServer.getExtensionlessFile(src); const extensionless = devServer.getExtensionlessFile(src);

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 url, { URL } from 'url';
import http from 'http'; import http from 'http';
import fs from 'fs-extra'; import fs from 'fs-extra';
import chalk from 'chalk'; import chalk from 'chalk';
import fetch from 'node-fetch';
import plural from 'pluralize'; import plural from 'pluralize';
import rawBody from 'raw-body'; import rawBody from 'raw-body';
import listen from 'async-listen'; import listen from 'async-listen';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import ms from 'ms';
import httpProxy from 'http-proxy'; import httpProxy from 'http-proxy';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler'; import serveHandler from 'serve-handler';
@@ -16,11 +17,11 @@ import path, { isAbsolute, basename, dirname, extname, join } from 'path';
import once from '@tootallnate/once'; import once from '@tootallnate/once';
import directoryTemplate from 'serve-handler/src/directory'; import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port'; import getPort from 'get-port';
import { ChildProcess } from 'child_process';
import isPortReachable from 'is-port-reachable'; import isPortReachable from 'is-port-reachable';
import deepEqual from 'fast-deep-equal'; import deepEqual from 'fast-deep-equal';
import which from 'which'; import which from 'which';
import npa from 'npm-package-arg'; import npa from 'npm-package-arg';
import type { ChildProcess } from 'child_process';
import { getVercelIgnore, fileNameSymbol } from '@vercel/client'; import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
import { import {
@@ -35,12 +36,14 @@ import {
StartDevServerResult, StartDevServerResult,
FileFsRef, FileFsRef,
PackageJson, PackageJson,
spawnCommand,
} from '@vercel/build-utils';
import {
detectBuilders, detectBuilders,
detectApiDirectory, detectApiDirectory,
detectApiExtensions, detectApiExtensions,
spawnCommand,
isOfficialRuntime, isOfficialRuntime,
} from '@vercel/build-utils'; } from '@vercel/fs-detectors';
import frameworkList from '@vercel/frameworks'; import frameworkList from '@vercel/frameworks';
import cmd from '../output/cmd'; import cmd from '../output/cmd';
@@ -90,6 +93,7 @@ import {
import { ProjectEnvVariable, ProjectSettings } from '../../types'; import { ProjectEnvVariable, ProjectSettings } from '../../types';
import exposeSystemEnvs from './expose-system-envs'; import exposeSystemEnvs from './expose-system-envs';
import { treeKill } from '../tree-kill'; import { treeKill } from '../tree-kill';
import { nodeHeadersToFetchHeaders } from './headers';
const frontendRuntimeSet = new Set( const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build') frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
@@ -327,6 +331,8 @@ export default class DevServer {
): Promise<void> { ): Promise<void> {
const name = relative(this.cwd, fsPath); const name = relative(this.cwd, fsPath);
try { try {
await this.getVercelConfig();
this.files[name] = await FileFsRef.fromFsPath({ fsPath }); this.files[name] = await FileFsRef.fromFsPath({ fsPath });
const extensionless = this.getExtensionlessFile(name); const extensionless = this.getExtensionlessFile(name);
if (extensionless) { if (extensionless) {
@@ -593,7 +599,7 @@ export default class DevServer {
await this.exit(); await this.exit();
} }
if (warnings && warnings.length > 0) { if (warnings?.length > 0) {
warnings.forEach(warning => warnings.forEach(warning =>
this.output.warn(warning.message, null, warning.link, warning.action) this.output.warn(warning.message, null, warning.link, warning.action)
); );
@@ -1106,6 +1112,7 @@ export default class DevServer {
view = errorTemplate({ view = errorTemplate({
http_status_code: statusCode, http_status_code: statusCode,
http_status_description, http_status_description,
error_code,
request_id: requestId, request_id: requestId,
}); });
} }
@@ -1337,32 +1344,6 @@ export default class DevServer {
return false; 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. * Serve project directory as a v2 deployment.
*/ */
@@ -1429,13 +1410,143 @@ export default class DevServer {
let statusCode: number | undefined; let statusCode: number | undefined;
let prevUrl = req.url; let prevUrl = req.url;
let prevHeaders: HttpHeadersConfig = {}; let prevHeaders: HttpHeadersConfig = {};
let middlewarePid: number | undefined;
/* // Run the middleware file, if present, and apply any
const middlewareResult = await this.runDevMiddleware(req, res); // 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 (startMiddlewareResult) {
if (middlewareResult.error) { const { port, pid } = startMiddlewareResult;
this.sendError( 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, req,
res, res,
requestId, requestId,
@@ -1443,24 +1554,12 @@ export default class DevServer {
500 500
); );
return; return;
} } finally {
if (middlewareResult.finished) { if (middlewarePid) {
return; this.killBuilderDevServer(middlewarePid);
} }
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);
} }
} }
*/
for (const phase of phases) { for (const phase of phases) {
statusCode = undefined; statusCode = undefined;
@@ -1735,11 +1834,15 @@ export default class DevServer {
entrypoint: match.entrypoint, entrypoint: match.entrypoint,
workPath, workPath,
config: match.config || {}, config: match.config || {},
repoRootPath: this.cwd,
meta: { meta: {
isDev: true, isDev: true,
requestPath, requestPath,
devCacheDir, devCacheDir,
env: { ...envConfigs.runEnv }, env: {
...envConfigs.runEnv,
VERCEL_BUILDER_DEBUG: this.output.debugEnabled ? '1' : undefined,
},
buildEnv: { ...envConfigs.buildEnv }, buildEnv: { ...envConfigs.buildEnv },
}, },
}); });
@@ -2184,13 +2287,7 @@ function proxyPass(
`Failed to complete request to ${req.url}: ${error}` `Failed to complete request to ${req.url}: ${error}`
); );
if (!res.headersSent) { if (!res.headersSent) {
devServer.sendError( devServer.sendError(req, res, requestId, 'FUNCTION_INVOCATION_FAILED');
req,
res,
requestId,
'NO_RESPONSE_FROM_FUNCTION',
502
);
} }
} }
); );
@@ -2268,11 +2365,12 @@ async function findBuildMatch(
if (!isIndex(match.src)) { if (!isIndex(match.src)) {
return match; return match;
} else { } else {
// if isIndex === true and ends in .html, we're done. Otherwise, keep searching // If isIndex === true and ends in `.html`, we're done.
bestIndexMatch = match; // Otherwise, keep searching.
if (extname(match.src) === '.html') { if (extname(match.src) === '.html') {
return bestIndexMatch; return match;
} }
bestIndexMatch = match;
} }
} }
} }
@@ -2294,6 +2392,13 @@ async function shouldServe(
config, config,
builderWithPkg: { builder }, builderWithPkg: { builder },
} = match; } = 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 cleanSrc = src.endsWith('.html') ? src.slice(0, -5) : src;
const trimmedPath = requestPath.endsWith('/') const trimmedPath = requestPath.endsWith('/')
? requestPath.slice(0, -1) ? requestPath.slice(0, -1)
@@ -2446,12 +2551,10 @@ function needsBlockingBuild(buildMatch: BuildMatch): boolean {
return typeof builder.shouldServe !== 'function'; return typeof builder.shouldServe !== 'function';
} }
async function checkForPort( async function checkForPort(port: number, timeout: number): Promise<void> {
port: number | undefined, const opts = { host: '127.0.0.1' };
timeout: number
): Promise<void> {
const start = Date.now(); const start = Date.now();
while (!(await isPortReachable(port))) { while (!(await isPortReachable(port, opts))) {
if (Date.now() - start > timeout) { if (Date.now() - start > timeout) {
throw new Error(`Detecting port ${port} timed out after ${timeout}ms`); throw new Error(`Detecting port ${port} timed out after ${timeout}ms`);
} }

View File

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

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