Compare commits

..

73 Commits

Author SHA1 Message Date
Nathan Rajlich
d48f704eb9 [WIP] Use @vercel/style-guide 2022-09-21 11:15:14 -07:00
Steven
75840896ee [tests] Update workflows to use global env (#8594)
Follow up to #8586 because [that syntax](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable) never worked on windows
2022-09-20 20:16:54 +00:00
Nathan Rajlich
9657bda86d [cli] Add "images" optimization configuration support in vercel.json (#8566)
This is a (further 😄) continuation of #8379 by @theMosaad, so that the tests can run.

I also added a change to make `images` _not_ be sent to the create deployment API endpoint, since it fails validation there.
2022-09-20 19:58:20 +00:00
Steven
4536029d42 Revert "[fs-detectors] Rush json workspace" (#8591)
Reverts vercel/vercel#8565 since tests are failing on windows

https://github.com/vercel/vercel/actions/runs/3091763554/jobs/5002704682
2022-09-20 15:13:23 -04:00
Steven
a35654d047 [tests] Skip test 01-normalize-routes temporarily (#8590)
The  `01-normalize-routes` test is failing on windows so we need to temporarily disable until the root cause can be corrected.
2022-09-20 14:51:01 -04:00
github-actions[bot]
f6a01a1af3 [examples] Upgrade Next.js to version 12.3.1 (#8587)
This auto-generated PR updates Next.js to version 12.3.1
Co-authored-by: Steven <steven@ceriously.com>
2022-09-20 14:13:12 -04:00
chloetedder
c2bca954e0 [fs-detectors] Rush json workspace (#8565)
### Related Issues

Add in Rush workspace

### 📋 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-09-20 12:14:45 -05:00
Steven
21701c3c3d [tests] Fix turbo remote cache (#8586)
Follow up to #7874 since the remote cache was not used on `main`.

We can unconditionally set these env vars with the latest turbo so it will work for contributors too.
2022-09-20 12:19:38 -04:00
JJ Kasper
7f4723b3f8 [next] Update local app edge test (#8588)
### Related Issues

Fixes:
https://github.com/vercel/vercel/actions/runs/3086450305/jobs/4990850756

### 📋 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-09-19 18:15:13 -07:00
Sean Massa
619ca93421 Publish Stable
- @vercel/build-utils@5.4.3
 - vercel@28.3.0
 - @vercel/client@12.2.5
 - @vercel/frameworks@1.1.6
 - @vercel/fs-detectors@3.3.0
 - @vercel/go@2.2.6
 - @vercel/hydrogen@0.0.19
 - @vercel/next@3.1.26
 - @vercel/node@2.5.15
 - @vercel/python@3.1.15
 - @vercel/redwood@1.0.24
 - @vercel/remix@1.0.25
 - @vercel/ruby@1.3.32
 - @vercel/static-build@1.0.24
2022-09-19 16:40:10 -05:00
Steven
cea2981512 [tests] Change some runNpmInstall() unit tests to mocks (#8581)
- Closes #8580
2022-09-19 21:30:32 +00:00
JJ Kasper
1f30e3a4b7 [next] Update app test fixture (#8584)
### Related Issues

Updates our test fixture for related changes in latest canary of
Next.js.

Fixes: https://vercel.slack.com/archives/CGU8HUTUH/p1663607276817069

### 📋 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-09-19 13:08:10 -07:00
Steven
20e8fdd049 [tests] Add yarn cache to github actions (#8567)
We had this enable for some workflows but not all so this PR enables the yarn cache for the remaining workflows.
2022-09-15 02:05:26 +00:00
Chris Barber
3df8313d69 [next] Use posix path separator to join routes and overrides paths (#8530)
Use posix path separators for routes (static pages, lambdas, etc) instead of the native platform path separator (e.g. backslash on Windows).

#### 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-09-14 23:47:57 +00:00
Steven
95c9ea92c4 [build-utils] Add flag for npm install --legacy-peer-deps (#8550)
Prior to `npm@8.6.0`, running `npm install --legacy-peer-deps` to generate a lock file and then later running `npm install` would work fine.

After `npm@8.6.0`, running `npm install` with that same lock file will fail with `code ERESOLVE` and `Conflicting peer dependency`.

Steps to reproduce:

```sh
corepack enable npm
echo '{"packageManager":"npm@8.5.5","dependencies":{"next":"12.3.0","react":"16.8.0"}}' > package.json
npm install --legacy-peer-deps
npm install # works
echo '{"packageManager":"npm@8.6.0","dependencies":{"next":"12.3.0","react":"16.8.0"}}' > package.json
npm install # fails with code ERESOLVE
```


This PR introduces a flag so we can safely upgrade npm to a newer version without breaking thousands of deployments.

The [npm docs](https://docs.npmjs.com/cli/v8/using-npm/config#legacy-peer-deps) say the flag is not recommended so we also make sure that once we upgrade to Node.js 18, we stop flagging `--legacy-peer-deps` because its safe to introduce breaking changes.
2022-09-14 22:18:40 +00:00
Chris Barber
7ddebb099d [build-utils][cli][go][node][ruby][static-build] Explicitly set PATH when copying env vars (#8532)
On Windows 10 and 11 machines, environment variables are not case sensitive. The system PATH is actually defined as `process.env.Path`, however Node.js kindly handles the case sensitivity and will automatically return the system path when specifying `process.env.PATH`.

When we clone the environment variables via `{ ...process.env }`, we lose the automatic resolving of `Path` to `PATH`. To fix this, we need to explicitly copy the `PATH`.

#### 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-09-14 20:29:52 +00:00
chloetedder
6498fd1aab [fs-detectors] Add Nx Workspace (#8549)
### Related Issues


https://nx.dev/more-concepts/folder-structure#integrated-repo-folder-structure

Nx monorepo has an option to use Nx workspaces.
Nx workspace is defined within the root `workspace.json` file
Within this `workspace.json` file the workspace packages are under
projects
```{
  "$schema": "./node_modules/nx/schemas/workspace-schema.json",
  "version": 2,
  "projects": {
    "myblog": "apps/myblog",
    "svelte-app": "apps/svelte-app",
  }
}
```
Within `getNxWorkspacePackagePaths` get the projects object values for
the paths
Nx is listed as the last workspace manager within
`workspace-managers.ts` because other workspace managers could exist to
check for before Nx workspaces because the `workspace.json` could exist
but not be the correct workspace manager

Nx workspace file can exist when yarn/npm workspaces exist. When this
happens, the workspace.json file is empty with no projects so it will
not add any package paths to the list to look through for projects.


### 📋 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-09-14 15:10:14 -05:00
Nathan Rajlich
9d7d822da3 [tests] Add test for path segments when go.mod is present (#7361)
Path segment previously were not working when a `go.mod` file is present. This was fixed likely by https://github.com/vercel/vercel/pull/8291, so now this PR is just a test to ensure this case stays working.
2022-09-14 19:10:49 +00:00
Nathan Rajlich
b8110d97d1 [tests] Add more tests for importBuilders() scenarios (#8554)
Covers:
  - Importing a built-in Builder with `@latest` should use the CLI's version
  - Importing a built-in Builder with `@canary` should use the CLI's version
  - Importing a built-in Builder with an explicit version should install to `.vercel/builders`
2022-09-13 23:56:10 +00:00
Nathan Rajlich
2d9373b0f1 [tests] Fix "50-ionic-react" test fixture (#8556)
Update "typescript" and add a `yarn.lock` file.

It started failing without the lockfile with compilation issues:

```
$ react-scripts build
Creating an optimized production build...
Failed to compile.
/vercel/path1/node_modules/@types/react-router/index.d.ts
TypeScript error in /vercel/path1/node_modules/@types/react-router/index.d.ts(149,100):
Type expected.  TS1110
    147 | ): match<Params> | null;
    148 |
  > 149 | export type ExtractRouteOptionalParam<T extends string, U = string | number | boolean> = T extends `${infer Param}?`
        |                                                                                                    ^
    150 |     ? { [k in Param]?: U }
    151 |     : T extends `${infer Param}*`
    152 |     ? { [k in Param]?: U }
error Command failed with exit code 1.
```
2022-09-13 23:28:36 +00:00
Nathan Rajlich
cc7577a648 [cli] Use Builder importing logic from vc build in vc dev (#8485)
This makes `vc dev` utilize the same Builder installing and importing logic that `vc build` uses. So now the Builders will be installed locally on a per-project basis, rather than being installed to a shared global installation directory.
2022-09-13 21:35:01 +00:00
Nathan Rajlich
f3f3d7df5b [cli] Replace "0.0.0.0" with "localhost" in vc dev (#8547)
Instead of replacing "127.0.0.1" in `vc dev`, replace "0.0.0.0" when rendering the URL to access the `vc dev` server.

The thought here is that the IPv6 form is replacing the catch-all host name (`::`), but the IPv4 version is only replacing the explicit "127.0.0.1" version, so there's an inconsistency.

Considering the catch-all host names are not usually intended to be routable anyways, we will fix the inconsistency by only replacing the catch-all versions instead of localhost.

Also, since Node.js will default to binding to the catch-all address when not explicitly specifying a host, this replacement will only happen in that case. If the user is explicitly specifing a host to bind to (i.e. `vc dev -l 127.0.0.1:8080` or `vc dev -l tcp://[::1]:8080`) then it makes more sense to print explicitly what the user specified.
2022-09-13 18:38:42 +00:00
Steven
0bafea6d51 [tests] Fix tarball lookup with sha contains 0 (#8552)
This PR fixes an issue when the sha begins with `0` and causes [an error](https://vercel.com/vercel/vercel/EP7fVcXKsoodzWRy3fwG8AoeQcs6) when looking up the tarball:

> Error: ENOENT: no such file or directory, 
> copyfile '/vercel/path0/packages/build-utils/vercel-build-utils-v5.4.2-0251253.tgz' 
>  -> '/vercel/path0/public/tarballs/@vercel/build-utils.tgz'

The fix is to no longer rely on the exact tarball name because we can't guarantee the name from `yarn pack` and instead rely on a pattern for the tarball name.
2022-09-13 18:08:45 +00:00
Nathan Rajlich
5abb74eddd [cli] Use isStaticRuntime() helper function when importing Builders (#8551) 2022-09-13 12:40:06 +00:00
Steven
23d7eaccab [frameworks] Update framework logos to be file paths (#8546)
This removes the reliance on raw github hosting and instead relies on the Vercel deployment hosting the logo images.

Since we already have static files in each deployment (tarballs), it makes sense to start adding static images too.
2022-09-12 23:07:46 +00:00
Nathan Rajlich
2cb008e8ed [cli] Switch to npm and add more context to install errors during Builder import (#8545)
Before:

<img width="1012" alt="Screen Shot 2022-09-09 at 7 03 01 PM" src="https://user-images.githubusercontent.com/71256/189464732-8cf6398a-9432-423b-8509-a52bd714333e.png">

After:

<img width="579" alt="Screen Shot 2022-09-12 at 12 27 31 PM" src="https://user-images.githubusercontent.com/71256/189739091-86399428-d9b8-4d03-b0e6-7d27a1037bce.png">

This is a precursor to https://github.com/vercel/vercel/pull/8485, since the last remaining test failure there is related to specialized messaging we had for these same cases.
2022-09-12 22:05:38 +00:00
chloetedder
b5c3610113 Publish Stable
- vercel@28.2.5
 - @vercel/fs-detectors@3.2.0
2022-09-12 11:32:13 -05:00
chloetedder
f23c7fc4fc [fs-detectors] Add monorepo managers (#8534)
### Related Issues

Adding in Nx and Rush as monorepo managers.
This will allow to help with starting zero config for both the above managers.
I have added in unit tests for both Nx and Rush.

### 📋 Checklist

#### 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-09-09 22:51:56 +00:00
Sean Massa
3dc1ad6437 [dev] consolidate dev server address parsing (#8538)
Consolidates parsing and access to `this.address` so that we don't have to `new URL(this.address)` all over the place.

---

Follow-up from: https://github.com/vercel/vercel/pull/8457#issuecomment-1238653904
2022-09-09 22:07:58 +00:00
github-actions[bot]
4dc0bef62b [examples] Upgrade Next.js to version 12.3.0 (#8537)
This auto-generated PR updates Next.js to version 12.3.0

Co-authored-by: Steven <steven@ceriously.com>
2022-09-09 17:41:41 -04:00
Sean Massa
8b3c52b9e8 Publish Stable
- vercel@28.2.4
 - @vercel/frameworks@1.1.5
 - @vercel/fs-detectors@3.1.0
 - @vercel/static-build@1.0.23
2022-09-09 16:28:36 -05:00
Sean Massa
fc74300ad0 [dev] fix middleware rewrites to relative paths that do not exist (#8535)
When using a `rewrite` in middleware to a relative path that does not exist, the logic gets confused and falls back to the original path.

This was caused by two changes that, in combination, caused this behavior.

1. `prevUrl` was created to keep track of route results that point to other routes, but falls back to `req.url` (`prevUrl`'s initial value): [#4033](40e4b69267 (diff-142c93a61d03a1718eb765cd25e5f18d07a95358bb27ce5d5d4da314ee2fa286R1283-R1284))
2. `prevUrl` was reassigned when handling middleware: [#7973](ee1211416f (diff-00ef6e7b63ed4cae298afc2a8c84f49247855a2506270f748e4d3e41da97ad99R1538))

Because `prevUrl` was reset back to `req.url` after the first `phase` (null) was run, the updated `prevUrl` value from middleware was lost. This only matters if the second `phase` ("filesystem") was going to be hit.

Further confusing matters, this was partially fixed by https://github.com/vercel/vercel/pull/8457 because it either returned a proxy pass (which doesn't have this problem) or assigned `req.url` to include the `rewritePath`, which meant that later when `prevUrl` would default to `req.url`, it still had come from `rewritePath`. So, this is fixed for the absolute URL case, but not the relative path case.

Given all that, I think the fix we need is to keep `prevUrl` at its current value when it's not being updated based on the `routeResult`.

---

So, I made that change here. I added a test that exercises this specific behavior.
2022-09-09 20:34:14 +00:00
Andrew Gadzik
28f8a38e00 [fs-detectors] Add the writeFile function to DetectorFilesystem (#8536)
Adds a `writeFile` function to `DetectorFilesystem` that will be used to update the various file cache maps.

**Why is this needed?**

When detecting npm7+ monorepos, we identified a performance improvement where the service can inspect the `package-lock.json` file for workspaces, and reuse the package information for each workspace in framework-detection.

The pseudo code in `vercel/api` will look something like this

For a given lockfile
```json
{
  ...,
  "packages": {
    "": {
      "name": "npm-workspaces",
      "version": "1.0.0",
      "license": "ISC",
      "workspaces": {
        "packages": [
          "apps/*"
        ]
      }
    },
    "apps/admin": {
      "version": "0.1.0",
      "dependencies": {
        "next": "12.2.5",
        "react": "18.2.0",
        "react-dom": "18.2.0"
      },
      "devDependencies": {
        "eslint": "8.23.0",
        "eslint-config-next": "12.2.5"
      }
    },
    ...,
}
```

```ts
// for each projectPath we detect in package-lock.json
// switch the cwd of the fs to the project directory
const projectFs = fs.chdir(projectPath);
// gets the package info from the lockfile
const projectPackageInfo = lockFile.packages[projectPath];
// insert this content into fs cache
await projectFs.writeFile('package.json', projectPackageInfo)
// call detectFramework, which should now have a cached "package.json" file 
const projectFramework = await detectFramework(projectFs);
```

### Related Issues

Related to https://linear.app/vercel/issue/HIT-57/monorepo-detection-api-prevent-rate-limits

### 📋 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-09-09 19:39:05 +00:00
Jacob Ford
c318ce9695 [frameworks] Detect Sanity v3 config (#8540)
Sanity v3 is currently in [developer preview](https://www.sanity.io/blog/sanity-studio-v3-developer-preview), and has deprecated `sanity.json` in the project root, replacing it with `sanity.config.js`. This should detect either.

All commands remain the same in both v2 and v3.

Co-authored-by: Steven <steven@ceriously.com>
2022-09-09 14:59:12 -04:00
Nathan Rajlich
5b36eaacff [cli] Poll the Dev Command server on both IPv4 and IPv6 in vc dev (#8542)
This change will allow the downstream Dev Command server be able to listen on either of `127.0.0.1` for IPv4 or `[::1]` for IPv6.

- Fixes #6639
- Fixes #8511
- Fixes #8121
- Closes #8512
- Closes #8384
2022-09-09 14:57:50 -04:00
Chris Barber
c9f7ca23a8 [tests] Skip tests that won't run on Windows due to 'mkdir -p' and symlinks (#8531)
Found a few tests that won't run on Windows. The project settings override test runs `mkdir -p` which errors. The other two build tests fail because they try to create symlinks.

#### 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-09-08 14:26:30 +00:00
Sean Massa
57e0db0f65 [dev] allow middleware rewrites to change origin in vc dev (#8457)
Middleware rewrites in `vc dev` were not respecting changes to the origin. This PR implements that to match prodution.

---

Paired with: @TooTallNate
2022-09-07 20:30:10 +00:00
Steven
d5537500d8 Publish Stable
- @vercel/build-utils@5.4.2
 - vercel@28.2.3
 - @vercel/client@12.2.4
 - @vercel/frameworks@1.1.4
 - @vercel/fs-detectors@3.0.0
 - @vercel/go@2.2.5
 - @vercel/hydrogen@0.0.18
 - @vercel/next@3.1.25
 - @vercel/node@2.5.14
 - @vercel/python@3.1.14
 - @vercel/redwood@1.0.23
 - @vercel/remix@1.0.24
 - @vercel/ruby@1.3.31
 - @vercel/static-build@1.0.22
2022-09-07 15:39:47 -04:00
Andrew Gadzik
4b1736b2f2 [fs-detectors] optimize readdir for framework detection (#8497)
Adding the ability to explicitly set the `hasPath` and `isFile` caches via `readdir`

**Why is this needed?**

For framework detection, this allows us to fetch the contents of a directory (the root directory) of a git repo, and make assumptions about the existence of files,

```ts
import frameworks from "@vercel/frameworks";

// we can make the set of all file paths needed for framework detection like this
const setOfPaths = new Set(
  frameworks.frameworks
    .flatMap((f) => [
      ...(f.detectors?.every ?? []),
      ...(f.detectors?.some ?? []),
    ])
    .map((d) => d.path)
);

// then we can read the contents of the root directory of a git repo via the `DetectorFilesystem`
fs.readdir('./', { potentialFiles: [...setOfPaths] });

// The filesystem has been fully primed - any network calls from here will only happen if needed.
// This works because the logic of `detectFramework` calls `hasPath` and then `isFile` before 
// fetching file contents.  Since the filesystem knows this information, it doesn't have to make 
// any additional network calls 🙌 
const framework = detectFramework(fs);
```

### Related Issues

Related to [HIT-57](https://linear.app/vercel/issue/HIT-57/monorepo-detection-api-prevent-rate-limits)

### 📋 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-09-07 15:35:56 -04:00
Sean Massa
9da1c6fa66 [tests] set retry count to 0 when running tests locally (#8529)
When running tests locally that fail to make a fetch request, the retries add a lot of noise to debugging. This PR sets those retry counts to `0` locally, but keeps them at their current value for CI.
2022-09-07 18:47:09 +00:00
Jiachi Liu
a6c320846e [@vercel/next] Handle nextjs edge routes from app dir (#8524)
### Related Issues

Add handling for edge SSR in app dir, normalize the edge page routes
from app dir, like what we did for ones in `pages/`.

### 📋 Checklist

#### 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

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-09-06 16:19:28 -07:00
Shu Uesugi
4973814978 [frameworks] Update Next.js Logos (#8505)
Update them to match the new logos. See: https://vercel.com/design/brands#next-js

<img width="869" alt="Screen Shot 2022-09-02 at 10 43 52 AM" src="https://user-images.githubusercontent.com/992008/188208956-aae10437-77ef-43c8-9bc1-c2499e509a7c.png">
2022-09-02 18:06:31 +00:00
Sean Massa
f1289ff263 [node] update middleware-no-response behavior to match production changes (#8500)
Production was updated to allow middleware without responses to pass through. This PR makes `vc dev` behave the same way. Tests were updated.
2022-09-01 22:19:10 +00:00
Steven
3ff93279cd Publish Stable
- vercel@28.2.2
 - @vercel/next@3.1.24
 - @vercel/node@2.5.13
 - @vercel/redwood@1.0.22
 - @vercel/remix@1.0.23
2022-09-01 16:42:27 -04:00
Steven
58f9d649a8 [next][node][remix][redwood] Bump @vercel/nft@0.22.1 (#8502)
Bump `@vercel/nft` to version [0.22.1](https://github.com/vercel/nft/releases/tag/0.22.1)
2022-09-01 16:41:30 -04:00
JJ Kasper
cadc082ad1 [next] Fix index app dir case (#8501)
### Related Issues

This ensures we properly normalize the `/` case from the `app-path-routes-manifest` as the outputs expect this to be normalized to `index`. 

### 📋 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-09-01 12:06:18 -07:00
Steven
2906d83eae Publish Stable
- @vercel/build-utils@5.4.1
 - vercel@28.2.1
 - @vercel/client@12.2.3
 - @vercel/edge@0.0.4
 - @vercel/fs-detectors@2.1.0
 - @vercel/go@2.2.4
 - @vercel/hydrogen@0.0.17
 - @vercel/next@3.1.23
 - @vercel/node@2.5.12
 - @vercel/python@3.1.13
 - @vercel/redwood@1.0.21
 - @vercel/remix@1.0.22
 - @vercel/ruby@1.3.30
 - @vercel/static-build@1.0.21
2022-09-01 13:16:42 -04:00
Steven
675c3e2915 [cli] Change error message from Error! to Error: (#8498)
We have code that tries to detect and highlight errors in the build logs, however it doesn't look for `Error!`, only `Error:`.

We could update that highlight code or we could update Vercel CLI to make it consistent.

This PR is the latter.
2022-09-01 13:12:41 -04:00
Steven
13a8a7dbf6 [next] Revert nft to 0.21.0 (#8499)
In Next.js 12, we introduce `outputFileTracing` as the default behavior. This moved `@vercel/nft` from `@vercel/next` into `next` itself so users can upgrade or downgrade Next.js if there is a bug in `@vercel/nft`.

Unfortunately, some users are setting `outputFileTracing: false` which deopts tracing back from `next` to `@vercel/next`. So we should pin `@vercel/nft` here and never upgrade in case of any bugs.
2022-09-01 09:13:35 -07:00
Andrew Gadzik
b480e632a3 Add writeFile to DetectorFilesystem (#8493)
Adds a `writeFile` function to `DetectorFilesystem` that will be used to update the various file cache maps.

When detecting npm7+ monorepos, we identified a performance improvement where the service can inspect the `package-lock.json` file for workspaces, and reuse the package information for each workspace in framework-detection.

The pseudo code in `vercel/api` will look something like this

For a given lockfile
```json
{
  ...,
  "packages": {
    "": {
      "name": "npm-workspaces",
      "version": "1.0.0",
      "license": "ISC",
      "workspaces": {
        "packages": [
          "apps/*"
        ]
      }
    },
    "apps/admin": {
      "version": "0.1.0",
      "dependencies": {
        "next": "12.2.5",
        "react": "18.2.0",
        "react-dom": "18.2.0"
      },
      "devDependencies": {
        "eslint": "8.23.0",
        "eslint-config-next": "12.2.5"
      }
    },
    ...,
}
```

```ts
// for each projectPath we detect in package-lock.json
// switch the cwd of the fs to the project directory
const projectFs = fs.chdir(projectPath);
// gets the package info from the lockfile
const projectPackageInfo = lockFile.packages[projectPath];
// insert this content into fs cache
projectFs.writeFile('package.json', projectPackageInfo)
// call detectFramework, which should now have a cached "package.json" file 
const projectFramework = await detectFramework(projectFs);
```

### Related Issues

Related to [HIT-57](https://linear.app/vercel/issue/HIT-57/monorepo-detection-api-prevent-rate-limits)

### 📋 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-08-31 18:51:03 +00:00
Chris Barber
bc6c364888 [node] Added 'cause' to error message (#8467)
When running `vc dev` for a project using `experimental-edge` runtime and a fetch-related error occurs, the message is a little vague:

```
Unhandled rejection: fetch failed
```

In this case, the fetch promise (defined in @edge-runtime/primitives) is being rejected with a `TypeError: fetch failed` that has a `cause` property containing the actual `response.error` message. The `response.error` is the actual error and is never reported to the user.

This PR simply appends the `cause` message, if exists, to the fetch failure message.

```
Unhandled rejection: fetch failed: Error: getaddrinfo ENOTFOUND undefined
```

### 📋 Checklist

#### 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-08-30 02:32:38 +00:00
Steven
6cad10c899 [build-utils] Replace npm bin with custom implementation (#8483)
This PR removes `npm bin` in favor of a custom implementation since the command will be removed in npm 9.

- Related to https://github.com/npm/statusboard/issues/537
2022-08-30 01:23:34 +00:00
Gal Schlezinger
0d78ec4666 [edge] Add markdown documentation using TypeDoc (#8446) 2022-08-29 12:36:44 +03:00
Nathan Rajlich
b05d653cf1 [cli] Use unique "name" in package.json test fixtures (#8475)
This fixes a few warnings when running Jest that look something like:

```
jest-haste-map: Haste module naming collision: nextjs
  The following files share their name; please adjust your hasteImpl:
    * <rootDir>/test/dev/fixtures/10-nextjs-node/package.json
    * <rootDir>/test/dev/fixtures/25-nextjs-src-dir/package.json
```
2022-08-26 21:43:25 +00:00
Nathan Rajlich
5a38b94de2 [cli] Replace "progress" module with custom implementation (#8397)
The `progress` module is overkill for what we are using it for and it's inconsistent with our styling for the rest of the deployment process.

So drop the npm module in favor of an in-house progress bar implementation that can be used inside of the Ora `output.spinner()` that we already use for the other steps of the deployment process.

**Before:**

<img width="307" alt="Screen Shot 2022-08-25 at 5 07 28 PM" src="https://user-images.githubusercontent.com/71256/186790052-18431ac0-b05b-434e-b885-9eb76d880b9d.png">

**After:**

<img width="347" alt="Screen Shot 2022-08-25 at 5 09 35 PM" src="https://user-images.githubusercontent.com/71256/186790168-8dd3e9a2-863a-45cc-bd21-d98308d34e22.png">
2022-08-26 21:03:38 +00:00
Nathan Rajlich
6668d6cf21 [client] Add "tar-fs" dependency (#8473)
Fixes #8471.
2022-08-26 18:02:01 +00:00
Andrew Gadzik
b63f444abd [fs-detectors] Add a default workspace manager when there's no lock file (#8469)
This fixes an issue with this example,

https://vercel.com/api/v1/integrations/detect-eligible-projects?url=https%3A%2F%2Fgithub.com%2Fvercel%2Fturborepo%2Ftree%2Fmain%2Fexamples%2Fkitchen-sink

Where the repo has no lockfile, and hence our workspace detection fails.  This fix is to add a fallback of "npm" in this scenario

### Related Issues

Fixes https://github.com/vercel/turbo-internal/issues/232

### 📋 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-08-26 16:24:13 +00:00
Steven
9d71d4332f [tests] Update codeowners for fs-detectors (#8470)
Update codeowners for `@vercel/fs-detectors` to add @agadzik and @chloetedder
2022-08-26 15:43:14 +00:00
Steven
151db21a2f [tests] Add required labels (#8464)
This action will fail if the required labels are not applied to a PR.

We allow multiple `area` labels and only a single `semver` label.

- Example failure: https://github.com/vercel/vercel/runs/8024949804?check_suite_focus=true#step:2:19
- Example success: https://github.com/vercel/vercel/runs/8026212031?check_suite_focus=true#step:2:19
2022-08-25 22:20:37 +00:00
Steven
aba9c95ea2 [tests] Change PR automerge label (#8465)
We decided to change this to an explicit label and rely on a separate action to check for correct labels (see #8464).

This action likely won't work for PRs from forks, but that might be okay because our other actions don't work for forks either.
2022-08-25 22:05:55 +00:00
Sean Massa
e7e0a55b72 Publish Stable
- @vercel/build-utils@5.4.0
 - vercel@28.2.0
 - @vercel/client@12.2.2
 - @vercel/go@2.2.3
 - @vercel/hydrogen@0.0.16
 - @vercel/next@3.1.22
 - @vercel/node@2.5.11
 - @vercel/python@3.1.12
 - @vercel/redwood@1.0.20
 - @vercel/remix@1.0.21
 - @vercel/ruby@1.3.29
 - @vercel/static-build@1.0.20
2022-08-25 15:50:50 -05:00
Sean Massa
c2163e3e4f [tests] Update CODEOWNERS (#8463)
Changes:

- added @cb1kenobi @Ethan-Arrowood to sections owned by the Vercel CLI team
- removed outdated code owners: if this was you and you believe this was in error, let us know!
- removed no-op lines
2022-08-25 20:06:49 +00:00
Nathan Rajlich
873f549637 [cli] Normalize Builder output paths in vc dev (#8461)
Fixes https://github.com/vercel/og-image/issues/180.
2022-08-25 19:09:12 +00:00
Naoyuki Kanezawa
ead1e411ee [next] support middleware-manifest v2 (#8319)
### Related Issues

As the title, support a new version of middleware-manifest of next.js that is going to be added in https://github.com/vercel/next.js/pull/39257

### 📋 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-08-25 18:57:12 +00:00
Sean Massa
7c1c089a70 [tests] rename go test function (#8458)
This function name can be anything, but the current one is confusing. This is what the function gets renamed to during compilation. Let's change it back to avoid confusion.
2022-08-25 15:11:05 +00:00
Ethan Arrowood
a7dbd9649b [cli] add override support for vc build and vc dev (#8451)
### Related Issues

Adds configuration override support to `vc build` and `vc dev` commands.

### 📋 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-08-25 03:00:52 +00:00
Chris Barber
2e439045c9 [cli] Improved 'vc git' help description for --yes flag (#8460)
When adding the `--yes` flag to the `vc git` command's help screen, the description was copy/pasted and not accurate, so this PR improves the description to be more accurate.

### 📋 Checklist

#### 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-08-25 01:25:30 +00:00
Steven
520e0d01f4 [frameworks] Fix link to framework logo (#8459)
This link should be using our custom domain
2022-08-24 16:07:17 -04:00
Steven
3856623785 [build-utils] Prefix debug message (#8378)
This PR adds `VERCEL_DEBUG_PREFIX` environment variable which lets you set a prefix for the debug logs.
2022-08-24 18:50:20 +00:00
Steven
1c14e945f9 [tests] Reduce test logs (#8455)
This PR reduces the noisy test logs that currently look like this:

<img width="1013" alt="image" src="https://user-images.githubusercontent.com/229881/186479890-e954ca9c-782e-49b8-89e7-6409185bb490.png">
2022-08-24 14:00:10 -04:00
Steven
32357fc06f [node][next][redwood][remix] Bump @vercel/nft@0.22.0 (#8454)
This PR bumps `@vercel/nft` to version [0.22.0](https://github.com/vercel/nft/releases/tag/0.22.0)
2022-08-24 12:13:34 -04:00
Lee Robinson
f7c57dc539 [examples] Update SvelteKit example for breaking changes. (#8439)
There were recently a few breaking changes introduced into SvelteKit. This PR clones that latest changes from a demo SvelteKit application, while retaining support for Vercel Analytics.

Deployed latest to https://sveltekit-template.vercel.app/ and confirmed analytics are sending.
2022-08-24 15:15:50 +00:00
github-actions[bot]
34f7c35c13 [examples] Upgrade Next.js to version 12.2.5 (#8453)
This auto-generated PR updates Next.js to version 12.2.5

Co-authored-by: Vercel Team Bot <team@zeit.co>
2022-08-24 10:41:54 -04:00
559 changed files with 25737 additions and 12864 deletions

View File

@@ -8,7 +8,6 @@ packages/*/test/fixtures
# cli
packages/cli/@types
packages/cli/download
packages/cli/dist
packages/cli/test/dev/fixtures
packages/cli/bin

15
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
root: true,
extends: [
require.resolve('@vercel/style-guide/eslint/node'),
require.resolve('@vercel/style-guide/eslint/typescript'),
],
overrides: [
{
files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
extends: [
require.resolve('@vercel/style-guide/eslint/jest'),
],
},
],
};

28
.github/CODEOWNERS vendored
View File

@@ -1,23 +1,17 @@
# Documentation
# https://help.github.com/en/articles/about-code-owners
* @TooTallNate @EndangeredMassa @styfle
/.github/workflows @TooTallNate @EndangeredMassa @styfle @ijjk
/packages/frameworks @TooTallNate @EndangeredMassa @styfle @AndyBitz
/packages/cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
/packages/cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
/packages/cli/src/commands/env @styfle @lucleray
/packages/client @TooTallNate @EndangeredMassa @styfle
/packages/build-utils @TooTallNate @EndangeredMassa @styfle @AndyBitz
/packages/middleware @gdborton @javivelasco
/packages/node @TooTallNate @EndangeredMassa @styfle
/packages/node-bridge @TooTallNate @EndangeredMassa @styfle @ijjk
/packages/next @TooTallNate @ijjk
/packages/go @TooTallNate @EndangeredMassa @styfle
/packages/python @TooTallNate @EndangeredMassa @styfle
/packages/ruby @TooTallNate @EndangeredMassa @styfle
/packages/static-build @TooTallNate @EndangeredMassa @styfle @AndyBitz
/packages/routing-utils @TooTallNate @EndangeredMassa @styfle @ijjk
* @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood
/.github/workflows @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
/packages/cli/src/commands/domains @mglagola @anatrajkovska
/packages/cli/src/commands/certs @mglagola @anatrajkovska
/packages/cli/src/commands/env @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood
/packages/fs-detectors @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @agadzik @chloetedder
/packages/middleware @gdborton @vercel/edge-function
/packages/node-bridge @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
/packages/next @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
/packages/routing-utils @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
/packages/edge @vercel/edge-function
/examples @leerob
/examples/create-react-app @Timer
/examples/nextjs @timneutkens @ijjk @styfle

View File

@@ -34,6 +34,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 14
cache: 'yarn'
- name: Install
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
run: yarn install --check-files --frozen-lockfile --network-timeout 1000000

22
.github/workflows/required-pr-label.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Required PR Label
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Check PR Labels
uses: actions/github-script@v6
with:
script: |
const labels = context.payload.pull_request.labels.map(l => l.name);
if (labels.filter(l => l.startsWith('area:')).length === 0) {
console.error('\u001b[31mMissing label: Please add at least one "area" label.');
process.exit(1);
}
if (labels.filter(l => l.startsWith('semver:')).length !== 1) {
console.error('\u001b[31mMissing label: Please add exactly one "semver" label.');
process.exit(1);
}
console.log('\u001b[32mSuccess: This pull request has correct labels, thanks!');

View File

@@ -8,6 +8,11 @@ on:
- '!*'
pull_request:
env:
TURBO_REMOTE_ONLY: 'true'
TURBO_TEAM: 'vercel'
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
jobs:
test:
name: CLI
@@ -19,25 +24,16 @@ jobs:
node: [14]
runs-on: ${{ matrix.os }}
steps:
- name: Conditionally set remote env
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
echo "TURBO_REMOTE_ONLY=true" >> $GITHUB_ENV
echo "TURBO_TEAM=vercel" >> $GITHUB_ENV
echo "TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}" >> $GITHUB_ENV
- uses: actions/checkout@v3
with:
fetch-depth: 2
- uses: actions/setup-go@v3
with:
go-version: '1.13.15'
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: actions/checkout@v3
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin main --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/main...HEAD --name-only
cache: 'yarn'
- run: yarn install --network-timeout 1000000 --frozen-lockfile
- run: yarn run build
- run: yarn test-integration-cli

View File

@@ -8,6 +8,11 @@ on:
- '!*'
pull_request:
env:
TURBO_REMOTE_ONLY: 'true'
TURBO_TEAM: 'vercel'
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
jobs:
test:
name: Unit
@@ -19,25 +24,16 @@ jobs:
node: [14]
runs-on: ${{ matrix.os }}
steps:
- name: Conditionally set remote env
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
echo "TURBO_REMOTE_ONLY=true" >> $GITHUB_ENV
echo "TURBO_TEAM=vercel" >> $GITHUB_ENV
echo "TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}" >> $GITHUB_ENV
- uses: actions/setup-go@v3
with:
go-version: '1.13.15'
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: actions/checkout@v3
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin main --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/main...HEAD --name-only
fetch-depth: 2
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: 'yarn'
- run: yarn install --network-timeout 1000000 --frozen-lockfile
- run: yarn run build
- run: yarn run lint

View File

@@ -10,6 +10,9 @@ on:
env:
NODE_VERSION: '14'
TURBO_REMOTE_ONLY: 'true'
TURBO_TEAM: 'vercel'
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
jobs:
setup:
@@ -20,8 +23,11 @@ jobs:
dplUrl: ${{ steps.waitForTarball.outputs.url }}
steps:
- uses: actions/checkout@v3
- run: git --version
- run: git fetch origin main
with:
fetch-depth: 2
- uses: actions/setup-go@v3
with:
go-version: '1.13.15'
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
@@ -52,12 +58,6 @@ jobs:
matrix:
include: ${{ fromJson(needs.setup.outputs['tests']) }}
steps:
- name: Conditionally set remote env
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
echo "TURBO_REMOTE_ONLY=true" >> $GITHUB_ENV
echo "TURBO_TEAM=vercel" >> $GITHUB_ENV
echo "TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}" >> $GITHUB_ENV
- uses: actions/checkout@v3
with:
fetch-depth: 2

View File

@@ -1,7 +1,7 @@
version = 1
[merge]
automerge_label = ["semver-major","semver-minor","semver-patch"]
automerge_label = ["pr: automerge"]
blacklist_title_regex = "^WIP.*"
blacklist_labels = ["work in progress"]
method = "squash"

View File

@@ -1,6 +1,5 @@
import fs from 'fs/promises';
import { join, dirname } from 'path';
import execa from 'execa';
import { getExampleList } from '../examples/example-list';
import { mapOldToNew } from '../examples/map-old-to-new';
@@ -13,6 +12,12 @@ async function main() {
await fs.rm(pubDir, { recursive: true, force: true });
await fs.mkdir(pubDir);
await fs.cp(
join(repoRoot, 'packages', 'frameworks', 'logos'),
join(pubDir, 'framework-logos'),
{ recursive: true, force: true }
);
const examples = await getExampleList();
const pathListAll = join(pubDir, 'list-all.json');
await fs.writeFile(pathListAll, JSON.stringify(examples));
@@ -41,10 +46,6 @@ async function main() {
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);
@@ -55,12 +56,21 @@ async function main() {
'utf-8'
);
const packageJson = JSON.parse(packageJsonRaw);
const tarballName = `${packageJson.name
.replace('@', '')
.replace('/', '-')}-v${packageJson.version}-${sha.trim()}.tgz`;
const files = await fs.readdir(fullDir);
const tarballName = files.find(f => /^vercel-.+\.tgz$/.test(f));
if (!tarballName) {
throw new Error(
`Expected vercel-*.tgz in ${fullDir} but found ${JSON.stringify(
files,
null,
2
)}`
);
}
const srcTarballPath = join(fullDir, tarballName);
const destTarballPath = join(tarballsDir, `${packageJson.name}.tgz`);
await fs.mkdir(dirname(destTarballPath), { recursive: true });
await fs.copyFile(join(fullDir, tarballName), destTarballPath);
await fs.copyFile(srcTarballPath, destTarballPath);
}
console.log('Completed building static frontend.');

View File

@@ -16,10 +16,6 @@ const frameworks = (_frameworks as Framework[])
defaultRoutes: undefined,
};
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
return framework;
});

View File

@@ -26,10 +26,11 @@ yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
{
"name": "nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -7,12 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"next": "12.1.4",
"react": "18.0.0",
"react-dom": "18.0.0"
"next": "12.3.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"eslint": "8.12.0",
"eslint-config-next": "12.1.4"
"eslint": "8.23.1",
"eslint-config-next": "12.3.1"
}
}

View File

@@ -114,3 +114,16 @@
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}

View File

@@ -14,3 +14,13 @@ a {
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}

1703
examples/nextjs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,21 +8,9 @@ Everything you need to build a Svelte project, powered by [`create-svelte`](http
_Live Example: https://sveltekit-template.vercel.app_
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte
# create a new project in my-app
npm init svelte my-app
```
## Developing
Once you've created a project and installed dependencies with `pnpm install`, start a development server:
Once you've installed dependencies with `pnpm install`, start a development server:
```bash
pnpm run dev

View File

@@ -10,4 +10,8 @@
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -1,25 +1,25 @@
{
"private": true,
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"dev": "vite dev",
"build": "vite build",
"package": "svelte-kit package",
"preview": "svelte-kit preview",
"prepare": "svelte-kit sync",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-check --tsconfig ./jsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. .",
"format": "prettier --write --plugin-search-dir=. ."
"lint": "prettier --check .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "1.0.0-next.50",
"@sveltejs/kit": "1.0.0-next.347",
"@types/cookie": "^0.4.1",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0",
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@types/cookie": "^0.5.1",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.46.0",
"svelte-check": "^2.2.6",
"typescript": "~4.6.2"
"svelte-check": "^2.7.1",
"typescript": "^4.7.4",
"vite": "^3.0.8"
},
"type": "module",
"dependencies": {

1458
examples/sveltekit/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Locals {
userid: string;
@@ -9,7 +8,7 @@ declare namespace App {
// interface Platform {}
// interface Session {}
// interface PrivateEnv {}
// interface Stuff {}
// interface PublicEnv {}
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>

View File

@@ -1,107 +1,107 @@
<script>
import { spring } from 'svelte/motion';
import { spring } from 'svelte/motion';
let count = 0;
let count = 0;
const displayed_count = spring();
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
const displayed_count = spring();
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
/**
* @param {number} n
* @param {number} m
*/
function modulo(n, m) {
// handle negative numbers
return ((n % m) + m) % m;
}
/**
* @param {number} n
* @param {number} m
*/
function modulo(n, m) {
// handle negative numbers
return ((n % m) + m) % m;
}
</script>
<div class="counter">
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" />
</svg>
</button>
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" />
</svg>
</button>
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong>
</div>
</div>
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong>
</div>
</div>
<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg>
</button>
<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg>
</button>
</div>
<style>
.counter {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.counter {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.counter button {
width: 2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background-color: transparent;
touch-action: manipulation;
color: var(--text-color);
font-size: 2rem;
}
.counter button {
width: 2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background-color: transparent;
touch-action: manipulation;
color: var(--text-color);
font-size: 2rem;
}
.counter button:hover {
background-color: var(--secondary-color);
}
.counter button:hover {
background-color: var(--secondary-color);
}
svg {
width: 25%;
height: 25%;
}
svg {
width: 25%;
height: 25%;
}
path {
vector-effect: non-scaling-stroke;
stroke-width: 2px;
stroke: var(--text-color);
}
path {
vector-effect: non-scaling-stroke;
stroke-width: 2px;
stroke: var(--text-color);
}
.counter-viewport {
width: 8em;
height: 4em;
overflow: hidden;
text-align: center;
position: relative;
}
.counter-viewport {
width: 8em;
height: 4em;
overflow: hidden;
text-align: center;
position: relative;
}
.counter-viewport strong {
position: absolute;
display: flex;
width: 100%;
height: 100%;
font-weight: 400;
color: var(--accent-color);
font-size: 4rem;
align-items: center;
justify-content: center;
}
.counter-viewport strong {
position: absolute;
display: flex;
width: 100%;
height: 100%;
font-weight: 400;
color: var(--accent-color);
font-size: 4rem;
align-items: center;
justify-content: center;
}
.counter-digits {
position: absolute;
width: 100%;
height: 100%;
}
.counter-digits {
position: absolute;
width: 100%;
height: 100%;
}
.hidden {
top: -100%;
user-select: none;
}
.hidden {
top: -100%;
user-select: none;
}
</style>

View File

@@ -31,11 +31,11 @@ import { invalidate } from '$app/navigation';
export function enhance(form, { pending, error, result } = {}) {
let current_token;
/** @param {SubmitEvent} e */
async function handle_submit(e) {
/** @param {SubmitEvent} event */
async function handle_submit(event) {
const token = (current_token = {});
e.preventDefault();
event.preventDefault();
const data = new FormData(form);
@@ -63,11 +63,11 @@ export function enhance(form, { pending, error, result } = {}) {
} else {
console.error(await response.text());
}
} catch (e) {
if (error && e instanceof Error) {
error({ data, form, error: e, response: null });
} catch (err) {
if (error && err instanceof Error) {
error({ data, form, error: err, response: null });
} else {
throw e;
throw err;
}
}
}

View File

@@ -1,124 +1,124 @@
<script>
import { page } from '$app/stores';
import logo from './svelte-logo.svg';
import { page } from '$app/stores';
import logo from './svelte-logo.svg';
</script>
<header>
<div class="corner">
<a href="https://kit.svelte.dev">
<img src={logo} alt="SvelteKit" />
</a>
</div>
<div class="corner">
<a href="https://kit.svelte.dev">
<img src={logo} alt="SvelteKit" />
</a>
</div>
<nav>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}>
<a sveltekit:prefetch href="/about">About</a>
</li>
<li class:active={$page.url.pathname === '/todos'}>
<a sveltekit:prefetch href="/todos">Todos</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
</svg>
</nav>
<nav>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}>
<a sveltekit:prefetch href="/about">About</a>
</li>
<li class:active={$page.url.pathname === '/todos'}>
<a sveltekit:prefetch href="/todos">Todos</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
</svg>
</nav>
<div class="corner">
<!-- TODO put something else here? github link? -->
</div>
<div class="corner">
<!-- TODO put something else here? github link? -->
</div>
</header>
<style>
header {
display: flex;
justify-content: space-between;
}
header {
display: flex;
justify-content: space-between;
}
.corner {
width: 3em;
height: 3em;
}
.corner {
width: 3em;
height: 3em;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner img {
width: 2em;
height: 2em;
object-fit: contain;
}
.corner img {
width: 2em;
height: 2em;
object-fit: contain;
}
nav {
display: flex;
justify-content: center;
--background: rgba(255, 255, 255, 0.7);
}
nav {
display: flex;
justify-content: center;
--background: rgba(255, 255, 255, 0.7);
}
svg {
width: 2em;
height: 3em;
display: block;
}
svg {
width: 2em;
height: 3em;
display: block;
}
path {
fill: var(--background);
}
path {
fill: var(--background);
}
ul {
position: relative;
padding: 0;
margin: 0;
height: 3em;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background: var(--background);
background-size: contain;
}
ul {
position: relative;
padding: 0;
margin: 0;
height: 3em;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background: var(--background);
background-size: contain;
}
li {
position: relative;
height: 100%;
}
li {
position: relative;
height: 100%;
}
li.active::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--accent-color);
}
li.active::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--accent-color);
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 1em;
color: var(--heading-color);
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 1em;
color: var(--heading-color);
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover {
color: var(--accent-color);
}
a:hover {
color: var(--accent-color);
}
</style>

View File

@@ -0,0 +1,58 @@
<script>
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';
let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID;
$: if (browser && analyticsId) {
webVitals({
path: $page.url.pathname,
params: $page.params,
analyticsId
})
}
</script>
<Header />
<main>
<slot />
</main>
<footer>
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
</footer>
<style>
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
width: 100%;
max-width: 1024px;
margin: 0 auto;
box-sizing: border-box;
}
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 40px 0;
}
}
</style>

View File

@@ -0,0 +1 @@
export const prerender = true;

View File

@@ -0,0 +1,57 @@
<script>
import Counter from '$lib/Counter.svelte';
</script>
<svelte:head>
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>
<section>
<h1>
<span class="welcome">
<picture>
<source srcset="svelte-welcome.webp" type="image/webp" />
<img src="svelte-welcome.png" alt="Welcome" />
</picture>
</span>
to your new<br />SvelteKit app
</h1>
<h2>
try editing <strong>src/routes/index.svelte</strong>
</h2>
<Counter />
</section>
<style>
section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
h1 {
width: 100%;
}
.welcome {
display: block;
position: relative;
width: 100%;
height: 0;
padding: 0 0 calc(100% * 495 / 2048) 0;
}
.welcome img {
position: absolute;
width: 100%;
height: 100%;
top: 0;
display: block;
}
</style>

View File

@@ -1,58 +0,0 @@
<script>
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';
let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID;
$: if (browser && analyticsId) {
webVitals({
path: $page.url.pathname,
params: $page.params,
analyticsId
})
}
</script>
<Header />
<main>
<slot />
</main>
<footer>
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
</footer>
<style>
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
width: 100%;
max-width: 1024px;
margin: 0 auto;
box-sizing: border-box;
}
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 40px 0;
}
}
</style>

View File

@@ -1,50 +0,0 @@
<script context="module">
import { browser, dev } from '$app/env';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement...
export const hydrate = dev;
// ...but if the client-side router is already loaded
// (i.e. we came here from elsewhere in the app), use it
export const router = browser;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod
export const prerender = true;
</script>
<svelte:head>
<title>About</title>
<meta name="description" content="About this app" />
</svelte:head>
<div class="content">
<h1>About this app</h1>
<p>
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
following into your command line and following the prompts:
</p>
<pre>npm init svelte</pre>
<p>
The page you're looking at is purely static HTML, with no client-side interactivity needed.
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
the devtools network panel and reloading.
</p>
<p>
The <a href="/todos">TODOs</a> page illustrates SvelteKit's data loading and form handling. Try using
it with JavaScript disabled!
</p>
</div>
<style>
.content {
width: 100%;
max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto;
}
</style>

View File

@@ -0,0 +1,13 @@
import { browser, dev } from '$app/env';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement...
export const hydrate = dev;
// ...but if the client-side router is already loaded
// (i.e. we came here from elsewhere in the app), use it
export const router = browser;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod
export const prerender = true;

View File

@@ -0,0 +1,34 @@
<svelte:head>
<title>About</title>
<meta name="description" content="About this app" />
</svelte:head>
<div class="content">
<h1>About this app</h1>
<p>
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
following into your command line and following the prompts:
</p>
<pre>npm create svelte@latest</pre>
<p>
The page you're looking at is purely static HTML, with no client-side interactivity needed.
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
the devtools network panel and reloading.
</p>
<p>
The <a href="/todos">TODOs</a> page illustrates SvelteKit's data loading and form handling. Try using
it with JavaScript disabled!
</p>
</div>
<style>
.content {
width: 100%;
max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto;
}
</style>

View File

@@ -1,60 +0,0 @@
<script context="module">
export const prerender = true;
</script>
<script>
import Counter from '$lib/Counter.svelte';
</script>
<svelte:head>
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>
<section>
<h1>
<div class="welcome">
<picture>
<source srcset="svelte-welcome.webp" type="image/webp" />
<img src="svelte-welcome.png" alt="Welcome" />
</picture>
</div>
to your new<br />SvelteKit app
</h1>
<h2>
try editing <strong>src/routes/index.svelte</strong>
</h2>
<Counter />
</section>
<style>
section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
h1 {
width: 100%;
}
.welcome {
position: relative;
width: 100%;
height: 0;
padding: 0 0 calc(100% * 495 / 2048) 0;
}
.welcome img {
position: absolute;
width: 100%;
height: 100%;
top: 0;
display: block;
}
</style>

View File

@@ -0,0 +1,62 @@
import { error } from '@sveltejs/kit';
import { api } from './api';
/**
* @typedef {{
* uid: string;
* created_at: Date;
* text: string;
* done: boolean;
* pending_delete: boolean;
* }} Todo
*/
/** @type {import('./$types').PageServerLoad} */
export const load = async ({ locals }) => {
// locals.userid comes from src/hooks.js
const response = await api('GET', `todos/${locals.userid}`);
if (response.status === 404) {
// user hasn't created a todo list.
// start with an empty array
return {
/** @type {Todo[]} */
todos: []
};
}
if (response.status === 200) {
return {
/** @type {Todo[]} */
todos: await response.json()
};
}
throw error(response.status);
};
/** @type {import('./$types').Action} */
export const POST = async ({ request, locals }) => {
const form = await request.formData();
await api('POST', `todos/${locals.userid}`, {
text: form.get('text')
});
};
/** @type {import('./$types').Action} */
export const PATCH = async ({ request, locals }) => {
const form = await request.formData();
await api('PATCH', `todos/${locals.userid}/${form.get('uid')}`, {
text: form.has('text') ? form.get('text') : undefined,
done: form.has('done') ? !!form.get('done') : undefined
});
};
/** @type {import('./$types').Action} */
export const DELETE = async ({ request, locals }) => {
const form = await request.formData();
await api('DELETE', `todos/${locals.userid}/${form.get('uid')}`);
};

View File

@@ -0,0 +1,180 @@
<script>
import { enhance } from '$lib/form';
import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
/** @type {import('./$types').PageData} */
export let data;
</script>
<svelte:head>
<title>Todos</title>
<meta name="description" content="A todo list app" />
</svelte:head>
<div class="todos">
<h1>Todos</h1>
<form
class="new"
action="/todos"
method="post"
use:enhance={{
result: async ({ form }) => {
form.reset();
}
}}
>
<input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" />
</form>
{#each data.todos as todo (todo.uid)}
<div
class="todo"
class:done={todo.done}
transition:scale|local={{ start: 0.7 }}
animate:flip={{ duration: 200 }}
>
<form
action="/todos?_method=PATCH"
method="post"
use:enhance={{
pending: ({ data }) => {
todo.done = !!data.get('done');
}
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form>
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
<input type="hidden" name="uid" value={todo.uid} />
<input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" />
</form>
<form
action="/todos?_method=DELETE"
method="post"
use:enhance={{
pending: () => (todo.pending_delete = true)
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</form>
</div>
{/each}
</div>
<style>
.todos {
width: 100%;
max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto;
line-height: 1;
}
.new {
margin: 0 0 0.5rem 0;
}
input {
border: 1px solid transparent;
}
input:focus-visible {
box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ff3e00 !important;
outline: none;
}
.new input {
font-size: 28px;
width: 100%;
padding: 0.5em 1em 0.3em 1em;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
text-align: center;
}
.todo {
display: grid;
grid-template-columns: 2rem 1fr 2rem;
grid-gap: 0.5rem;
align-items: center;
margin: 0 0 0.5rem 0;
padding: 0.5rem;
background-color: white;
border-radius: 8px;
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
transform: translate(-1px, -1px);
transition: filter 0.2s, transform 0.2s;
}
.done {
transform: none;
opacity: 0.4;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.1));
}
form.text {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.todo input {
flex: 1;
padding: 0.5em 2em 0.5em 0.8em;
border-radius: 3px;
}
.todo button {
width: 2em;
height: 2em;
border: none;
background-color: transparent;
background-position: 50% 50%;
background-repeat: no-repeat;
}
button.toggle {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 50%;
box-sizing: border-box;
background-size: 1em auto;
}
.done .toggle {
background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.delete {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
opacity: 0.2;
}
.delete:hover,
.delete:focus {
transition: opacity 0.2s;
opacity: 1;
}
.save {
position: absolute;
right: 0;
opacity: 0;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A");
}
.todo input:focus + .save,
.save:focus {
transition: opacity 0.2s;
opacity: 1;
}
</style>

View File

@@ -1,9 +1,7 @@
/*
This module is used by the /todos endpoint to
make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint visiting /todos/_api
will net you a 404 response.
for each user.
(The data on the todo app will expire periodically; no
guarantees are made. Don't use it to organise your life.)

View File

@@ -1,70 +0,0 @@
import { api } from './_api';
/** @type {import('./__types').RequestHandler} */
export const get = async ({ locals }) => {
// locals.userid comes from src/hooks.js
const response = await api('get', `todos/${locals.userid}`);
if (response.status === 404) {
// user hasn't created a todo list.
// start with an empty array
return {
body: {
todos: []
}
};
}
if (response.status === 200) {
return {
body: {
todos: await response.json()
}
};
}
return {
status: response.status
};
};
/** @type {import('./index').RequestHandler} */
export const post = async ({ request, locals }) => {
const form = await request.formData();
await api('post', `todos/${locals.userid}`, {
text: form.get('text')
});
return {};
};
// If the user has JavaScript disabled, the URL will change to
// include the method override unless we redirect back to /todos
const redirect = {
status: 303,
headers: {
location: '/todos'
}
};
/** @type {import('./index').RequestHandler} */
export const patch = async ({ request, locals }) => {
const form = await request.formData();
await api('patch', `todos/${locals.userid}/${form.get('uid')}`, {
text: form.has('text') ? form.get('text') : undefined,
done: form.has('done') ? !!form.get('done') : undefined
});
return redirect;
};
/** @type {import('./index').RequestHandler} */
export const del = async ({ request, locals }) => {
const form = await request.formData();
await api('delete', `todos/${locals.userid}/${form.get('uid')}`);
return redirect;
};

View File

@@ -1,190 +0,0 @@
<script>
import { enhance } from '$lib/form';
import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
/**
* @typedef {{
* uid: string;
* created_at: Date;
* text: string;
* done: boolean;
* pending_delete: boolean;
* }} Todo
*/
/** @type {Todo[]} */
export let todos;
</script>
<svelte:head>
<title>Todos</title>
<meta name="description" content="A todo list app" />
</svelte:head>
<div class="todos">
<h1>Todos</h1>
<form
class="new"
action="/todos"
method="post"
use:enhance={{
result: async ({ form }) => {
form.reset();
}
}}
>
<input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" />
</form>
{#each todos as todo (todo.uid)}
<div
class="todo"
class:done={todo.done}
transition:scale|local={{ start: 0.7 }}
animate:flip={{ duration: 200 }}
>
<form
action="/todos?_method=PATCH"
method="post"
use:enhance={{
pending: ({ data }) => {
todo.done = !!data.get('done');
}
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form>
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
<input type="hidden" name="uid" value={todo.uid} />
<input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" />
</form>
<form
action="/todos?_method=DELETE"
method="post"
use:enhance={{
pending: () => (todo.pending_delete = true)
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</form>
</div>
{/each}
</div>
<style>
.todos {
width: 100%;
max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto;
line-height: 1;
}
.new {
margin: 0 0 0.5rem 0;
}
input {
border: 1px solid transparent;
}
input:focus-visible {
box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ff3e00 !important;
outline: none;
}
.new input {
font-size: 28px;
width: 100%;
padding: 0.5em 1em 0.3em 1em;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
text-align: center;
}
.todo {
display: grid;
grid-template-columns: 2rem 1fr 2rem;
grid-gap: 0.5rem;
align-items: center;
margin: 0 0 0.5rem 0;
padding: 0.5rem;
background-color: white;
border-radius: 8px;
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
transform: translate(-1px, -1px);
transition: filter 0.2s, transform 0.2s;
}
.done {
transform: none;
opacity: 0.4;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.1));
}
form.text {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.todo input {
flex: 1;
padding: 0.5em 2em 0.5em 0.8em;
border-radius: 3px;
}
.todo button {
width: 2em;
height: 2em;
border: none;
background-color: transparent;
background-position: 50% 50%;
background-repeat: no-repeat;
}
button.toggle {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 50%;
box-sizing: border-box;
background-size: 1em auto;
}
.done .toggle {
background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.delete {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
opacity: 0.2;
}
.delete:hover,
.delete:focus {
transition: opacity 0.2s;
opacity: 1;
}
.save {
position: absolute;
right: 0;
opacity: 0;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A");
}
.todo input:focus + .save,
.save:focus {
transition: opacity 0.2s;
opacity: 1;
}
</style>

View File

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

View File

@@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
define: {
'import.meta.env.VERCEL_ANALYTICS_ID': JSON.stringify(process.env.VERCEL_ANALYTICS_ID)
}
};
export default config;

File diff suppressed because it is too large Load Diff

View File

@@ -8,20 +8,20 @@
"packages/*"
],
"nohoist": [
"**/@types/**"
"**/@types/**",
"**/typedoc",
"**/typedoc-plugin-markdown",
"**/typedoc-plugin-mdn-links"
]
},
"dependencies": {
"lerna": "3.16.4"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.21.0",
"@typescript-eslint/parser": "5.21.0",
"@vercel/style-guide": "3.0.0",
"async-retry": "1.2.3",
"buffer-replace": "1.0.0",
"eslint": "8.14.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-jest": "26.1.5",
"husky": "7.0.4",
"jest": "28.0.2",
"json5": "2.1.1",
@@ -31,7 +31,7 @@
"prettier": "2.6.2",
"ts-eager": "2.0.2",
"ts-jest": "28.0.5",
"turbo": "1.3.2-canary.1"
"turbo": "1.4.7"
},
"scripts": {
"lerna": "lerna",
@@ -63,87 +63,5 @@
"git add"
]
},
"prettier": {
"trailingComma": "es5",
"singleQuote": true,
"arrowParens": "avoid"
},
"eslintConfig": {
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"modules": true
},
"plugins": [
"jest"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"env": {
"node": true,
"jest": true,
"es6": true
},
"rules": {
"no-restricted-syntax": [
"warn",
"WithStatement",
{
"message": "substr() is deprecated, use slice() or substring() instead",
"selector": "MemberExpression > Identifier[name='substr']"
}
],
"no-dupe-keys": 2,
"require-atomic-updates": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-unused-vars": 2,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-var-requires": 0,
"jest/no-disabled-tests": 2,
"jest/no-focused-tests": 2
},
"overrides": [
{
"files": [
"packages/cli/**/*"
],
"rules": {
"lines-between-class-members": 0,
"no-async-promise-executor": 0,
"no-control-regex": 0,
"no-empty": 0,
"prefer-const": 0,
"prefer-destructuring": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/consistent-type-assertions": 0,
"@typescript-eslint/member-delimiter-style": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-inferrable-types": 0
}
},
{
"files": [
"packages/client/**/*"
],
"rules": {
"prefer-const": 0,
"require-atomic-updates": 0,
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/no-explicit-any": 0
}
}
]
}
"prettier": "@vercel/style-guide/prettier"
}

View File

@@ -0,0 +1,20 @@
const { resolve } = require('path');
const baseConfig = require.resolve('../../.eslintrc.cjs');
const project = resolve(__dirname, 'tsconfig.json');
module.exports = {
root: true,
extends: [baseConfig],
parserOptions: {
project,
},
settings: {
'import/resolver': {
typescript: {
project,
},
},
},
};

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
const { join } = require('path');
const fs = require('fs-extra');
const execa = require('execa');
const { join } = require('path');
async function main() {
const outDir = join(__dirname, 'dist');
@@ -24,7 +24,8 @@ async function main() {
await fs.remove(mainDir);
}
main().catch(err => {
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});

View File

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

View File

@@ -0,0 +1,29 @@
import type { Env } from './types';
const { hasOwnProperty } = Object.prototype;
/**
* Clones zero or more objects into a single new object while ensuring that the
* `PATH` environment variable is defined when the `PATH` or `Path` environment
* variables are defined.
*
* @param {Object} [...envs] Objects and/or `process.env` to clone and merge
* @returns {Object} The new object
*/
export function cloneEnv(...envs: (Env | undefined)[]): Env {
return envs.reduce((obj: Env, env) => {
if (!env) {
return obj;
}
// the system path is called `Path` on Windows and Node.js will
// automatically return the system path when accessing `PATH`,
// however we lose this proxied value when we destructure and
// thus we must explicitly copy it
if (hasOwnProperty.call(env, 'PATH') || hasOwnProperty.call(env, 'Path')) {
obj.PATH = env.PATH;
}
return Object.assign(obj, env);
}, {});
}

View File

@@ -3,5 +3,7 @@ import { getPlatformEnv } from './get-platform-env';
export default function debug(message: string, ...additional: any[]) {
if (getPlatformEnv('BUILDER_DEBUG')) {
console.log(message, ...additional);
} else if (process.env.VERCEL_DEBUG_PREFIX) {
console.log(`${process.env.VERCEL_DEBUG_PREFIX}${message}`, ...additional);
}
}

View File

@@ -62,7 +62,7 @@ export function getPrettyError(obj: {
return new NowBuildError({
code: 'DEV_VALIDATE_CONFIG',
message: message,
message,
link: prop ? `${docsUrl}#project/${prop.toLowerCase()}` : docsUrl,
action: 'View Documentation',
});
@@ -91,7 +91,7 @@ function getTopLevelPropertyName(dataPath?: string): string {
return '';
}
const mapTypoToSuggestion: { [key: string]: { [key: string]: string } } = {
const mapTypoToSuggestion: Record<string, Record<string, string>> = {
'': {
builder: 'builds',
'build.env': '{ "build": { "env": {"name": "value"} } }',

View File

@@ -1,6 +1,6 @@
import assert from 'assert';
import intoStream from 'into-stream';
import { FileBase } from './types';
import type { FileBase } from './types';
interface FileBlobOptions {
mode?: number;
@@ -39,8 +39,8 @@ export default class FileBlob implements FileBase {
const chunks: Buffer[] = [];
await new Promise<void>((resolve, reject) => {
stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
stream.on('error', error => reject(error));
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
stream.on('error', (error) => reject(error));
stream.on('end', () => resolve());
});

View File

@@ -1,9 +1,9 @@
import assert from 'assert';
import path from 'path';
import fs from 'fs-extra';
import multiStream from 'multistream';
import path from 'path';
import Sema from 'async-sema';
import { FileBase } from './types';
import type { FileBase } from './types';
const semaToPreventEMFILE = new Sema(20);
@@ -84,16 +84,15 @@ class FileFsRef implements FileBase {
toStream(): NodeJS.ReadableStream {
let flag = false;
// eslint-disable-next-line consistent-return
return multiStream(cb => {
return multiStream((cb) => {
if (flag) return cb(null, null);
flag = true;
this.toStreamAsync()
.then(stream => {
.then((stream) => {
cb(null, stream);
})
.catch(error => {
.catch((error) => {
cb(error, null);
});
});

View File

@@ -3,7 +3,7 @@ import fetch from 'node-fetch';
import multiStream from 'multistream';
import retry from 'async-retry';
import Sema from 'async-sema';
import { FileBase } from './types';
import type { FileBase } from './types';
interface FileRefOptions {
mode?: number;
@@ -73,14 +73,14 @@ export default class FileRef implements FileBase {
const resp = await fetch(url);
if (!resp.ok) {
const error = new BailableError(
`download: ${resp.status} ${resp.statusText} for ${url}`
`download: ${resp.status} ${resp.statusText} for ${url}`,
);
if (resp.status === 403) error.bail = true;
throw error;
}
return resp.body;
},
{ factor: 1, retries: 3 }
{ factor: 1, retries: 3 },
);
} finally {
// console.timeEnd(`downloading ${url}`);
@@ -91,16 +91,15 @@ export default class FileRef implements FileBase {
toStream(): NodeJS.ReadableStream {
let flag = false;
// eslint-disable-next-line consistent-return
return multiStream(cb => {
return multiStream((cb) => {
if (flag) return cb(null, null);
flag = true;
this.toStreamAsync()
.then(stream => {
.then((stream) => {
cb(null, stream);
})
.catch(error => {
.catch((error) => {
cb(error, null);
});
});

View File

@@ -1,13 +1,11 @@
import path from 'path';
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
import debug from '../debug';
import FileFsRef from '../file-fs-ref';
import { File, Files, Meta } from '../types';
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
import streamToBuffer from './stream-to-buffer';
import type { File, Files, Meta } from '../types';
export interface DownloadedFiles {
[filePath: string]: FileFsRef;
}
export type DownloadedFiles = Record<string, FileFsRef>;
const S_IFMT = 61440; /* 0170000 type of file */
const S_IFLNK = 40960; /* 0120000 symbolic link */
@@ -18,7 +16,7 @@ export function isSymbolicLink(mode: number): boolean {
async function prepareSymlinkTarget(
file: File,
fsPath: string
fsPath: string,
): Promise<string> {
const mkdirPromise = mkdirp(path.dirname(fsPath));
if (file.type === 'FileFsRef') {
@@ -36,13 +34,13 @@ async function prepareSymlinkTarget(
}
throw new Error(
`file.type "${(file as any).type}" not supported for symlink`
`file.type "${(file as any).type}" not supported for symlink`,
);
}
export async function downloadFile(
file: File,
fsPath: string
fsPath: string,
): Promise<FileFsRef> {
const { mode } = file;
@@ -68,7 +66,7 @@ async function removeFile(basePath: string, fileMatched: string) {
export default async function download(
files: Files,
basePath: string,
meta?: Meta
meta?: Meta,
): Promise<DownloadedFiles> {
const {
isDev = false,
@@ -90,7 +88,7 @@ export default async function download(
const filenames = Object.keys(files);
await Promise.all(
filenames.map(async name => {
filenames.map(async (name) => {
// If the file does not exist anymore, remove it.
if (Array.isArray(filesRemoved) && filesRemoved.includes(name)) {
await removeFile(basePath, name);
@@ -112,17 +110,19 @@ export default async function download(
const parent = files[dir];
if (parent && isSymbolicLink(parent.mode)) {
console.warn(
`Warning: file "${name}" is within a symlinked directory "${dir}" and will be ignored`
`Warning: file "${name}" is within a symlinked directory "${dir}" and will be ignored`,
);
return;
}
}
const file = files[name];
if (!file) return;
const fsPath = path.join(basePath, name);
files2[name] = await downloadFile(file, fsPath);
})
}),
);
const duration = Date.now() - start;

View File

@@ -1,10 +1,11 @@
import path from 'path';
import assert from 'assert';
import vanillaGlob_ from 'glob';
import { promisify } from 'util';
import { lstat, Stats } from 'fs-extra';
import { normalizePath } from './normalize-path';
import vanillaGlob_ from 'glob';
import { lstat } from 'fs-extra';
import FileFsRef from '../file-fs-ref';
import { normalizePath } from './normalize-path';
import type { Stats } from 'fs-extra';
export type GlobOptions = vanillaGlob_.IOptions;
@@ -13,7 +14,7 @@ const vanillaGlob = promisify(vanillaGlob_);
export default async function glob(
pattern: string,
opts: GlobOptions | string,
mountpoint?: string
mountpoint?: string,
): Promise<Record<string, FileFsRef>> {
let options: GlobOptions;
if (typeof opts === 'string') {
@@ -24,7 +25,7 @@ export default async function glob(
if (!options.cwd) {
throw new Error(
'Second argument (basePath) must be specified for names of resulting files'
'Second argument (basePath) must be specified for names of resulting files',
);
}
@@ -47,9 +48,9 @@ export default async function glob(
let stat = statCache[fsPath];
assert(
stat,
`statCache does not contain value for ${relativePath} (resolved to ${fsPath})`
`statCache does not contain value for ${relativePath} (resolved to ${fsPath})`,
);
const isSymlink = options.symlinks![fsPath];
const isSymlink = options.symlinks[fsPath];
if (isSymlink || stat.isFile()) {
if (isSymlink) {
stat = await lstat(fsPath);

View File

@@ -1,7 +1,7 @@
import { intersects, validRange } from 'semver';
import { NodeVersion } from '../types';
import { NowBuildError } from '../errors';
import debug from '../debug';
import type { NodeVersion } from '../types';
const allOptions = [
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
@@ -43,14 +43,14 @@ export function getDiscontinuedNodeVersions(): NodeVersion[] {
export async function getSupportedNodeVersion(
engineRange: string | undefined,
isAuto = false
isAuto = false,
): Promise<NodeVersion> {
let selection: NodeVersion = getLatestNodeVersion();
if (engineRange) {
const found =
validRange(engineRange) &&
allOptions.some(o => {
allOptions.some((o) => {
// the array is already in order so return the first
// match which will be the newest version of node
selection = o;
@@ -61,7 +61,7 @@ export async function getSupportedNodeVersion(
code: 'BUILD_UTILS_NODE_VERSION_INVALID',
link: 'http://vercel.link/node-version',
message: `Found invalid Node.js Version: "${engineRange}". ${getHint(
isAuto
isAuto,
)}`,
});
}
@@ -84,8 +84,8 @@ export async function getSupportedNodeVersion(
`Error: Node.js version ${
selection.range
} has reached End-of-Life. Deployments created on or after ${d} will fail to build. ${getHint(
isAuto
)}`
isAuto,
)}`,
);
}

View File

@@ -1,4 +1,5 @@
import { Files } from '../types';
import type { Files } from '../types';
type Delegate = (name: string) => string;
export default function rename(files: Files, delegate: Delegate): Files {
@@ -7,6 +8,6 @@ export default function rename(files: Files, delegate: Delegate): Files {
...newFiles,
[delegate(name)]: files[name],
}),
{}
{},
);
}

View File

@@ -1,16 +1,17 @@
import assert from 'assert';
import fs from 'fs-extra';
import path from 'path';
import { deprecate } from 'util';
import fs from 'fs-extra';
import Sema from 'async-sema';
import spawn from 'cross-spawn';
import { coerce, intersects, validRange } from 'semver';
import { SpawnOptions } from 'child_process';
import { deprecate } from 'util';
import debug from '../debug';
import { NowBuildError } from '../errors';
import { Meta, PackageJson, NodeVersion, Config } from '../types';
import { cloneEnv } from '../clone-env';
import { getSupportedNodeVersion, getLatestNodeVersion } from './node-version';
import { readConfigFile } from './read-config-file';
import type { Meta, PackageJson, NodeVersion, Config } from '../types';
import type { SpawnOptions } from 'child_process';
// Only allow one `runNpmInstall()` invocation to run concurrently
const runNpmInstallSema = new Sema(1);
@@ -23,8 +24,7 @@ export interface ScanParentDirsResult {
*/
cliType: CliType;
/**
* The file path of found `package.json` file, or `undefined` if none was
* found.
* The file path of found `package.json` file, or `undefined` if not found.
*/
packageJsonPath?: string;
/**
@@ -33,8 +33,13 @@ export interface ScanParentDirsResult {
*/
packageJson?: PackageJson;
/**
* The `lockfileVersion` number from the `package-lock.json` file,
* when present.
* The file path of the lockfile (`yarn.lock`, `package-lock.json`, or `pnpm-lock.yaml`)
* or `undefined` if not found.
*/
lockfilePath?: string;
/**
* The `lockfileVersion` number from lockfile (`package-lock.json` or `pnpm-lock.yaml`),
* or `undefined` if not found.
*/
lockfileVersion?: number;
}
@@ -73,7 +78,7 @@ export interface SpawnOptionsExtended extends SpawnOptions {
export function spawnAsync(
command: string,
args: string[],
opts: SpawnOptionsExtended = {}
opts: SpawnOptionsExtended = {},
) {
return new Promise<void>((resolve, reject) => {
const stderrLogs: Buffer[] = [];
@@ -81,7 +86,7 @@ export function spawnAsync(
const child = spawn(command, args, opts);
if (opts.stdio === 'pipe' && child.stderr) {
child.stderr.on('data', data => stderrLogs.push(data));
child.stderr.on('data', (data) => stderrLogs.push(data));
}
child.on('error', reject);
@@ -99,8 +104,8 @@ export function spawnAsync(
message:
opts.stdio === 'inherit'
? `${cmd} exited with ${code || signal}`
: stderrLogs.map(line => line.toString()).join(''),
})
: stderrLogs.map((line) => line.toString()).join(''),
}),
);
});
});
@@ -109,7 +114,7 @@ export function spawnAsync(
export function execAsync(
command: string,
args: string[],
opts: SpawnOptionsExtended = {}
opts: SpawnOptionsExtended = {},
) {
return new Promise<{ stdout: string; stderr: string; code: number }>(
(resolve, reject) => {
@@ -120,11 +125,11 @@ export function execAsync(
const child = spawn(command, args, opts);
child.stderr!.on('data', data => {
child.stderr!.on('data', (data) => {
stderrList.push(data);
});
child.stdout!.on('data', data => {
child.stdout!.on('data', (data) => {
stdoutList.push(data);
});
@@ -146,10 +151,10 @@ export function execAsync(
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
}),
);
});
}
},
);
}
@@ -178,25 +183,9 @@ export async function getNodeBinPath({
}: {
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})`,
});
const { lockfilePath } = await scanParentDirs(cwd);
const dir = path.dirname(lockfilePath || cwd);
return path.join(dir, 'node_modules', '.bin');
}
async function chmodPlusX(fsPath: string) {
@@ -210,7 +199,7 @@ async function chmodPlusX(fsPath: string) {
export async function runShellScript(
fsPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions
spawnOpts?: SpawnOptions,
) {
assert(path.isAbsolute(fsPath));
const destPath = path.dirname(fsPath);
@@ -226,17 +215,17 @@ export async function runShellScript(
export function getSpawnOptions(
meta: Meta,
nodeVersion: NodeVersion
nodeVersion: NodeVersion,
): SpawnOptions {
const opts = {
env: { ...process.env },
env: cloneEnv(process.env),
};
if (!meta.isDev) {
let found = false;
const oldPath = opts.env.PATH || process.env.PATH || '';
const pathSegments = oldPath.split(path.delimiter).map(segment => {
const pathSegments = oldPath.split(path.delimiter).map((segment) => {
if (/^\/node[0-9]+\/bin/.test(segment)) {
found = true;
return `/node${nodeVersion.major}/bin`;
@@ -259,7 +248,7 @@ export async function getNodeVersion(
destPath: string,
_nodeVersion?: string,
config: Config = {},
meta: Meta = {}
meta: Meta = {},
): Promise<NodeVersion> {
const latest = getLatestNodeVersion();
if (meta && meta.isDev) {
@@ -278,11 +267,11 @@ export async function getNodeVersion(
!meta.isDev
) {
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`
`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) &&
@@ -290,7 +279,7 @@ export async function getNodeVersion(
!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`
`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;
@@ -301,7 +290,7 @@ export async function getNodeVersion(
export async function scanParentDirs(
destPath: string,
readPackageJson = false
readPackageJson = false,
): Promise<ScanParentDirsResult> {
assert(path.isAbsolute(destPath));
@@ -319,6 +308,7 @@ export async function scanParentDirs(
start: destPath,
filenames: ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'],
});
let lockfilePath: string | undefined;
let lockfileVersion: number | undefined;
let cliType: CliType = 'yarn';
@@ -335,17 +325,25 @@ export async function scanParentDirs(
// Priority order is Yarn > pnpm > npm
if (hasYarnLock) {
cliType = 'yarn';
lockfilePath = yarnLockPath;
} else if (pnpmLockYaml) {
cliType = 'pnpm';
// just ensure that it is read as a number and not a string
lockfilePath = pnpmLockPath;
lockfileVersion = Number(pnpmLockYaml.lockfileVersion);
} else if (packageLockJson) {
cliType = 'npm';
lockfilePath = npmLockPath;
lockfileVersion = packageLockJson.lockfileVersion;
}
const packageJsonPath = pkgJsonPath || undefined;
return { cliType, packageJson, lockfileVersion, packageJsonPath };
return {
cliType,
packageJson,
lockfilePath,
lockfileVersion,
packageJsonPath,
};
}
export async function walkParentDirs({
@@ -387,11 +385,11 @@ async function walkParentDirsMulti({
}): Promise<(string | undefined)[]> {
let parent = '';
for (let current = start; base.length <= current.length; current = parent) {
const fullPaths = filenames.map(f => path.join(current, f));
const fullPaths = filenames.map((f) => path.join(current, f));
const existResults = await Promise.all(
fullPaths.map(f => fs.pathExists(f))
fullPaths.map((f) => fs.pathExists(f)),
);
const foundOneOrMore = existResults.some(b => b);
const foundOneOrMore = existResults.some((b) => b);
if (foundOneOrMore) {
return fullPaths.map((f, i) => (existResults[i] ? f : undefined));
@@ -417,7 +415,7 @@ export async function runNpmInstall(
args: string[] = [],
spawnOpts?: SpawnOptions,
meta?: Meta,
nodeVersion?: NodeVersion
nodeVersion?: NodeVersion,
): Promise<boolean> {
if (meta?.isDev) {
debug('Skipping dependency installation because dev mode is enabled');
@@ -429,7 +427,7 @@ export async function runNpmInstall(
try {
await runNpmInstallSema.acquire();
const { cliType, packageJsonPath, lockfileVersion } = await scanParentDirs(
destPath
destPath,
);
// Only allow `runNpmInstall()` to run once per `package.json`
@@ -441,9 +439,8 @@ export async function runNpmInstall(
if (isSet<string>(meta.runNpmInstallSet)) {
if (meta.runNpmInstallSet.has(packageJsonPath)) {
return false;
} else {
meta.runNpmInstallSet.add(packageJsonPath);
}
meta.runNpmInstallSet.add(packageJsonPath);
}
}
@@ -452,7 +449,7 @@ export async function runNpmInstall(
debug(`Installing to ${destPath}`);
const opts: SpawnOptionsExtended = { cwd: destPath, ...spawnOpts };
const env = opts.env ? { ...opts.env } : { ...process.env };
const env = cloneEnv(opts.env || process.env);
delete env.NODE_ENV;
opts.env = getEnvForPackageManager({
cliType,
@@ -465,14 +462,27 @@ export async function runNpmInstall(
if (cliType === 'npm') {
opts.prettyCommand = 'npm install';
commandArgs = args
.filter(a => a !== '--prefer-offline')
.filter((a) => a !== '--prefer-offline')
.concat(['install', '--no-audit', '--unsafe-perm']);
if (
nodeVersion?.major === 16 &&
spawnOpts?.env?.VERCEL_NPM_LEGACY_PEER_DEPS === '1' &&
spawnOpts.env.ENABLE_EXPERIMENTAL_COREPACK !== '1'
) {
// Starting in npm@8.6.0, if you ran `npm install --legacy-peer-deps`,
// and then later ran `npm install`, it would fail. So the only way
// to safely upgrade npm from npm@8.5.0 is to set this flag. The docs
// say this flag is not recommended so its is behind a feature flag
// so we can remove it in node@18, which can introduce breaking changes.
// See https://docs.npmjs.com/cli/v8/using-npm/config#legacy-peer-deps
commandArgs.push('--legacy-peer-deps');
}
} else if (cliType === 'pnpm') {
// PNPM's install command is similar to NPM's but without the audit nonsense
// @see options https://pnpm.io/cli/install
opts.prettyCommand = 'pnpm install';
commandArgs = args
.filter(a => a !== '--prefer-offline')
.filter((a) => a !== '--prefer-offline')
.concat(['install', '--unsafe-perm']);
} else {
opts.prettyCommand = 'yarn install';
@@ -500,10 +510,10 @@ export function getEnvForPackageManager({
cliType: CliType;
lockfileVersion: number | undefined;
nodeVersion: NodeVersion | undefined;
env: { [x: string]: string | undefined };
env: Record<string, string | undefined>;
}) {
const newEnv: { [x: string]: string | undefined } = { ...env };
const oldPath = env.PATH + '';
const newEnv: Record<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';
@@ -559,7 +569,7 @@ export async function runCustomInstallCommand({
nodeVersion,
env: spawnOpts?.env || {},
});
debug(`Running with $PATH:`, env?.PATH || '');
debug(`Running with $PATH:`, env.PATH || '');
await execCommand(installCommand, {
...spawnOpts,
env,
@@ -570,17 +580,17 @@ export async function runCustomInstallCommand({
export async function runPackageJsonScript(
destPath: string,
scriptNames: string | Iterable<string>,
spawnOpts?: SpawnOptions
spawnOpts?: SpawnOptions,
) {
assert(path.isAbsolute(destPath));
const { packageJson, cliType, lockfileVersion } = await scanParentDirs(
destPath,
true
true,
);
const scriptName = getScriptName(
packageJson,
typeof scriptNames === 'string' ? [scriptNames] : scriptNames
typeof scriptNames === 'string' ? [scriptNames] : scriptNames,
);
if (!scriptName) return false;
@@ -594,10 +604,7 @@ export async function runPackageJsonScript(
cliType,
lockfileVersion,
nodeVersion: undefined,
env: {
...process.env,
...spawnOpts?.env,
},
env: cloneEnv(process.env, spawnOpts?.env),
}),
};
@@ -620,7 +627,7 @@ export async function runBundleInstall(
destPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions,
meta?: Meta
meta?: Meta,
) {
if (meta && meta.isDev) {
debug('Skipping dependency installation because dev mode is enabled');
@@ -637,7 +644,7 @@ export async function runPipInstall(
destPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions,
meta?: Meta
meta?: Meta,
) {
if (meta && meta.isDev) {
debug('Skipping dependency installation because dev mode is enabled');
@@ -650,13 +657,13 @@ export async function runPipInstall(
await spawnAsync(
'pip3',
['install', '--disable-pip-version-check', ...args],
opts
opts,
);
}
export function getScriptName(
pkg: Pick<PackageJson, 'scripts'> | null | undefined,
possibleNames: Iterable<string>
possibleNames: Iterable<string>,
): string | null {
if (pkg?.scripts) {
for (const name of possibleNames) {
@@ -674,5 +681,5 @@ export function getScriptName(
*/
export const installDependencies = deprecate(
runNpmInstall,
'installDependencies() is deprecated. Please use runNpmInstall() instead.'
'installDependencies() is deprecated. Please use runNpmInstall() instead.',
);

View File

@@ -1,14 +1,14 @@
import eos from 'end-of-stream';
export default function streamToBuffer(
stream: NodeJS.ReadableStream
stream: NodeJS.ReadableStream,
): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
stream.on('data', buffers.push.bind(buffers));
eos(stream, err => {
eos(stream, (err) => {
if (err) {
reject(err);
return;
@@ -18,7 +18,7 @@ export default function streamToBuffer(
resolve(Buffer.allocUnsafe(0));
break;
case 1:
resolve(buffers[0]);
resolve(buffers[0]!);
break;
default:
resolve(Buffer.concat(buffers));

View File

@@ -1,4 +1,4 @@
type Envs = { [key: string]: string | undefined };
type Envs = Record<string, string | undefined>;
/**
* Get the framework-specific prefixed System Environment Variables.
@@ -17,8 +17,8 @@ export function getPrefixedEnvVars({
const newEnvs: Envs = {};
if (envPrefix && envs.VERCEL_URL) {
Object.keys(envs)
.filter(key => key.startsWith(vercelSystemEnvPrefix))
.forEach(key => {
.filter((key) => key.startsWith(vercelSystemEnvPrefix))
.forEach((key) => {
const newKey = `${envPrefix}${key}`;
if (!(newKey in envs)) {
newEnvs[newKey] = envs[key];

View File

@@ -4,13 +4,9 @@ import FileRef from './file-ref';
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
import { NodejsLambda } from './nodejs-lambda';
import { Prerender } from './prerender';
import download, {
downloadFile,
DownloadedFiles,
isSymbolicLink,
} from './fs/download';
import download, { downloadFile, isSymbolicLink } from './fs/download';
import getWriteableDirectory from './fs/get-writable-directory';
import glob, { GlobOptions } from './fs/glob';
import glob from './fs/glob';
import rename from './fs/rename';
import {
execAsync,
@@ -41,6 +37,9 @@ import debug from './debug';
import getIgnoreFilter from './get-ignore-filter';
import { getPlatformEnv } from './get-platform-env';
import { getPrefixedEnvVars } from './get-prefixed-env-vars';
import { cloneEnv } from './clone-env';
import type { GlobOptions } from './fs/glob';
import type { DownloadedFiles } from './fs/download';
export {
FileBlob,
@@ -52,10 +51,8 @@ export {
Prerender,
download,
downloadFile,
DownloadedFiles,
getWriteableDirectory,
glob,
GlobOptions,
rename,
execAsync,
spawnAsync,
@@ -84,8 +81,11 @@ export {
getLambdaOptionsFromFunction,
scanParentDirs,
getIgnoreFilter,
cloneEnv,
};
export type { DownloadedFiles, GlobOptions };
export { EdgeFunction } from './edge-function';
export { readConfigFile } from './fs/read-config-file';
export { normalizePath } from './fs/normalize-path';

View File

@@ -7,9 +7,7 @@ import { isSymbolicLink } from './fs/download';
import streamToBuffer from './fs/stream-to-buffer';
import type { Files, Config } from './types';
interface Environment {
[key: string]: string;
}
type Environment = Record<string, string>;
export type LambdaOptions = LambdaOptionsWithFiles | LambdaOptionsWithZipBuffer;
@@ -94,30 +92,30 @@ export class Lambda {
if (allowQuery !== undefined) {
assert(Array.isArray(allowQuery), '"allowQuery" is not an Array');
assert(
allowQuery.every(q => typeof q === 'string'),
'"allowQuery" is not a string Array'
allowQuery.every((q) => typeof q === 'string'),
'"allowQuery" is not a string Array',
);
}
if (supportsMultiPayloads !== undefined) {
assert(
typeof supportsMultiPayloads === 'boolean',
'"supportsMultiPayloads" is not a boolean'
'"supportsMultiPayloads" is not a boolean',
);
}
if (supportsWrapper !== undefined) {
assert(
typeof supportsWrapper === 'boolean',
'"supportsWrapper" is not a boolean'
'"supportsWrapper" is not a boolean',
);
}
if (regions !== undefined) {
assert(Array.isArray(regions), '"regions" is not an Array');
assert(
regions.every(r => typeof r === 'string'),
'"regions" is not a string Array'
regions.every((r) => typeof r === 'string'),
'"regions" is not a string Array',
);
}
this.type = 'Lambda';
@@ -172,6 +170,7 @@ export async function createZip(files: Files): Promise<Buffer> {
const symlinkTargets = new Map<string, string>();
for (const name of names) {
const file = files[name];
if (!file) continue;
if (file.mode && isSymbolicLink(file.mode) && file.type === 'FileFsRef') {
const symlinkTarget = await readlink(file.fsPath);
symlinkTargets.set(name, symlinkTarget);
@@ -182,6 +181,7 @@ export async function createZip(files: Files): Promise<Buffer> {
const zipBuffer = await new Promise<Buffer>((resolve, reject) => {
for (const name of names) {
const file = files[name];
if (!file) continue;
const opts = { mode: file.mode, mtime };
const symlinkTarget = symlinkTargets.get(name);
if (typeof symlinkTarget === 'string') {

View File

@@ -1,4 +1,5 @@
import { Lambda, LambdaOptionsWithFiles } from './lambda';
import { Lambda } from './lambda';
import type { LambdaOptionsWithFiles } from './lambda';
interface NodejsLambdaOptions extends LambdaOptionsWithFiles {
shouldAddHelpers: boolean;

View File

@@ -1,5 +1,5 @@
import { File } from './types';
import { Lambda } from './lambda';
import type { File } from './types';
import type { Lambda } from './lambda';
interface PrerenderOptions {
expiration: number | false;
@@ -36,30 +36,30 @@ export class Prerender {
(group <= 0 || !Number.isInteger(group))
) {
throw new Error(
'The `group` argument for `Prerender` needs to be a natural number.'
'The `group` argument for `Prerender` needs to be a natural number.',
);
}
this.group = group;
if (bypassToken == null) {
if (typeof bypassToken === 'undefined' || bypassToken === null) {
this.bypassToken = null;
} else if (typeof bypassToken === 'string') {
if (bypassToken.length < 32) {
// Enforce 128 bits of entropy for safety reasons (UUIDv4 size)
throw new Error(
'The `bypassToken` argument for `Prerender` must be 32 characters or more.'
'The `bypassToken` argument for `Prerender` must be 32 characters or more.',
);
}
this.bypassToken = bypassToken;
} else {
throw new Error(
'The `bypassToken` argument for `Prerender` must be a `string`.'
'The `bypassToken` argument for `Prerender` must be a `string`.',
);
}
if (typeof fallback === 'undefined') {
throw new Error(
'The `fallback` argument for `Prerender` needs to be a `FileBlob`, `FileFsRef`, `FileRef`, or null.'
'The `fallback` argument for `Prerender` needs to be a `FileBlob`, `FileFsRef`, `FileRef`, or null.',
);
}
this.fallback = fallback;
@@ -67,12 +67,12 @@ export class Prerender {
if (allowQuery !== undefined) {
if (!Array.isArray(allowQuery)) {
throw new Error(
'The `allowQuery` argument for `Prerender` must be Array.'
'The `allowQuery` argument for `Prerender` must be Array.',
);
}
if (!allowQuery.every(q => typeof q === 'string')) {
if (!allowQuery.every((q) => typeof q === 'string')) {
throw new Error(
'The `allowQuery` argument for `Prerender` must be Array of strings.'
'The `allowQuery` argument for `Prerender` must be Array of strings.',
);
}
this.allowQuery = allowQuery;

View File

@@ -1,4 +1,5 @@
import { parse } from 'path';
import { normalizePath } from './fs/normalize-path';
import type FileFsRef from './file-fs-ref';
import type { ShouldServe } from './types';
@@ -7,21 +8,28 @@ export const shouldServe: ShouldServe = ({
files,
requestPath,
}) => {
requestPath = requestPath.replace(/\/$/, ''); // sanitize trailing '/'
entrypoint = entrypoint.replace(/\\/, '/'); // windows compatibility
const normalizedRequestPath = requestPath.replace(/\/$/, ''); // sanitize trailing '/'
const normalizedEntrypoint = normalizePath(entrypoint);
if (entrypoint === requestPath && hasProp(files, entrypoint)) {
if (
normalizedEntrypoint === normalizedRequestPath &&
hasProp(files, normalizedEntrypoint)
) {
return true;
}
const { dir, name } = parse(entrypoint);
if (name === 'index' && dir === requestPath && hasProp(files, entrypoint)) {
const { dir, name } = parse(normalizedEntrypoint);
if (
name === 'index' &&
dir === normalizedRequestPath &&
hasProp(files, normalizedEntrypoint)
) {
return true;
}
return false;
};
function hasProp(obj: { [path: string]: FileFsRef }, key: string): boolean {
function hasProp(obj: Record<string, FileFsRef>, key: string): boolean {
return Object.hasOwnProperty.call(obj, key);
}

View File

@@ -5,9 +5,7 @@ import type { Lambda } from './lambda';
import type { Prerender } from './prerender';
import type { EdgeFunction } from './edge-function';
export interface Env {
[name: string]: string | undefined;
}
export type Env = Record<string, string | undefined>;
export type File = FileRef | FileFsRef | FileBlob;
export interface FileBase {
@@ -18,9 +16,7 @@ export interface FileBase {
toStreamAsync?: () => Promise<NodeJS.ReadableStream>;
}
export interface Files {
[filePath: string]: File;
}
export type Files = Record<string, File>;
export interface Config {
maxLambdaSize?: string;
@@ -32,7 +28,7 @@ export interface Config {
rust?: string;
debug?: boolean;
zeroConfig?: boolean;
import?: { [key: string]: string };
import?: Record<string, string>;
functions?: BuilderFunctions;
projectSettings?: ProjectSettings;
outputDirectory?: string;
@@ -41,6 +37,7 @@ export interface Config {
devCommand?: string;
framework?: string | null;
nodeVersion?: string;
middleware?: boolean;
[key: string]: unknown;
}
@@ -149,9 +146,7 @@ export interface ShouldServeOptions {
/**
* All source files of the project
*/
files: {
[path: string]: FileFsRef;
};
files: Record<string, FileFsRef>;
/**
* A writable temporary directory where you are encouraged to perform your
@@ -209,9 +204,7 @@ export namespace PackageJson {
/**
* A map of exposed bin commands
*/
export interface BinMap {
[commandName: string]: string;
}
export type BinMap = Record<string, string>;
/**
* A bugs link
@@ -229,9 +222,7 @@ export namespace PackageJson {
/**
* A map of dependencies
*/
export interface DependencyMap {
[dependencyName: string]: string;
}
export type DependencyMap = Record<string, string>;
/**
* CommonJS package structure
@@ -261,9 +252,7 @@ export namespace PackageJson {
url: string;
}
export interface ScriptsMap {
[scriptName: string]: string;
}
export type ScriptsMap = Record<string, string>;
}
export interface PackageJson {
@@ -311,15 +300,16 @@ export interface Builder {
config?: Config;
}
export interface BuilderFunctions {
[key: string]: {
export type BuilderFunctions = Record<
string,
{
memory?: number;
maxDuration?: number;
runtime?: string;
includeFiles?: string;
excludeFiles?: string;
};
}
}
>;
export interface ProjectSettings {
framework?: string | null;
@@ -341,6 +331,7 @@ export interface BuilderV2 {
version: 2;
build: BuildV2;
prepareCache?: PrepareCache;
shouldServe?: ShouldServe;
}
export interface BuilderV3 {
@@ -353,7 +344,7 @@ export interface BuilderV3 {
type ImageFormat = 'image/avif' | 'image/webp';
export type RemotePattern = {
export interface RemotePattern {
/**
* Must be `http` or `https`.
*/
@@ -378,7 +369,7 @@ export type RemotePattern = {
* Double `**` matches any number of path segments.
*/
pathname?: string;
};
}
export interface Images {
domains: string[];
@@ -416,13 +407,11 @@ export interface BuildResultV2Typical {
// TODO: use proper `Route` type from `routing-utils` (perhaps move types to a common package)
routes?: any[];
images?: Images;
output: {
[key: string]: File | Lambda | Prerender | EdgeFunction;
};
wildcard?: Array<{
output: Record<string, File | Lambda | Prerender | EdgeFunction>;
wildcard?: {
domain: string;
value: string;
}>;
}[];
}
export type BuildResultV2 = BuildResultV2Typical | BuildResultBuildOutput;
@@ -437,8 +426,8 @@ export type BuildV2 = (options: BuildOptions) => Promise<BuildResultV2>;
export type BuildV3 = (options: BuildOptions) => Promise<BuildResultV3>;
export type PrepareCache = (options: PrepareCacheOptions) => Promise<Files>;
export type ShouldServe = (
options: ShouldServeOptions
options: ShouldServeOptions,
) => boolean | Promise<boolean>;
export type StartDevServer = (
options: StartDevServerOptions
options: StartDevServerOptions,
) => Promise<StartDevServerResult>;

View File

@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "20-npm-7",
"version": "1.0.0"
}
}

View File

@@ -2,7 +2,7 @@ import path from 'path';
import fs from 'fs-extra';
import {
testDeployment,
// @ts-ignore
// @ts-expect-error
} from '../../../test/lib/deployment/test-deployment';
jest.setTimeout(4 * 60 * 1000);
@@ -21,18 +21,18 @@ const skipFixtures: string[] = [
'08-zero-config-middleman',
'21-npm-workspaces',
'23-pnpm-workspaces',
'41-nx-monorepo',
'42-npm-workspace-with-nx',
];
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (skipFixtures.includes(fixture)) {
continue; // eslint-disable-line no-continue
continue;
}
// eslint-disable-next-line no-loop-func
it(`Should build "${fixture}"`, async () => {
await expect(
testDeployment(path.join(fixturesPath, fixture))
testDeployment(path.join(fixturesPath, fixture)),
).resolves.toBeDefined();
});
}
@@ -41,21 +41,18 @@ for (const fixture of fs.readdirSync(fixturesPath)) {
const buildersToTestWith = ['node'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
const fixturesPath2 = path.resolve(
__dirname,
`../../${builder}/test/fixtures`
`../../${builder}/test/fixtures`,
);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath2)) {
// don't run all foreign fixtures, just some
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
// eslint-disable-next-line no-loop-func
it(`Should build "${builder}/${fixture}"`, async () => {
await expect(
testDeployment(path.join(fixturesPath2, fixture))
testDeployment(path.join(fixturesPath2, fixture)),
).resolves.toBeDefined();
});
}

View File

@@ -1,4 +0,0 @@
{
"extends": "../tsconfig.json",
"include": ["*.test.ts"]
}

View File

@@ -0,0 +1,120 @@
import { cloneEnv } from '../src';
it('should clone env with Path', () => {
expect(
cloneEnv(
new Proxy(
{
foo: 'bar',
Path: 'baz',
},
{
get(target: typeof process.env, prop: string) {
if (prop === 'PATH') {
return target.PATH ?? target.Path;
}
return target[prop];
},
}
)
)
).toEqual({
foo: 'bar',
Path: 'baz',
PATH: 'baz',
});
});
it('should clone env with PATH', () => {
expect(
cloneEnv({
foo: 'bar',
PATH: 'baz',
})
).toEqual({
foo: 'bar',
PATH: 'baz',
});
});
it('should clone and merge multiple env objects', () => {
// note: this also tests the last object doesn't overwrite `PATH` with
// `undefined`
expect(
cloneEnv(
{
foo: 'bar',
},
{
PATH: 'baz',
},
{
baz: 'wiz',
}
)
).toEqual({
foo: 'bar',
PATH: 'baz',
baz: 'wiz',
});
});
it('should clone the actual process.env object', () => {
expect(cloneEnv(process.env).PATH).toEqual(process.env.PATH);
});
it('should overwrite PATH with last value', () => {
expect(
cloneEnv(
new Proxy(
{
Path: 'foo',
},
{
get(target: typeof process.env, prop: string) {
if (prop === 'PATH') {
return target.PATH ?? target.Path;
}
return target[prop];
},
}
),
{
PATH: 'bar',
},
{
PATH: undefined,
}
)
).toEqual({
Path: 'foo',
PATH: undefined,
});
});
it('should handle process.env at any argument position', () => {
expect(
cloneEnv(
{
foo: 'bar',
},
new Proxy(
{
Path: 'baz',
},
{
get(target: typeof process.env, prop: string) {
if (prop === 'PATH') {
return target.PATH ?? target.Path;
}
return target[prop];
},
}
)
)
).toEqual({
foo: 'bar',
Path: 'baz',
PATH: 'baz',
});
});

View File

@@ -3,11 +3,11 @@ import { delimiter } from 'path';
import { getEnvForPackageManager } from '../src';
describe('Test `getEnvForPackageManager()`', () => {
const cases: Array<{
const cases: {
name: string;
args: Parameters<typeof getEnvForPackageManager>[0];
want: unknown;
}> = [
}[] = [
{
name: 'should do nothing to env for npm < 6 and node < 16',
args: {
@@ -190,7 +190,7 @@ describe('Test `getEnvForPackageManager()`', () => {
nodeVersion: args.nodeVersion,
env: args.env,
}),
want
want,
);
});
}

View File

@@ -0,0 +1,46 @@
import { join, parse } from 'path';
import { getNodeBinPath } from '../src';
describe('Test `getNodeBinPath()`', () => {
it('should work with npm7', async () => {
const cwd = join(__dirname, 'fixtures', '20-npm-7');
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, 'node_modules', '.bin'));
});
it('should work with yarn', async () => {
const cwd = join(__dirname, 'fixtures', '19-yarn-v2');
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, 'node_modules', '.bin'));
});
it('should work with npm 6', async () => {
const cwd = join(__dirname, 'fixtures', '08-yarn-npm/with-npm');
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, 'node_modules', '.bin'));
});
it('should work with npm workspaces', async () => {
const cwd = join(__dirname, 'fixtures', '21-npm-workspaces/a');
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, '..', 'node_modules', '.bin'));
});
it('should work with pnpm', async () => {
const cwd = join(__dirname, 'fixtures', '22-pnpm');
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, 'node_modules', '.bin'));
});
it('should work with pnpm workspaces', async () => {
const cwd = join(__dirname, 'fixtures', '23-pnpm-workspaces/c');
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, '..', 'node_modules', '.bin'));
});
it('should fallback to cwd if no lockfile found', async () => {
const cwd = parse(process.cwd()).root;
const result = await getNodeBinPath({ cwd });
expect(result).toBe(join(cwd, 'node_modules', '.bin'));
});
});

View File

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

View File

@@ -1,11 +1,11 @@
import { getPrefixedEnvVars } from '../src';
describe('Test `getPrefixedEnvVars()`', () => {
const cases: Array<{
const cases: {
name: string;
args: Parameters<typeof getPrefixedEnvVars>[0];
want: ReturnType<typeof getPrefixedEnvVars>;
}> = [
}[] = [
{
name: 'should work with NEXT_PUBLIC_',
args: {

View File

@@ -12,12 +12,12 @@ describe('Test `getSpawnOptions()`', () => {
process.env.PATH = origProcessEnvPath;
});
const cases: Array<{
const cases: {
name: string;
args: Parameters<typeof getSpawnOptions>;
envPath: string | undefined;
want: string | undefined;
}> = [
}[] = [
{
name: 'should do nothing when isDev and node14',
args: [

View File

@@ -0,0 +1,172 @@
import path from 'path';
import { runNpmInstall, cloneEnv } from '../src';
import type { Meta } from '../src/types';
const spawnMock = jest.fn();
jest.mock('cross-spawn', () => {
const spawn = (...args: any) => {
spawnMock(...args);
const child = {
on: (type: string, fn: (code: number) => void) => {
if (type === 'close') {
return fn(0);
}
},
};
return child;
};
return spawn;
});
afterEach(() => {
spawnMock.mockClear();
});
function getTestSpawnOpts(env: Record<string, string>) {
return { env: cloneEnv(process.env, env) };
}
function getNodeVersion(major: number) {
return { major, range: `${major}.x`, runtime: `nodejs${major}.x` };
}
it('should not include peer dependencies when missing VERCEL_NPM_LEGACY_PEER_DEPS on node16', async () => {
const fixture = path.join(__dirname, 'fixtures', '20-npm-7');
const meta: Meta = {};
const spawnOpts = getTestSpawnOpts({});
const nodeVersion = { major: 16 } as any;
await runNpmInstall(fixture, [], spawnOpts, meta, nodeVersion);
expect(spawnMock.mock.calls.length).toBe(1);
const args = spawnMock.mock.calls[0];
expect(args[0]).toEqual('npm');
expect(args[1]).toEqual(['install', '--no-audit', '--unsafe-perm']);
expect(args[2]).toEqual({
cwd: fixture,
prettyCommand: 'npm install',
stdio: 'inherit',
env: expect.any(Object),
});
});
it('should include peer dependencies when VERCEL_NPM_LEGACY_PEER_DEPS=1 on node16', async () => {
const fixture = path.join(__dirname, 'fixtures', '20-npm-7');
const meta: Meta = {};
const spawnOpts = getTestSpawnOpts({ VERCEL_NPM_LEGACY_PEER_DEPS: '1' });
const nodeVersion = getNodeVersion(16);
await runNpmInstall(fixture, [], spawnOpts, meta, nodeVersion);
expect(spawnMock.mock.calls.length).toBe(1);
const args = spawnMock.mock.calls[0];
expect(args[0]).toEqual('npm');
expect(args[1]).toEqual([
'install',
'--no-audit',
'--unsafe-perm',
'--legacy-peer-deps',
]);
expect(args[2]).toEqual({
cwd: fixture,
prettyCommand: 'npm install',
stdio: 'inherit',
env: expect.any(Object),
});
});
it('should not include peer dependencies when VERCEL_NPM_LEGACY_PEER_DEPS=1 on node14', async () => {
const fixture = path.join(__dirname, 'fixtures', '20-npm-7');
const meta: Meta = {};
const spawnOpts = getTestSpawnOpts({ VERCEL_NPM_LEGACY_PEER_DEPS: '1' });
const nodeVersion = getNodeVersion(14);
await runNpmInstall(fixture, [], spawnOpts, meta, nodeVersion);
expect(spawnMock.mock.calls.length).toBe(1);
const args = spawnMock.mock.calls[0];
expect(args[0]).toEqual('npm');
expect(args[1]).toEqual(['install', '--no-audit', '--unsafe-perm']);
expect(args[2]).toEqual({
cwd: fixture,
prettyCommand: 'npm install',
stdio: 'inherit',
env: expect.any(Object),
});
});
it('should not include peer dependencies when VERCEL_NPM_LEGACY_PEER_DEPS=1 on node16 with corepack enabled', async () => {
const fixture = path.join(__dirname, 'fixtures', '20-npm-7');
const meta: Meta = {};
const spawnOpts = getTestSpawnOpts({
VERCEL_NPM_LEGACY_PEER_DEPS: '1',
ENABLE_EXPERIMENTAL_COREPACK: '1',
});
const nodeVersion = getNodeVersion(16);
await runNpmInstall(fixture, [], spawnOpts, meta, nodeVersion);
expect(spawnMock.mock.calls.length).toBe(1);
const args = spawnMock.mock.calls[0];
expect(args[0]).toEqual('npm');
expect(args[1]).toEqual(['install', '--no-audit', '--unsafe-perm']);
expect(args[2]).toEqual({
cwd: fixture,
prettyCommand: 'npm install',
stdio: 'inherit',
env: expect.any(Object),
});
});
it('should only invoke `runNpmInstall()` once per `package.json` file (serial)', async () => {
const meta: Meta = {};
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const apiDir = path.join(fixture, 'api');
const run1 = await runNpmInstall(apiDir, [], undefined, meta);
expect(run1).toEqual(true);
expect(
(meta.runNpmInstallSet as Set<string>).has(
path.join(fixture, 'package.json'),
),
).toEqual(true);
const run2 = await runNpmInstall(apiDir, [], undefined, meta);
expect(run2).toEqual(false);
const run3 = await runNpmInstall(fixture, [], undefined, meta);
expect(run3).toEqual(false);
expect(spawnMock.mock.calls.length).toBe(1);
const args = spawnMock.mock.calls[0];
expect(args[0]).toEqual('yarn');
expect(args[1]).toEqual(['install']);
expect(args[2]).toEqual({
cwd: apiDir,
prettyCommand: 'yarn install',
stdio: 'inherit',
env: expect.any(Object),
});
});
it('should only invoke `runNpmInstall()` once per `package.json` file (parallel)', async () => {
const meta: Meta = {};
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const apiDir = path.join(fixture, 'api');
const [run1, run2, run3] = await Promise.all([
runNpmInstall(apiDir, [], undefined, meta),
runNpmInstall(apiDir, [], undefined, meta),
runNpmInstall(fixture, [], undefined, meta),
]);
expect(run1).toEqual(true);
expect(run2).toEqual(false);
expect(run3).toEqual(false);
expect(
(meta.runNpmInstallSet as Set<string>).has(
path.join(fixture, 'package.json'),
),
).toEqual(true);
expect(spawnMock.mock.calls.length).toBe(1);
const args = spawnMock.mock.calls[0];
expect(args[0]).toEqual('yarn');
expect(args[1]).toEqual(['install']);
expect(args[2]).toEqual({
cwd: apiDir,
prettyCommand: 'yarn install',
stdio: 'inherit',
env: expect.any(Object),
});
});

View File

@@ -1,8 +1,7 @@
import ms from 'ms';
import path from 'path';
import fs, { readlink } from 'fs-extra';
import retry from 'async-retry';
import { strict as assert, strictEqual } from 'assert';
import ms from 'ms';
import fs, { readlink } from 'fs-extra';
import { createZip } from '../src/lambda';
import { getSupportedNodeVersion } from '../src/fs/node-version';
import download from '../src/fs/download';
@@ -16,7 +15,6 @@ import {
runPackageJsonScript,
scanParentDirs,
FileBlob,
Meta,
} from '../src';
jest.setTimeout(10 * 1000);
@@ -31,11 +29,11 @@ async function expectBuilderError(promise: Promise<any>, pattern: string) {
assert('message' in result, `Expected error message but found ${result}`);
assert(
typeof result.message === 'string',
`Expected error to be a string but found ${typeof result.message}`
`Expected error to be a string but found ${typeof result.message}`,
);
assert(
result.message.includes(pattern),
`Expected ${pattern} but found "${result.message}"`
`Expected ${pattern} but found "${result.message}"`,
);
}
@@ -43,7 +41,7 @@ let warningMessages: string[];
const originalConsoleWarn = console.warn;
beforeEach(() => {
warningMessages = [];
console.warn = m => {
console.warn = (m) => {
warningMessages.push(m);
};
});
@@ -220,22 +218,22 @@ it('should download symlinks even with incorrect file', async () => {
it('should only match supported node versions, otherwise throw an error', async () => {
expect(await getSupportedNodeVersion('12.x', false)).toHaveProperty(
'major',
12
12,
);
expect(await getSupportedNodeVersion('14.x', false)).toHaveProperty(
'major',
14
14,
);
expect(await getSupportedNodeVersion('16.x', false)).toHaveProperty(
'major',
16
16,
);
const autoMessage =
'Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.';
await expectBuilderError(
getSupportedNodeVersion('8.11.x', true),
autoMessage
autoMessage,
);
await expectBuilderError(getSupportedNodeVersion('6.x', true), autoMessage);
await expectBuilderError(getSupportedNodeVersion('999.x', true), autoMessage);
@@ -244,32 +242,32 @@ it('should only match supported node versions, otherwise throw an error', async
expect(await getSupportedNodeVersion('12.x', true)).toHaveProperty(
'major',
12
12,
);
expect(await getSupportedNodeVersion('14.x', true)).toHaveProperty(
'major',
14
14,
);
expect(await getSupportedNodeVersion('16.x', true)).toHaveProperty(
'major',
16
16,
);
const foundMessage =
'Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.';
await expectBuilderError(
getSupportedNodeVersion('8.11.x', false),
foundMessage
foundMessage,
);
await expectBuilderError(getSupportedNodeVersion('6.x', false), foundMessage);
await expectBuilderError(
getSupportedNodeVersion('999.x', false),
foundMessage
foundMessage,
);
await expectBuilderError(getSupportedNodeVersion('foo', false), foundMessage);
await expectBuilderError(
getSupportedNodeVersion('=> 10', false),
foundMessage
foundMessage,
);
});
@@ -281,17 +279,17 @@ it('should match all semver ranges', async () => {
expect(await getSupportedNodeVersion('>=10.3.0')).toHaveProperty('major', 16);
expect(await getSupportedNodeVersion('11.5.0 - 12.5.0')).toHaveProperty(
'major',
12
12,
);
expect(await getSupportedNodeVersion('>=9.5.0 <=12.5.0')).toHaveProperty(
'major',
12
12,
);
expect(await getSupportedNodeVersion('~12.5.0')).toHaveProperty('major', 12);
expect(await getSupportedNodeVersion('^12.5.0')).toHaveProperty('major', 12);
expect(await getSupportedNodeVersion('12.5.0 - 14.5.0')).toHaveProperty(
'major',
14
14,
);
});
@@ -301,14 +299,14 @@ it('should ignore node version in vercel dev getNodeVersion()', async () => {
'/tmp',
undefined,
{ nodeVersion: '1' },
{ isDev: true }
)
{ isDev: true },
),
).toHaveProperty('runtime', 'nodejs');
});
it('should select project setting from config when no package.json is found', async () => {
expect(
await getNodeVersion('/tmp', undefined, { nodeVersion: '16.x' }, {})
await getNodeVersion('/tmp', undefined, { nodeVersion: '16.x' }, {}),
).toHaveProperty('range', '16.x');
expect(warningMessages).toStrictEqual([]);
});
@@ -319,8 +317,8 @@ it('should prefer package.json engines over project setting from config and warn
path.join(__dirname, 'pkg-engine-node'),
undefined,
{ nodeVersion: '12.x' },
{}
)
{},
),
).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([
'Warning: Due to "engines": { "node": "14.x" } in your `package.json` file, the Node.js Version defined in your Project Settings ("12.x") will not apply. Learn More: http://vercel.link/node-version',
@@ -333,8 +331,8 @@ it('should warn when package.json engines is exact version', async () => {
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',
@@ -347,8 +345,8 @@ it('should warn when package.json engines is greater than', async () => {
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',
@@ -361,8 +359,8 @@ it('should not warn when package.json engines matches project setting from confi
path.join(__dirname, 'pkg-engine-node'),
undefined,
{ nodeVersion: '14' },
{}
)
{},
),
).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([]);
@@ -371,8 +369,8 @@ it('should not warn when package.json engines matches project setting from confi
path.join(__dirname, 'pkg-engine-node'),
undefined,
{ nodeVersion: '14.x' },
{}
)
{},
),
).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([]);
@@ -381,8 +379,8 @@ it('should not warn when package.json engines matches project setting from confi
path.join(__dirname, 'pkg-engine-node'),
undefined,
{ nodeVersion: '<15' },
{}
)
{},
),
).toHaveProperty('range', '14.x');
expect(warningMessages).toStrictEqual([]);
});
@@ -419,19 +417,19 @@ it('should warn for deprecated versions, soon to be discontinued', async () => {
expect(await getSupportedNodeVersion('10.x', false)).toHaveProperty(
'major',
10
10,
);
expect(await getSupportedNodeVersion('10.x', true)).toHaveProperty(
'major',
10
10,
);
expect(await getSupportedNodeVersion('12.x', false)).toHaveProperty(
'major',
12
12,
);
expect(await getSupportedNodeVersion('12.x', true)).toHaveProperty(
'major',
12
12,
);
expect(warningMessages).toStrictEqual([
'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
@@ -493,7 +491,7 @@ it(
const out = await fs.readFile(path.join(fixture, 'env.txt'), 'utf8');
expect(out.trim()).toBeTruthy();
},
ms('1m')
ms('1m'),
);
it('should return lockfileVersion 2 with npm7', async () => {
@@ -501,6 +499,7 @@ it('should return lockfileVersion 2 with npm7', async () => {
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('npm');
expect(result.lockfileVersion).toEqual(2);
expect(result.lockfilePath).toEqual(path.join(fixture, 'package-lock.json'));
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
@@ -509,6 +508,7 @@ it('should not return lockfileVersion with yarn', async () => {
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('yarn');
expect(result.lockfileVersion).toEqual(undefined);
expect(result.lockfilePath).toEqual(path.join(fixture, 'yarn.lock'));
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
@@ -517,6 +517,7 @@ it('should return lockfileVersion 1 with older versions of npm', async () => {
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('npm');
expect(result.lockfileVersion).toEqual(1);
expect(result.lockfilePath).toEqual(path.join(fixture, 'package-lock.json'));
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
@@ -525,6 +526,9 @@ it('should detect npm Workspaces', async () => {
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('npm');
expect(result.lockfileVersion).toEqual(2);
expect(result.lockfilePath).toEqual(
path.join(fixture, '..', 'package-lock.json'),
);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
@@ -533,6 +537,7 @@ it('should detect pnpm without workspace', async () => {
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('pnpm');
expect(result.lockfileVersion).toEqual(5.3);
expect(result.lockfilePath).toEqual(path.join(fixture, 'pnpm-lock.yaml'));
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
@@ -541,78 +546,32 @@ it('should detect pnpm with workspaces', async () => {
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('pnpm');
expect(result.lockfileVersion).toEqual(5.3);
expect(result.lockfilePath).toEqual(
path.join(fixture, '..', 'pnpm-lock.yaml'),
);
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
it('should detect package.json in nested backend', async () => {
const fixture = path.join(
__dirname,
'../../node/test/fixtures/18.1-nested-packagejson/backend'
'../../node/test/fixtures/18.1-nested-packagejson/backend',
);
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('yarn');
expect(result.lockfileVersion).toEqual(undefined);
// There is no lockfile but this test will pick up vercel/vercel/yarn.lock
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
it('should detect package.json in nested frontend', async () => {
const fixture = path.join(
__dirname,
'../../node/test/fixtures/18.1-nested-packagejson/frontend'
'../../node/test/fixtures/18.1-nested-packagejson/frontend',
);
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('yarn');
expect(result.lockfileVersion).toEqual(undefined);
// There is no lockfile but this test will pick up vercel/vercel/yarn.lock
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});
it('should only invoke `runNpmInstall()` once per `package.json` file (serial)', async () => {
const meta: Meta = {};
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const apiDir = path.join(fixture, 'api');
const retryOpts = { maxRetryTime: 1000 };
let run1, run2, run3;
await retry(async () => {
run1 = await runNpmInstall(apiDir, [], undefined, meta);
expect(run1).toEqual(true);
expect(
(meta.runNpmInstallSet as Set<string>).has(
path.join(fixture, 'package.json')
)
).toEqual(true);
}, retryOpts);
await retry(async () => {
run2 = await runNpmInstall(apiDir, [], undefined, meta);
expect(run2).toEqual(false);
}, retryOpts);
await retry(async () => {
run3 = await runNpmInstall(fixture, [], undefined, meta);
expect(run3).toEqual(false);
}, retryOpts);
});
it('should only invoke `runNpmInstall()` once per `package.json` file (parallel)', async () => {
const meta: Meta = {};
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const apiDir = path.join(fixture, 'api');
let results: [boolean, boolean, boolean] | undefined;
await retry(
async () => {
results = await Promise.all([
runNpmInstall(apiDir, [], undefined, meta),
runNpmInstall(apiDir, [], undefined, meta),
runNpmInstall(fixture, [], undefined, meta),
]);
},
{ maxRetryTime: 3000 }
);
const [run1, run2, run3] = results || [];
expect(run1).toEqual(true);
expect(run2).toEqual(false);
expect(run3).toEqual(false);
expect(
(meta.runNpmInstallSet as Set<string>).has(
path.join(fixture, 'package.json')
)
).toEqual(true);
});

View File

@@ -1,31 +1,38 @@
import { walkParentDirs } from '../src';
import { strict } from 'assert';
import { join } from 'path';
import { promises } from 'fs';
const { deepEqual, notDeepEqual, fail } = strict;
import { walkParentDirs } from '../src';
const { deepEqual, fail } = strict;
const { readFile } = promises;
const fixture = (name: string) => join(__dirname, 'walk', name);
const filename = 'file.txt';
async function assertContent(target: string | null, contents: string) {
notDeepEqual(target, null);
const actual = await readFile(target!, 'utf8');
async function expectContent(target: string | null, contents: string) {
if (typeof target !== 'string') {
throw new Error(`Expected target to be a string`);
}
const actual = await readFile(target, 'utf8');
expect;
deepEqual(actual.trim(), contents.trim());
}
describe('Test `walkParentDirs`', () => {
describe('walkParentDirs()', () => {
it('should throw when `base` is relative', async () => {
let err: Error | undefined;
const base = './relative';
const start = __dirname;
try {
await walkParentDirs({ base, start, filename });
fail('Expected error');
} catch (error) {
deepEqual(
(error as Error).message,
'Expected "base" to be absolute path'
);
} catch (_err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
err = _err;
}
if (!err) {
throw new Error('Expected `err` to be defined');
}
expect(err.message).toEqual('Expected "base" to be absolute path');
});
it('should throw when `start` is relative', async () => {
@@ -37,7 +44,7 @@ describe('Test `walkParentDirs`', () => {
} catch (error) {
deepEqual(
(error as Error).message,
'Expected "start" to be absolute path'
'Expected "start" to be absolute path',
);
}
});
@@ -46,21 +53,21 @@ describe('Test `walkParentDirs`', () => {
const base = fixture('every-directory');
const start = base;
const target = await walkParentDirs({ base, start, filename });
await assertContent(target, 'First');
await expectContent(target, 'First');
});
it('should find nested two', async () => {
const base = fixture('every-directory');
const start = join(base, 'two');
const target = await walkParentDirs({ base, start, filename });
await assertContent(target, 'Second');
await expectContent(target, 'Second');
});
it('should find nested three', async () => {
const base = fixture('every-directory');
const start = join(base, 'two', 'three');
const target = await walkParentDirs({ base, start, filename });
await assertContent(target, 'Third');
await expectContent(target, 'Third');
});
it('should not find nested one', async () => {
@@ -88,20 +95,20 @@ describe('Test `walkParentDirs`', () => {
const base = fixture('only-one');
const start = join(base, 'two', 'three');
const target = await walkParentDirs({ base, start, filename });
await assertContent(target, 'First');
await expectContent(target, 'First');
});
it('should find only two', async () => {
const base = fixture('only-two');
const start = join(base, 'two', 'three');
const target = await walkParentDirs({ base, start, filename });
await assertContent(target, 'Second');
await expectContent(target, 'Second');
});
it('should find only three', async () => {
const base = fixture('only-three');
const start = join(base, 'two', 'three');
const target = await walkParentDirs({ base, start, filename });
await assertContent(target, 'Third');
await expectContent(target, 'Third');
});
});

View File

@@ -1,20 +1,19 @@
{
"extends": "@vercel/style-guide/typescript",
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"esModuleInterop": true,
"lib": ["ES2020"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./dist",
"types": ["node", "jest"],
"strict": true,
"target": "ES2020"
},
"include": ["src/**/*"],
"include": ["src/**/*", "test/*.test.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,4 @@
dist
test/fixtures
test/dev/fixtures
src/util/dev/templates/*.ts

View File

@@ -0,0 +1,20 @@
const { resolve } = require('path');
const baseConfig = require.resolve('../../.eslintrc.cjs');
const project = resolve(__dirname, 'tsconfig.json');
module.exports = {
root: true,
extends: [baseConfig],
parserOptions: {
project,
},
settings: {
'import/resolver': {
typescript: {
project,
},
},
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.1.4",
"version": "28.3.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -41,16 +41,16 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "5.3.2",
"@vercel/go": "2.2.2",
"@vercel/hydrogen": "0.0.15",
"@vercel/next": "3.1.21",
"@vercel/node": "2.5.10",
"@vercel/python": "3.1.11",
"@vercel/redwood": "1.0.19",
"@vercel/remix": "1.0.20",
"@vercel/ruby": "1.3.28",
"@vercel/static-build": "1.0.19",
"@vercel/build-utils": "5.4.3",
"@vercel/go": "2.2.6",
"@vercel/hydrogen": "0.0.19",
"@vercel/next": "3.1.26",
"@vercel/node": "2.5.15",
"@vercel/python": "3.1.15",
"@vercel/redwood": "1.0.24",
"@vercel/remix": "1.0.25",
"@vercel/ruby": "1.3.32",
"@vercel/static-build": "1.0.24",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -85,7 +85,6 @@
"@types/node-fetch": "2.5.10",
"@types/npm-package-arg": "6.1.0",
"@types/pluralize": "0.0.29",
"@types/progress": "2.0.3",
"@types/psl": "1.1.0",
"@types/semver": "6.0.1",
"@types/tar-fs": "1.16.1",
@@ -96,9 +95,9 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.2.1",
"@vercel/frameworks": "1.1.3",
"@vercel/fs-detectors": "2.0.5",
"@vercel/client": "12.2.5",
"@vercel/frameworks": "1.1.6",
"@vercel/fs-detectors": "3.3.0",
"@vercel/fun": "1.0.4",
"@vercel/ncc": "0.24.0",
"@zeit/source-map-support": "0.6.2",
@@ -153,7 +152,6 @@
"ora": "3.4.0",
"pcre-to-regexp": "1.0.0",
"pluralize": "7.0.0",
"progress": "2.0.3",
"promisepipe": "3.0.0",
"psl": "1.1.31",
"qr-image": "3.2.0",

View File

@@ -1,5 +1,5 @@
const execa = require('execa');
const { join } = require('path');
const execa = require('execa');
const { readFile, writeFile, readdir, remove } = require('fs-extra');
async function main() {
@@ -14,7 +14,7 @@ async function main() {
});
const files = await readdir(templatesDir);
const compiledFiles = files.filter(f => f.endsWith('.js'));
const compiledFiles = files.filter((f) => f.endsWith('.js'));
// Prettier
console.log('\nMaking the compiled template functions prettier...');
@@ -25,7 +25,7 @@ async function main() {
{
cwd: templatesDir,
stdio: 'inherit',
}
},
);
console.log('\nConverting template functions to TypeScript');
@@ -36,9 +36,7 @@ async function main() {
const def = await readFile(fnPath.replace(/\.js$/, '.tsdef'), 'utf8');
const interfaceName = def.match(/interface (\w+)/)[1];
const lines = require(fnPath)
.toString()
.split('\n');
const lines = require(fnPath).toString().split('\n');
let errorHtmlStart = -1;
let errorHtmlEnd = -1;
for (let i = 0; i < lines.length; i++) {
@@ -56,35 +54,35 @@ async function main() {
lines[0] = `export default ${lines[0].replace(
'(it)',
`(it: ${interfaceName}): string`
`(it: ${interfaceName}): string`,
)}`;
lines.unshift(
"import encodeHTML from 'escape-html';",
'',
...def.split('\n')
...def.split('\n'),
);
await Promise.all([writeFile(tsPath, lines.join('\n')), remove(fnPath)]);
console.log(
`${file} -> ${file.replace(/\.js$/, '.ts')} (${Date.now() - start}ms)`
`${file} -> ${file.replace(/\.js$/, '.ts')} (${Date.now() - start}ms)`,
);
}
}
process.on('unhandledRejection', err => {
process.on('unhandledRejection', (err) => {
console.error('Unhandled Rejection:');
console.error(err);
process.exit(1);
});
process.on('uncaughtException', err => {
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:');
console.error(err);
process.exit(1);
});
main().catch(err => {
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -4,7 +4,7 @@ const { statSync } = require('fs');
const pkg = require('../package');
function error(command) {
console.error('> Error!', command);
console.error('> Error:', command);
}
function debug(str) {

View File

@@ -1,16 +1,13 @@
import chalk from 'chalk';
import { handleError } from '../../util/error';
import Client from '../../util/client';
import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import ls from './ls';
import rm from './rm';
import set from './set';
import type Client from '../../util/client';
const help = () => {
console.log(`
@@ -26,14 +23,14 @@ const help = () => {
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
'FILE',
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
'DIR',
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
'TOKEN',
)} Login token
-S, --scope Set a custom scope
-N, --next Show next page of results
@@ -45,23 +42,23 @@ const help = () => {
${chalk.cyan(
`$ ${getPkgName()} alias set ${chalk.underline(
'api-ownv3nc9f8.vercel.app'
)} ${chalk.underline('my-api.vercel.app')}`
'api-ownv3nc9f8.vercel.app',
)} ${chalk.underline('my-api.vercel.app')}`,
)}
Custom domains work as alias targets
${chalk.cyan(
`$ ${getPkgName()} alias set ${chalk.underline(
'api-ownv3nc9f8.vercel.app'
)} ${chalk.underline('my-api.com')}`
'api-ownv3nc9f8.vercel.app',
)} ${chalk.underline('my-api.com')}`,
)}
${chalk.dim('')} The subcommand ${chalk.dim(
'`set`'
'`set`',
)} is the default and can be skipped.
${chalk.dim('')} ${chalk.dim(
'Protocols'
'Protocols',
)} in the URLs are unneeded and ignored.
`);
};

View File

@@ -1,15 +1,14 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import Client from '../../util/client';
import getAliases from '../../util/alias/get-aliases';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import { Alias } from '../../types';
import type Client from '../../util/client';
import type { Alias } from '../../types';
interface Options {
'--next'?: number;
@@ -18,7 +17,7 @@ interface Options {
export default async function ls(
client: Client,
opts: Options,
args: string[]
args: string[],
) {
const { output } = client;
const { '--next': nextTimestamp } = opts;
@@ -34,8 +33,8 @@ export default async function ls(
if (args.length > 0) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('alias ls')}`
)}`
`${getCommandName('alias ls')}`,
)}`,
);
return 1;
}
@@ -46,7 +45,7 @@ export default async function ls(
const { aliases, pagination } = await getAliases(
client,
undefined,
nextTimestamp
nextTimestamp,
);
output.log(`aliases found under ${chalk.bold(contextName)} ${lsStamp()}`);
console.log(printAliasTable(aliases));
@@ -55,8 +54,8 @@ export default async function ls(
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page run ${getCommandName(
`alias ls${flags} --next ${pagination.next}`
)}`
`alias ls${flags} --next ${pagination.next}`,
)}`,
);
}
@@ -66,8 +65,8 @@ export default async function ls(
function printAliasTable(aliases: Alias[]) {
return `${table(
[
['source', 'url', 'age'].map(header => chalk.gray(header)),
...aliases.map(a => [
['source', 'url', 'age'].map((header) => chalk.gray(header)),
...aliases.map((a) => [
// for legacy reasons, we might have situations
// where the deployment was deleted and the alias
// not collected appropriately, and we need to handle it
@@ -80,6 +79,6 @@ function printAliasTable(aliases: Alias[]) {
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen,
}
},
).replace(/^/gm, ' ')}\n\n`;
}

View File

@@ -1,26 +1,25 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import removeAliasById from '../../util/alias/remove-alias-by-id';
import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import confirm from '../../util/input/confirm';
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
import { Alias } from '../../types';
import { isValidName } from '../../util/is-valid-name';
import { getCommandName } from '../../util/pkg-name';
import type { Alias } from '../../types';
import type Client from '../../util/client';
type Options = {
interface Options {
'--yes': boolean;
};
}
export default async function rm(
client: Client,
opts: Partial<Options>,
args: string[]
args: string[],
) {
const { output } = client;
const { contextName } = await getScope(client);
@@ -30,8 +29,8 @@ export default async function rm(
if (args.length !== 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('alias rm <alias>')}`
)}`
`${getCommandName('alias rm <alias>')}`,
)}`,
);
return 1;
}
@@ -51,7 +50,7 @@ export default async function rm(
if (!alias) {
output.error(
`Alias not found by "${aliasOrId}" under ${chalk.bold(contextName)}`
`Alias not found by "${aliasOrId}" under ${chalk.bold(contextName)}`,
);
output.log(`Run ${getCommandName('alias ls')} to see your aliases.`);
return 1;
@@ -66,8 +65,8 @@ export default async function rm(
await removeAliasById(client, alias.uid);
console.log(
`${chalk.cyan('> Success!')} Alias ${chalk.bold(
alias.alias
)} removed ${removeStamp()}`
alias.alias,
)} removed ${removeStamp()}`,
);
return 0;
}
@@ -88,7 +87,7 @@ async function confirmAliasRemove(client: Client, alias: Alias) {
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen,
}
},
);
client.output.log(`The following alias will be removed permanently`);

View File

@@ -1,15 +1,9 @@
import chalk from 'chalk';
import { SetDifference } from 'utility-types';
import { AliasRecord } from '../../util/alias/create-alias';
import { Domain } from '../../types';
import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import assignAlias from '../../util/alias/assign-alias';
import Client from '../../util/client';
import getDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host';
import { getDeploymentForAlias } from '../../util/alias/get-deployment-by-alias';
import getScope from '../../util/get-scope';
import setupDomain from '../../util/domains/setup-domain';
import stamp from '../../util/output/stamp';
import { isValidName } from '../../util/is-valid-name';
import handleCertError from '../../util/certs/handle-cert-error';
@@ -17,17 +11,23 @@ import isWildcardAlias from '../../util/alias/is-wildcard-alias';
import link from '../../util/output/link';
import { getCommandName } from '../../util/pkg-name';
import toHost from '../../util/to-host';
import type setupDomain from '../../util/domains/setup-domain';
import type Client from '../../util/client';
import type { Output } from '../../util/output';
import type { Domain } from '../../types';
import type { AliasRecord } from '../../util/alias/create-alias';
import type { SetDifference } from 'utility-types';
import type { VercelConfig } from '@vercel/client';
type Options = {
interface Options {
'--debug': boolean;
'--local-config': string;
};
}
export default async function set(
client: Client,
opts: Partial<Options>,
args: string[]
args: string[],
) {
const setStamp = stamp();
const { output, localConfig } = client;
@@ -37,15 +37,15 @@ export default async function set(
if (args.length > 2) {
output.error(
`${getCommandName(
`alias <deployment> <target>`
)} accepts at most two arguments`
`alias <deployment> <target>`,
)} accepts at most two arguments`,
);
return 1;
}
if (args.length >= 1 && !isValidName(args[0])) {
output.error(
`The provided argument "${args[0]}" is not a valid deployment`
`The provided argument "${args[0]}" is not a valid deployment`,
);
return 1;
}
@@ -58,8 +58,8 @@ export default async function set(
if (args.length === 0) {
output.error(
`To ship to production, optionally configure your domains (${link(
'https://vercel.link/domain-configuration'
)}) and run ${getCommandName(`--prod`)}.`
'https://vercel.link/domain-configuration',
)}) and run ${getCommandName(`--prod`)}.`,
);
return 1;
}
@@ -75,8 +75,8 @@ export default async function set(
opts['--local-config'],
user,
contextName,
localConfig
)
localConfig,
),
);
if (deployment === 1) {
@@ -90,7 +90,7 @@ export default async function set(
if (!deployment) {
output.error(
`Couldn't find a deployment to alias. Please provide one as an argument.`
`Couldn't find a deployment to alias. Please provide one as an argument.`,
);
return 1;
}
@@ -111,12 +111,12 @@ export default async function set(
client,
deployment,
target,
contextName
contextName,
);
const handleResult = handleSetupDomainError(
output,
handleCreateAliasError(output, record)
handleCreateAliasError(output, record),
);
if (handleResult === 1) {
@@ -125,8 +125,8 @@ export default async function set(
console.log(
`${chalk.cyan('> Success!')} ${chalk.bold(
`${isWildcardAlias(target) ? '' : 'https://'}${handleResult.alias}`
)} now points to https://${deployment.url} ${setStamp()}`
`${isWildcardAlias(target) ? '' : 'https://'}${handleResult.alias}`,
)} now points to https://${deployment.url} ${setStamp()}`,
);
}
@@ -136,7 +136,7 @@ export default async function set(
const [deploymentIdOrHost, aliasTarget] = args;
const deployment = handleCertError(
output,
await getDeploymentByIdOrHost(client, contextName, deploymentIdOrHost)
await getDeploymentByIdOrHost(client, contextName, deploymentIdOrHost),
);
if (deployment === 1) {
@@ -146,8 +146,8 @@ export default async function set(
if (deployment instanceof ERRORS.DeploymentNotFound) {
output.error(
`Failed to find deployment "${deployment.meta.id}" under ${chalk.bold(
contextName
)}`
contextName,
)}`,
);
return 1;
}
@@ -156,7 +156,7 @@ export default async function set(
output.error(
`No permission to access deployment "${
deployment.meta.id
}" under ${chalk.bold(deployment.meta.context)}`
}" under ${chalk.bold(deployment.meta.context)}`,
);
return 1;
}
@@ -168,7 +168,7 @@ export default async function set(
if (deployment === null) {
output.error(
`Couldn't find a deployment to alias. Please provide one as an argument.`
`Couldn't find a deployment to alias. Please provide one as an argument.`,
);
return 1;
}
@@ -181,11 +181,11 @@ export default async function set(
client,
deployment,
aliasTarget,
contextName
contextName,
);
const handleResult = handleSetupDomainError(
output,
handleCreateAliasError(output, record)
handleCreateAliasError(output, record),
);
if (handleResult === 1) {
return 1;
@@ -195,8 +195,8 @@ export default async function set(
console.log(
`${chalk.cyan('> Success!')} ${chalk.bold(
`${prefix}${handleResult.alias}`
)} now points to https://${deployment.url} ${setStamp()}`
`${prefix}${handleResult.alias}`,
)} now points to https://${deployment.url} ${setStamp()}`,
);
return 0;
@@ -208,13 +208,13 @@ type SetupDomainError = Exclude<SetupDomainResolve, Domain>;
function handleSetupDomainError<T>(
output: Output,
error: SetupDomainError | T
error: SetupDomainError | T,
): T | 1 {
if (error instanceof ERRORS.DomainPermissionDenied) {
output.error(
`You don't have permissions over domain ${chalk.underline(
error.meta.domain
)} under ${chalk.bold(error.meta.context)}.`
error.meta.domain,
)} under ${chalk.bold(error.meta.context)}.`,
);
return 1;
}
@@ -231,28 +231,28 @@ function handleSetupDomainError<T>(
if (error instanceof ERRORS.UnsupportedTLD) {
output.error(
`The TLD for domain name ${error.meta.domain} is not supported.`
`The TLD for domain name ${error.meta.domain} is not supported.`,
);
return 1;
}
if (error instanceof ERRORS.InvalidDomain) {
output.error(
`The domain ${error.meta.domain} used for the alias is not valid.`
`The domain ${error.meta.domain} used for the alias is not valid.`,
);
return 1;
}
if (error instanceof ERRORS.DomainNotAvailable) {
output.error(
`The domain ${error.meta.domain} is not available to be purchased.`
`The domain ${error.meta.domain} is not available to be purchased.`,
);
return 1;
}
if (error instanceof ERRORS.DomainServiceNotAvailable) {
output.error(
`The domain purchase service is not available. Try again later.`
`The domain purchase service is not available. Try again later.`,
);
return 1;
}
@@ -264,24 +264,24 @@ function handleSetupDomainError<T>(
if (error instanceof ERRORS.DomainAlreadyExists) {
output.error(
`The domain ${error.meta.domain} exists for a different account.`
`The domain ${error.meta.domain} exists for a different account.`,
);
return 1;
}
if (error instanceof ERRORS.DomainPurchasePending) {
output.error(
`The domain ${error.meta.domain} is processing and will be available once the order is completed.`
`The domain ${error.meta.domain} is processing and will be available once the order is completed.`,
);
output.print(
` An email will be sent upon completion so you can alias to your new domain.\n`
` An email will be sent upon completion so you can alias to your new domain.\n`,
);
return 1;
}
if (error instanceof ERRORS.SourceNotFound) {
output.error(
`You can't purchase the domain you're aliasing to since you have no valid payment method.`
`You can't purchase the domain you're aliasing to since you have no valid payment method.`,
);
output.print(` Please add a valid payment method and retry.\n`);
return 1;
@@ -289,7 +289,7 @@ function handleSetupDomainError<T>(
if (error instanceof ERRORS.DomainPaymentError) {
output.error(
`You can't purchase the domain you're aliasing to since your card was declined.`
`You can't purchase the domain you're aliasing to since your card was declined.`,
);
output.print(` Please add a valid payment method and retry.\n`);
return 1;
@@ -307,7 +307,7 @@ type RemainingAssignAliasErrors = SetDifference<
function handleCreateAliasError<T>(
output: Output,
errorOrResult: RemainingAssignAliasErrors | T
errorOrResult: RemainingAssignAliasErrors | T,
): 1 | T {
const error = handleCertError(output, errorOrResult);
if (error === 1) {
@@ -317,8 +317,8 @@ function handleCreateAliasError<T>(
if (error instanceof ERRORS.AliasInUse) {
output.error(
`The alias ${chalk.dim(
error.meta.alias
)} is a deployment URL or it's in use by a different team.`
error.meta.alias,
)} is a deployment URL or it's in use by a different team.`,
);
return 1;
}
@@ -326,41 +326,41 @@ function handleCreateAliasError<T>(
if (error instanceof ERRORS.DeploymentNotFound) {
output.error(
`Failed to find deployment ${chalk.dim(error.meta.id)} under ${chalk.bold(
error.meta.context
)}`
error.meta.context,
)}`,
);
return 1;
}
if (error instanceof ERRORS.InvalidAlias) {
output.error(
`Invalid alias. Please confirm that the alias you provided is a valid hostname. Note: For \`vercel.app\`, only sub and sub-sub domains are supported.`
`Invalid alias. Please confirm that the alias you provided is a valid hostname. Note: For \`vercel.app\`, only sub and sub-sub domains are supported.`,
);
return 1;
}
if (error instanceof ERRORS.DeploymentPermissionDenied) {
output.error(
`No permission to access deployment ${chalk.dim(
error.meta.id
)} under ${chalk.bold(error.meta.context)}`
error.meta.id,
)} under ${chalk.bold(error.meta.context)}`,
);
return 1;
}
if (error instanceof ERRORS.CertMissing) {
output.error(
`There is no certificate for the domain ${error.meta.domain} and it could not be created.`
`There is no certificate for the domain ${error.meta.domain} and it could not be created.`,
);
output.log(
`Please generate a new certificate manually with ${getCommandName(
`certs issue ${error.meta.domain}`
)}`
`certs issue ${error.meta.domain}`,
)}`,
);
return 1;
}
if (error instanceof ERRORS.InvalidDomain) {
output.error(
`The domain ${error.meta.domain} used for the alias is not valid.`
`The domain ${error.meta.domain} used for the alias is not valid.`,
);
return 1;
}
@@ -385,8 +385,8 @@ function handleCreateAliasError<T>(
function getTargetsForAlias(args: string[], { alias }: VercelConfig = {}) {
if (args.length) {
return [args[args.length - 1]]
.map(target => (target.indexOf('.') !== -1 ? toHost(target) : target))
.filter((x): x is string => !!x && typeof x === 'string');
.map((target) => (target.includes('.') ? toHost(target) : target))
.filter((x): x is string => Boolean(x) && typeof x === 'string');
}
if (!alias) {

View File

@@ -1,21 +1,21 @@
import { resolve } from 'path';
import { URLSearchParams, parse } from 'url';
import open from 'open';
import boxen from 'boxen';
import execa from 'execa';
import plural from 'pluralize';
import inquirer from 'inquirer';
import { resolve } from 'path';
import chalk, { Chalk } from 'chalk';
import { URLSearchParams, parse } from 'url';
import chalk from 'chalk';
import sleep from '../../util/sleep';
import formatDate from '../../util/format-date';
import link from '../../util/output/link';
import logo from '../../util/output/logo';
import getArgs from '../../util/get-args';
import Client from '../../util/client';
import { getPkgName } from '../../util/pkg-name';
import { Deployment, PaginationOptions } from '../../types';
import { normalizeURL } from '../../util/bisect/normalize-url';
import type { Chalk } from 'chalk';
import type Client from '../../util/client';
import type { Deployment, PaginationOptions } from '../../types';
interface DeploymentV6
extends Pick<
@@ -108,8 +108,8 @@ export default async function main(client: Client): Promise<number> {
if (subpath && subpath !== parsed.path) {
output.note(
`Ignoring subpath ${chalk.bold(
parsed.path
)} in favor of \`--path\` argument ${chalk.bold(subpath)}`
parsed.path,
)} in favor of \`--path\` argument ${chalk.bold(subpath)}`,
);
} else {
subpath = parsed.path;
@@ -131,22 +131,22 @@ export default async function main(client: Client): Promise<number> {
) {
output.note(
`Ignoring subpath ${chalk.bold(
parsed.path
)} which does not match ${chalk.bold(subpath)}`
parsed.path,
)} which does not match ${chalk.bold(subpath)}`,
);
}
if (!subpath) {
subpath = await prompt(
client,
`Specify the URL subpath where the bug occurs:`
`Specify the URL subpath where the bug occurs:`,
);
}
output.spinner('Retrieving deployments…');
// `getDeployment` cannot be parallelized because it might prompt for login
const badDeployment = await getDeployment(client, bad).catch(err => err);
const badDeployment = await getDeployment(client, bad).catch((err) => err);
if (badDeployment) {
if (badDeployment instanceof Error) {
@@ -161,7 +161,7 @@ export default async function main(client: Client): Promise<number> {
}
// `getDeployment` cannot be parallelized because it might prompt for login
const goodDeployment = await getDeployment(client, good).catch(err => err);
const goodDeployment = await getDeployment(client, good).catch((err) => err);
if (goodDeployment) {
if (goodDeployment instanceof Error) {
@@ -172,7 +172,7 @@ export default async function main(client: Client): Promise<number> {
good = goodDeployment.url;
} else {
output.error(
`Failed to retrieve ${chalk.bold('good')} Deployment: ${good}`
`Failed to retrieve ${chalk.bold('good')} Deployment: ${good}`,
);
return 1;
}
@@ -195,7 +195,7 @@ export default async function main(client: Client): Promise<number> {
badDeployment.target || 'preview'
}" does not match good deployment target "${
goodDeployment.target || 'preview'
}"`
}"`,
);
return 1;
}
@@ -242,7 +242,7 @@ export default async function main(client: Client): Promise<number> {
if (!deployments.length) {
output.error(
'Cannot bisect because this project does not have any deployments'
'Cannot bisect because this project does not have any deployments',
);
return 1;
}
@@ -262,10 +262,10 @@ export default async function main(client: Client): Promise<number> {
output.log(
chalk.magenta(
`${chalk.bold(
'Bisecting:'
)} ${rem} left to test after this (roughly ${pSteps})`
'Bisecting:',
)} ${rem} left to test after this (roughly ${pSteps})`,
),
chalk.magenta
chalk.magenta,
);
const testUrl = `https://${deployment.url}${subpath}`;
output.log(`${chalk.bold('Deployment URL:')} ${link(testUrl)}`);
@@ -309,8 +309,8 @@ export default async function main(client: Client): Promise<number> {
}
output.log(
`Run script returned exit code ${chalk.bold(String(exitCode))}: ${color(
action
)}`
action,
)}`,
);
} else {
if (openEnabled) {
@@ -341,9 +341,9 @@ export default async function main(client: Client): Promise<number> {
output.print('\n');
let result = [
const result = [
chalk.bold(
`The first bad deployment is: ${link(`https://${lastBad.url}`)}`
`The first bad deployment is: ${link(`https://${lastBad.url}`)}`,
),
'',
` ${chalk.bold('Date:')} ${formatDate(lastBad.createdAt)}`,
@@ -366,7 +366,7 @@ export default async function main(client: Client): Promise<number> {
function getDeployment(
client: Client,
hostname: string
hostname: string,
): Promise<DeploymentV6> {
const query = new URLSearchParams();
query.set('url', hostname);
@@ -377,14 +377,14 @@ function getDeployment(
function getCommit(deployment: DeploymentV6) {
const sha =
deployment.meta?.githubCommitSha ||
deployment.meta?.gitlabCommitSha ||
deployment.meta?.bitbucketCommitSha;
deployment.meta.githubCommitSha ||
deployment.meta.gitlabCommitSha ||
deployment.meta.bitbucketCommitSha;
if (!sha) return null;
const message =
deployment.meta?.githubCommitMessage ||
deployment.meta?.gitlabCommitMessage ||
deployment.meta?.bitbucketCommitMessage;
deployment.meta.githubCommitMessage ||
deployment.meta.gitlabCommitMessage ||
deployment.meta.bitbucketCommitMessage;
return { sha, message };
}
@@ -398,8 +398,7 @@ async function prompt(client: Client, message: string): Promise<string> {
});
if (val) {
return val;
} else {
client.output.error('A value must be specified');
}
client.output.error('A value must be specified');
}
}

View File

@@ -1,11 +1,41 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import dotenv from 'dotenv';
import { join, normalize, relative, resolve } from 'path';
import dotenv from 'dotenv';
import chalk from 'chalk';
import fs from 'fs-extra';
import { normalizePath, FileFsRef, NowBuildError } from '@vercel/build-utils';
import { detectBuilders } from '@vercel/fs-detectors';
import minimatch from 'minimatch';
import {
normalizePath,
appendRoutesToPhase,
getTransformedRoutes,
mergeRoutes,
} from '@vercel/routing-utils';
import { staticFiles as getFiles } from '../util/get-files';
import getArgs from '../util/get-args';
import cmd from '../util/output/cmd';
import * as cli from '../util/pkg-name';
import cliPkg from '../util/pkg';
import readJSONFile from '../util/read-json-file';
import { CantParseJSONFile } from '../util/errors-ts';
import {
pickOverrides,
readProjectSettings,
} from '../util/projects/project-settings';
import { VERCEL_DIR } from '../util/projects/link';
import confirm from '../util/input/confirm';
import { emoji, prependEmoji } from '../util/emoji';
import stamp from '../util/output/stamp';
import { OUTPUT_DIR, writeBuildResult } from '../util/build/write-build-result';
import { importBuilders } from '../util/build/import-builders';
import { initCorepack, cleanupCorepack } from '../util/build/corepack';
import { sortBuilders } from '../util/build/sort-builders';
import { toEnumerableError } from '../util/error';
import pull from './pull';
import type { PathOverride } from '../util/build/write-build-result';
import type { ProjectLinkAndSettings } from '../util/projects/project-settings';
import type Client from '../util/client';
import type {
Files,
FileFsRef,
PackageJson,
BuildOptions,
Config,
@@ -14,46 +44,10 @@ import {
BuildResultV2,
BuildResultV2Typical,
BuildResultV3,
NowBuildError,
} from '@vercel/build-utils';
import { detectBuilders } from '@vercel/fs-detectors';
import minimatch from 'minimatch';
import {
appendRoutesToPhase,
getTransformedRoutes,
mergeRoutes,
MergeRoutesProps,
Route,
} from '@vercel/routing-utils';
import type { MergeRoutesProps, Route } from '@vercel/routing-utils';
import type { VercelConfig } from '@vercel/client';
import pull from './pull';
import { staticFiles as getFiles } from '../util/get-files';
import Client from '../util/client';
import getArgs from '../util/get-args';
import cmd from '../util/output/cmd';
import * as cli from '../util/pkg-name';
import cliPkg from '../util/pkg';
import readJSONFile from '../util/read-json-file';
import { CantParseJSONFile } from '../util/errors-ts';
import {
ProjectLinkAndSettings,
readProjectSettings,
} from '../util/projects/project-settings';
import { VERCEL_DIR } from '../util/projects/link';
import confirm from '../util/input/confirm';
import { emoji, prependEmoji } from '../util/emoji';
import stamp from '../util/output/stamp';
import {
OUTPUT_DIR,
PathOverride,
writeBuildResult,
} from '../util/build/write-build-result';
import { importBuilders } from '../util/build/import-builders';
import { initCorepack, cleanupCorepack } from '../util/build/corepack';
import { sortBuilders } from '../util/build/sort-builders';
import { toEnumerableError } from '../util/error';
type BuildResult = BuildResultV2 | BuildResultV3;
interface SerializedBuilder extends Builder {
@@ -82,10 +76,10 @@ const help = () => {
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
'FILE',
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
'DIR',
)} Path to the global ${'`.vercel`'} directory
--cwd [path] The current working directory
--output [path] Directory where built assets should be written to
@@ -109,18 +103,17 @@ export default async function main(client: Client): Promise<number> {
if (process.env.__VERCEL_BUILD_RUNNING) {
output.error(
`${cmd(
`${cli.name} build`
`${cli.name} build`,
)} must not recursively invoke itself. Check the Build Command in the Project Settings or the ${cmd(
'build'
)} script in ${cmd('package.json')}`
'build',
)} script in ${cmd('package.json')}`,
);
output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
`Learn More: https://vercel.link/recursive-invocation-of-commands`,
);
return 1;
} else {
process.env.__VERCEL_BUILD_RUNNING = '1';
}
process.env.__VERCEL_BUILD_RUNNING = '1';
// Parse CLI args
const argv = getArgs(client.argv.slice(2), {
@@ -156,8 +149,8 @@ export default async function main(client: Client): Promise<number> {
if (!isTTY) {
client.output.print(
`No Project Settings found locally. Run ${cli.getCommandName(
'pull --yes'
)} to retrieve them.`
'pull --yes',
)} to retrieve them.`,
);
return 1;
}
@@ -165,9 +158,9 @@ export default async function main(client: Client): Promise<number> {
confirmed = await confirm(
client,
`No Project Settings found locally. Run ${cli.getCommandName(
'pull'
'pull',
)} for retrieving them?`,
true
true,
);
}
if (!confirmed) {
@@ -212,7 +205,7 @@ export default async function main(client: Client): Promise<number> {
});
if (dotenvResult.error) {
output.debug(
`Failed loading environment variables: ${dotenvResult.error}`
`Failed loading environment variables: ${dotenvResult.error}`,
);
} else if (dotenvResult.parsed) {
for (const key of Object.keys(dotenvResult.parsed)) {
@@ -263,7 +256,7 @@ async function doBuild(
project: ProjectLinkAndSettings,
buildsJson: BuildsManifest,
cwd: string,
outputDir: string
outputDir: string,
): Promise<number> {
const { output } = client;
const workPath = join(cwd, project.settings.rootDirectory || '.');
@@ -272,15 +265,21 @@ async function doBuild(
const [pkg, vercelConfig] = await Promise.all([
readJSONFile<PackageJson>(join(workPath, 'package.json')),
readJSONFile<VercelConfig>(join(workPath, 'vercel.json')).then(
config => config || readJSONFile<VercelConfig>(join(workPath, 'now.json'))
(config) =>
config || readJSONFile<VercelConfig>(join(workPath, 'now.json')),
),
]);
if (pkg instanceof CantParseJSONFile) throw pkg;
if (vercelConfig instanceof CantParseJSONFile) throw vercelConfig;
const projectSettings = {
...project.settings,
...pickOverrides(vercelConfig || {}),
};
// Get a list of source files
const files = (await getFiles(workPath, client)).map(f =>
normalizePath(relative(workPath, f))
const files = (await getFiles(workPath, client)).map((f) =>
normalizePath(relative(workPath, f)),
);
const routesResult = getTransformedRoutes(vercelConfig || {});
@@ -303,9 +302,9 @@ async function doBuild(
if (builds.length > 0) {
output.warn(
'Due to `builds` existing in your configuration file, the Build and Development Settings defined in your Project Settings will not apply. Learn More: https://vercel.link/unused-build-settings'
'Due to `builds` existing in your configuration file, the Build and Development Settings defined in your Project Settings will not apply. Learn More: https://vercel.link/unused-build-settings',
);
builds = builds.map(b => expandBuild(files, b)).flat();
builds = builds.map((b) => expandBuild(files, b)).flat();
} else {
// Zero config
isZeroConfig = true;
@@ -313,7 +312,7 @@ async function doBuild(
// Detect the Vercel Builders that will need to be invoked
const detectedBuilders = await detectBuilders(files, pkg, {
...vercelConfig,
projectSettings: project.settings,
projectSettings,
ignoreBuildScript: true,
featHandleMiss: true,
});
@@ -324,7 +323,7 @@ async function doBuild(
for (const w of detectedBuilders.warnings) {
console.log(
`Warning: ${w.message} ${w.action || 'Learn More'}: ${w.link}`
`Warning: ${w.message} ${w.action || 'Learn More'}: ${w.link}`,
);
}
@@ -340,7 +339,7 @@ async function doBuild(
routes: [],
newRoutes: detectedBuilders.rewriteRoutes,
phase: 'filesystem',
})
}),
);
zeroConfigRoutes = appendRoutesToPhase({
routes: zeroConfigRoutes,
@@ -350,7 +349,7 @@ async function doBuild(
zeroConfigRoutes.push(...(detectedBuilders.defaultRoutes || []));
}
const builderSpecs = new Set(builds.map(b => b.use));
const builderSpecs = new Set(builds.map((b) => b.use));
const buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
@@ -371,7 +370,7 @@ async function doBuild(
// Write the `detectedBuilders` result to output dir
const buildsJsonBuilds = new Map<Builder, SerializedBuilder>(
builds.map(build => {
builds.map((build) => {
const builderWithPkg = buildersWithPkgs.get(build.use);
if (!builderWithPkg) {
throw new Error(`Failed to load Builder "${build.use}"`);
@@ -386,7 +385,7 @@ async function doBuild(
...build,
},
];
})
}),
);
buildsJson.builds = Array.from(buildsJsonBuilds.values());
const buildsJsonPath = join(outputDir, 'builds.json');
@@ -425,14 +424,14 @@ async function doBuild(
const buildConfig: Config = isZeroConfig
? {
outputDirectory: project.settings.outputDirectory ?? undefined,
outputDirectory: projectSettings.outputDirectory ?? undefined,
...build.config,
projectSettings: project.settings,
installCommand: project.settings.installCommand ?? undefined,
devCommand: project.settings.devCommand ?? undefined,
buildCommand: project.settings.buildCommand ?? undefined,
framework: project.settings.framework,
nodeVersion: project.settings.nodeVersion,
projectSettings,
installCommand: projectSettings.installCommand ?? undefined,
devCommand: projectSettings.devCommand ?? undefined,
buildCommand: projectSettings.buildCommand ?? undefined,
framework: projectSettings.framework,
nodeVersion: projectSettings.nodeVersion,
}
: build.config || {};
const buildOptions: BuildOptions = {
@@ -444,7 +443,7 @@ async function doBuild(
meta,
};
output.debug(
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
`Building entrypoint "${build.src}" with "${builderPkg.name}"`,
);
const buildResult = await builder.build(buildOptions);
@@ -460,13 +459,13 @@ async function doBuild(
build,
builder,
builderPkg,
vercelConfig
vercelConfig,
).then(
override => {
(override) => {
if (override) overrides.push(override);
},
err => err
)
(err) => err,
),
);
} catch (err: any) {
output.prettyError(err);
@@ -474,7 +473,7 @@ async function doBuild(
const writeConfigJsonPromise = fs.writeJSON(
join(outputDir, 'config.json'),
{ version: 3 },
{ spaces: 2 }
{ spaces: 2 },
);
await Promise.all([writeBuildsJsonPromise, writeConfigJsonPromise]);
@@ -527,10 +526,10 @@ async function doBuild(
}
const builderRoutes: MergeRoutesProps['builds'] = Array.from(
buildResults.entries()
buildResults.entries(),
)
.filter(b => 'routes' in b[1] && Array.isArray(b[1].routes))
.map(b => {
.filter((b) => 'routes' in b[1] && Array.isArray(b[1].routes))
.map((b) => {
return {
use: b[0].use,
entrypoint: b[0].src!,
@@ -549,7 +548,150 @@ async function doBuild(
builds: builderRoutes,
});
const mergedImages = mergeImages(buildResults.values());
const images = vercelConfig?.images;
if (images) {
if (typeof images !== 'object') {
throw new Error(
`vercel.json "images" should be an object received ${typeof images}.`,
);
}
if (!Array.isArray(images.domains)) {
throw new Error(
`vercel.json "images.domains" should be an Array received ${typeof images.domains}.`,
);
}
if (images.domains.length > 50) {
throw new Error(
`vercel.json "images.domains" exceeds length of 50 received length (${images.domains.length}).`,
);
}
const invalidImageDomains = images.domains.filter(
(d: unknown) => typeof d !== 'string',
);
if (invalidImageDomains.length > 0) {
throw new Error(
`vercel.json "images.domains" should be an Array of strings received invalid values (${invalidImageDomains.join(
', ',
)}).`,
);
}
if (images.remotePatterns) {
if (!Array.isArray(images.remotePatterns)) {
throw new Error(
`vercel.json "images.remotePatterns" should be an Array received ${typeof images.remotePatterns}.`,
);
}
if (images.remotePatterns.length > 50) {
throw new Error(
`vercel.json "images.remotePatterns" exceeds length of 50, received length (${images.remotePatterns.length}).`,
);
}
const validProps = new Set(['protocol', 'hostname', 'pathname', 'port']);
const requiredProps = ['hostname'];
const invalidPatterns = images.remotePatterns.filter(
(d: unknown) =>
!d ||
typeof d !== 'object' ||
Object.entries(d).some(
([k, v]) => !validProps.has(k) || typeof v !== 'string',
) ||
requiredProps.some((k) => !(k in d)),
);
if (invalidPatterns.length > 0) {
throw new Error(
`vercel.json "images.remotePatterns" received invalid values:\n${invalidPatterns
.map((item) => JSON.stringify(item))
.join(
'\n',
)}\n\nremotePatterns value must follow format { protocol: 'https', hostname: 'example.com', port: '', pathname: '/imgs/**' }.`,
);
}
}
if (!Array.isArray(images.sizes)) {
throw new Error(
`vercel.json "images.sizes" should be an Array received ${typeof images.sizes}.`,
);
}
if (images.sizes.length < 1 || images.sizes.length > 50) {
throw new Error(
`vercel.json "images.sizes" should be an Array of length between 1 to 50 received length (${images.sizes.length}).`,
);
}
const invalidImageSizes = images.sizes.filter((d: unknown) => {
return typeof d !== 'number' || d < 1 || d > 10000;
});
if (invalidImageSizes.length > 0) {
throw new Error(
`vercel.json "images.sizes" should be an Array of numbers that are between 1 and 10000, received invalid values (${invalidImageSizes.join(
', ',
)}).`,
);
}
if (images.minimumCacheTTL) {
if (
!Number.isInteger(images.minimumCacheTTL) ||
images.minimumCacheTTL < 0
) {
throw new Error(
`vercel.json "images.minimumCacheTTL" should be an integer 0 or more received (${images.minimumCacheTTL}).`,
);
}
}
if (images.formats) {
if (!Array.isArray(images.formats)) {
throw new Error(
`vercel.json "images.formats" should be an Array received ${typeof images.formats}.`,
);
}
if (images.formats.length < 1 || images.formats.length > 2) {
throw new Error(
`vercel.json "images.formats" must be length 1 or 2, received length (${images.formats.length}).`,
);
}
const invalid = images.formats.filter((f) => {
return f !== 'image/avif' && f !== 'image/webp';
});
if (invalid.length > 0) {
throw new Error(
`vercel.json "images.formats" should be an Array of mime type strings, received invalid values (${invalid.join(
', ',
)}).`,
);
}
}
if (
typeof images.dangerouslyAllowSVG !== 'undefined' &&
typeof images.dangerouslyAllowSVG !== 'boolean'
) {
throw new Error(
`vercel.json "images.dangerouslyAllowSVG" should be a boolean received (${images.dangerouslyAllowSVG}).`,
);
}
if (
typeof images.contentSecurityPolicy !== 'undefined' &&
typeof images.contentSecurityPolicy !== 'string'
) {
throw new Error(
`vercel.json "images.contentSecurityPolicy" should be a string received ${images.contentSecurityPolicy}`,
);
}
}
const mergedImages = mergeImages(images, buildResults.values());
const mergedWildcard = mergeWildcard(buildResults.values());
const mergedOverrides: Record<string, PathOverride> =
overrides.length > 0 ? Object.assign({}, ...overrides) : undefined;
@@ -570,10 +712,10 @@ async function doBuild(
output.print(
`${prependEmoji(
`Build Completed in ${chalk.bold(
relOutputDir.startsWith('..') ? outputDir : relOutputDir
relOutputDir.startsWith('..') ? outputDir : relOutputDir,
)} ${chalk.gray(buildStamp())}`,
emoji('success')
)}\n`
emoji('success'),
)}\n`,
);
return 0;
@@ -599,17 +741,17 @@ function expandBuild(files: string[], build: Builder): Builder[] {
});
}
if (src[0] === '/') {
if (src.startsWith('/')) {
// Remove a leading slash so that the globbing is relative
// to `cwd` instead of the root of the filesystem.
src = src.substring(1);
}
const matches = files.filter(
name => name === src || minimatch(name, src, { dot: true })
(name) => name === src || minimatch(name, src, { dot: true }),
);
return matches.map(m => {
return matches.map((m) => {
return {
...build,
src: m,
@@ -618,21 +760,21 @@ function expandBuild(files: string[], build: Builder): Builder[] {
}
function mergeImages(
buildResults: Iterable<BuildResult>
images: BuildResultV2Typical['images'],
buildResults: Iterable<BuildResult>,
): BuildResultV2Typical['images'] {
let images: BuildResultV2Typical['images'] = undefined;
for (const result of buildResults) {
if ('images' in result && result.images) {
images = Object.assign({}, images, result.images);
images = { ...images, ...result.images };
}
}
return images;
}
function mergeWildcard(
buildResults: Iterable<BuildResult>
buildResults: Iterable<BuildResult>,
): BuildResultV2Typical['wildcard'] {
let wildcard: BuildResultV2Typical['wildcard'] = undefined;
let wildcard: BuildResultV2Typical['wildcard'];
for (const result of buildResults) {
if ('wildcard' in result && result.wildcard) {
if (!wildcard) wildcard = [];

View File

@@ -1,11 +1,11 @@
import chalk from 'chalk';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import createCertFromFile from '../../util/certs/create-cert-from-file';
import createCertForCns from '../../util/certs/create-cert-for-cns';
import { getCommandName } from '../../util/pkg-name';
import { Cert } from '../../types';
import type Client from '../../util/client';
import type { Cert } from '../../types';
interface Options {
'--overwrite'?: boolean;
@@ -17,7 +17,7 @@ interface Options {
async function add(
client: Client,
opts: Options,
args: string[]
args: string[],
): Promise<number> {
const { output } = client;
const addStamp = stamp();
@@ -41,14 +41,14 @@ async function add(
if (crtPath || keyPath || caPath) {
if (args.length !== 0 || !crtPath || !keyPath || !caPath) {
output.error(
`Invalid number of arguments to create a custom certificate entry. Usage:`
`Invalid number of arguments to create a custom certificate entry. Usage:`,
);
output.print(
` ${chalk.cyan(
`${getCommandName(
'certs add --crt <domain.crt> --key <domain.key> --ca <ca.crt>'
)}`
)}\n`
'certs add --crt <domain.crt> --key <domain.key> --ca <ca.crt>',
)}`,
)}\n`,
);
return 1;
}
@@ -58,18 +58,18 @@ async function add(
} else {
output.warn(
`${chalk.cyan(
getCommandName('certs add')
getCommandName('certs add'),
)} will be soon deprecated. Please use ${chalk.cyan(
getCommandName('certs issue <cn> <cns>')
)} instead`
getCommandName('certs issue <cn> <cns>'),
)} instead`,
);
if (args.length < 1) {
output.error(
`Invalid number of arguments to create a custom certificate entry. Usage:`
`Invalid number of arguments to create a custom certificate entry. Usage:`,
);
output.print(
` ${chalk.cyan(getCommandName('certs add <cn>[, <cn>]'))}\n`
` ${chalk.cyan(getCommandName('certs add <cn>[, <cn>]'))}\n`,
);
return 1;
}
@@ -77,10 +77,10 @@ async function add(
// Create the certificate from the given array of CNs
const cns = args.reduce<string[]>(
(res, item) => res.concat(item.split(',')),
[]
[],
);
output.spinner(
`Generating a certificate for ${chalk.bold(cns.join(', '))}`
`Generating a certificate for ${chalk.bold(cns.join(', '))}`,
);
cert = await createCertForCns(client, cns, contextName);
@@ -90,14 +90,13 @@ async function add(
if (cert instanceof Error) {
output.error(cert.message);
return 1;
} else {
// Print success message
output.success(
`Certificate entry for ${chalk.bold(
cert.cns.join(', ')
)} created ${addStamp()}`
);
}
// Print success message
output.success(
`Certificate entry for ${chalk.bold(
cert.cns.join(', '),
)} created ${addStamp()}`,
);
return 0;
}

View File

@@ -1,18 +1,16 @@
import chalk from 'chalk';
// @ts-ignore
// @ts-expect-error
import { handleError } from '../../util/error';
import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import add from './add';
import issue from './issue';
import ls from './ls';
import rm from './rm';
import Client from '../../util/client';
import { getPkgName } from '../../util/pkg-name';
import type Client from '../../util/client';
const help = () => {
console.log(`
@@ -31,30 +29,30 @@ const help = () => {
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
'FILE',
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
'DIR',
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
'TOKEN',
)} Login token
-S, --scope Set a custom scope
--challenge-only Only show challenges needed to issue a cert
--crt ${chalk.bold.underline('FILE')} Certificate file
--key ${chalk.bold.underline(
'FILE'
'FILE',
)} Certificate key file
--ca ${chalk.bold.underline(
'FILE'
'FILE',
)} CA certificate chain file
-N, --next Show next page of results
${chalk.dim('Examples:')}
${chalk.gray(
''
'',
)} Generate a certificate with the cnames "acme.com" and "www.acme.com"
${chalk.cyan(`$ ${getPkgName()} certs issue acme.com www.acme.com`)}
@@ -64,7 +62,7 @@ const help = () => {
${chalk.cyan(`$ ${getPkgName()} certs rm id`)}
${chalk.gray('')} Paginate results, where ${chalk.dim(
'`1584722256178`'
'`1584722256178`',
)} is the time in milliseconds since the UNIX epoch.
${chalk.cyan(`$ ${getPkgName()} certs ls --next 1584722256178`)}

View File

@@ -1,9 +1,6 @@
import { parse } from 'psl';
import chalk from 'chalk';
import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import Client from '../../util/client';
import createCertForCns from '../../util/certs/create-cert-for-cns';
import createCertFromFile from '../../util/certs/create-cert-from-file';
import dnsTable from '../../util/format-dns-table';
@@ -14,19 +11,21 @@ import stamp from '../../util/output/stamp';
import startCertOrder from '../../util/certs/start-cert-order';
import handleCertError from '../../util/certs/handle-cert-error';
import { getCommandName } from '../../util/pkg-name';
import type Client from '../../util/client';
import type { Output } from '../../util/output';
type Options = {
interface Options {
'--ca': string;
'--challenge-only': boolean;
'--crt': string;
'--key': string;
'--overwrite': boolean;
};
}
export default async function issue(
client: Client,
opts: Partial<Options>,
args: string[]
args: string[],
) {
let cert;
const { output } = client;
@@ -49,14 +48,14 @@ export default async function issue(
if (crtPath || keyPath || caPath) {
if (args.length !== 0 || !crtPath || !keyPath || !caPath) {
output.error(
`Invalid number of arguments to create a custom certificate entry. Usage:`
`Invalid number of arguments to create a custom certificate entry. Usage:`,
);
output.print(
` ${chalk.cyan(
getCommandName(
'certs issue --crt <domain.crt> --key <domain.key> --ca <ca.crt>'
)
)}\n`
'certs issue --crt <domain.crt> --key <domain.key> --ca <ca.crt>',
),
)}\n`,
);
return 1;
}
@@ -72,18 +71,18 @@ export default async function issue(
// Print success message
output.success(
`Certificate entry for ${chalk.bold(
cert.cns.join(', ')
)} created ${addStamp()}`
cert.cns.join(', '),
)} created ${addStamp()}`,
);
return 0;
}
if (args.length < 1) {
output.error(
`Invalid number of arguments to create a custom certificate entry. Usage:`
`Invalid number of arguments to create a custom certificate entry. Usage:`,
);
output.print(
` ${chalk.cyan(getCommandName('certs issue <cn>[, <cn>]'))}\n`
` ${chalk.cyan(getCommandName('certs issue <cn>[, <cn>]'))}\n`,
);
return 1;
}
@@ -120,16 +119,16 @@ export default async function issue(
if (handledResult instanceof ERRORS.DomainPermissionDenied) {
output.error(
`You do not have permissions over domain ${chalk.underline(
handledResult.meta.domain
)} under ${chalk.bold(handledResult.meta.context)}.`
handledResult.meta.domain,
)} under ${chalk.bold(handledResult.meta.context)}.`,
);
return 1;
}
output.success(
`Certificate entry for ${chalk.bold(
handledResult.cns.join(', ')
)} created ${addStamp()}`
handledResult.cns.join(', '),
)} created ${addStamp()}`,
);
return 0;
}
@@ -140,48 +139,48 @@ async function runStartOrder(
cns: string[],
contextName: string,
stamp: () => string,
{ fallingBack = false } = {}
{ fallingBack = false } = {},
) {
const { challengesToResolve } = await startCertOrder(
client,
cns,
contextName
contextName,
);
const pendingChallenges = challengesToResolve.filter(
challenge => challenge.status === 'pending'
(challenge) => challenge.status === 'pending',
);
if (fallingBack) {
output.warn(
`To generate a wildcard certificate for domain for an external domain you must solve challenges manually.`
`To generate a wildcard certificate for domain for an external domain you must solve challenges manually.`,
);
}
if (pendingChallenges.length === 0) {
output.log(
`A certificate issuance for ${chalk.bold(
cns.join(', ')
)} has been started ${stamp()}`
cns.join(', '),
)} has been started ${stamp()}`,
);
output.print(
` There are no pending challenges. Finish the issuance by running: \n`
` There are no pending challenges. Finish the issuance by running: \n`,
);
output.print(
` ${chalk.cyan(getCommandName(`certs issue ${cns.join(' ')}`))}\n`
` ${chalk.cyan(getCommandName(`certs issue ${cns.join(' ')}`))}\n`,
);
return 0;
}
output.log(
`A certificate issuance for ${chalk.bold(
cns.join(', ')
)} has been started ${stamp()}`
cns.join(', '),
)} has been started ${stamp()}`,
);
output.print(
` Add the following TXT records with your registrar to be able to the solve the DNS challenge:\n\n`
` Add the following TXT records with your registrar to be able to the solve the DNS challenge:\n\n`,
);
const [header, ...rows] = dnsTable(
pendingChallenges.map(challenge => {
pendingChallenges.map((challenge) => {
const parsedDomain = parse(challenge.domain);
if (parsedDomain.error) {
throw new ERRORS.InvalidDomain(challenge.domain);
@@ -193,17 +192,17 @@ async function runStartOrder(
'TXT',
challenge.value,
];
})
}),
).split('\n');
output.print(`${header}\n`);
process.stdout.write(`${rows.join('\n')}\n\n`);
output.log(`To issue the certificate once the records are added, run:`);
output.print(
` ${chalk.cyan(getCommandName(`certs issue ${cns.join(' ')}`))}\n`
` ${chalk.cyan(getCommandName(`certs issue ${cns.join(' ')}`))}\n`,
);
output.print(
' Read more: https://err.sh/vercel/solve-challenges-manually\n'
' Read more: https://err.sh/vercel/solve-challenges-manually\n',
);
return 0;
}

View File

@@ -1,14 +1,14 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import getCerts from '../../util/certs/get-certs';
import strlen from '../../util/strlen';
import { Cert } from '../../types';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import type { Cert } from '../../types';
import type Client from '../../util/client';
interface Options {
'--next'?: number;
@@ -17,7 +17,7 @@ interface Options {
async function ls(
client: Client,
opts: Options,
args: string[]
args: string[],
): Promise<number> {
const { output } = client;
const { '--next': nextTimestamp } = opts;
@@ -32,21 +32,21 @@ async function ls(
if (args.length !== 0) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('certs ls')}`
)}`
`${getCommandName('certs ls')}`,
)}`,
);
return 1;
}
// Get the list of certificates
const { certs, pagination } = await getCerts(client, nextTimestamp).catch(
err => err
(err) => err,
);
output.log(
`${
certs.length > 0 ? 'Certificates' : 'No certificates'
} found under ${chalk.bold(contextName)} ${lsStamp()}`
} found under ${chalk.bold(contextName)} ${lsStamp()}`,
);
if (certs.length > 0) {
@@ -57,8 +57,8 @@ async function ls(
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page run ${getCommandName(
`certs ls${flags} --next ${pagination.next}`
)}`
`certs ls${flags} --next ${pagination.next}`,
)}`,
);
}
@@ -72,7 +72,7 @@ function formatCertsTable(certsList: Cert[]) {
align: ['l', 'l', 'r', 'c', 'r'],
hsep: ' '.repeat(2),
stringLength: strlen,
}
},
).replace(/^(.*)/gm, ' $1')}\n`;
}
@@ -90,7 +90,7 @@ function formatCertsTableBody(certsList: Cert[]) {
const now = new Date();
return certsList.reduce<string[][]>(
(result, cert) => result.concat(formatCert(now, cert)),
[]
[],
);
}
@@ -98,7 +98,7 @@ function formatCert(time: Date, cert: Cert) {
return cert.cns.map((cn, idx) =>
idx === 0
? formatCertFirstCn(time, cert, cn, cert.cns.length > 1)
: formatCertNonFirstCn(cn, cert.cns.length > 1)
: formatCertNonFirstCn(cn, cert.cns.length > 1),
);
}
@@ -114,7 +114,7 @@ function formatCertFirstCn(
time: Date,
cert: Cert,
cn: string,
multiple: boolean
multiple: boolean,
): string[] {
return [
cert.uid,

View File

@@ -2,19 +2,19 @@ import chalk from 'chalk';
import ms from 'ms';
import plural from 'pluralize';
import table from 'text-table';
import { Cert } from '../../types';
import * as ERRORS from '../../util/errors-ts';
import { Output } from '../../util/output';
import deleteCertById from '../../util/certs/delete-cert-by-id';
import getCertById from '../../util/certs/get-cert-by-id';
import { getCustomCertsForDomain } from '../../util/certs/get-custom-certs-for-domain';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import param from '../../util/output/param';
import { getCommandName } from '../../util/pkg-name';
import type Client from '../../util/client';
import type { Output } from '../../util/output';
import type { Cert } from '../../types';
type Options = {};
interface Options {}
async function rm(client: Client, opts: Options, args: string[]) {
const rmStamp = stamp();
@@ -24,8 +24,8 @@ async function rm(client: Client, opts: Options, args: string[]) {
if (args.length !== 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('certs rm <id or cn>')}`
)}`
`${getCommandName('certs rm <id or cn>')}`,
)}`,
);
return 1;
}
@@ -34,7 +34,7 @@ async function rm(client: Client, opts: Options, args: string[]) {
const certs = await getCertsToDelete(output, client, contextName, id);
if (certs instanceof ERRORS.CertsPermissionDenied) {
output.error(
`You don't have access to ${param(id)}'s certs under ${contextName}.`
`You don't have access to ${param(id)}'s certs under ${contextName}.`,
);
return 1;
}
@@ -43,12 +43,12 @@ async function rm(client: Client, opts: Options, args: string[]) {
if (id.includes('.')) {
output.error(
`No custom certificates found for "${id}" under ${chalk.bold(
contextName
)}`
contextName,
)}`,
);
} else {
output.error(
`No certificates found by id "${id}" under ${chalk.bold(contextName)}`
`No certificates found by id "${id}" under ${chalk.bold(contextName)}`,
);
}
return 1;
@@ -57,7 +57,7 @@ async function rm(client: Client, opts: Options, args: string[]) {
const yes = await readConfirmation(
output,
'The following certificates will be removed permanently',
certs
certs,
);
if (!yes) {
@@ -65,12 +65,12 @@ async function rm(client: Client, opts: Options, args: string[]) {
}
await Promise.all(
certs.map(cert => deleteCertById(output, client, cert.uid))
certs.map((cert) => deleteCertById(output, client, cert.uid)),
);
output.success(
`${chalk.bold(
plural('Certificate', certs.length, true)
)} removed ${rmStamp()}`
plural('Certificate', certs.length, true),
)} removed ${rmStamp()}`,
);
return 0;
}
@@ -79,7 +79,7 @@ async function getCertsToDelete(
output: Output,
client: Client,
contextName: string,
id: string
id: string,
) {
const cert = await getCertById(client, id);
if (cert instanceof ERRORS.CertNotFound) {
@@ -93,19 +93,19 @@ async function getCertsToDelete(
}
function readConfirmation(output: Output, msg: string, certs: Cert[]) {
return new Promise(resolve => {
return new Promise((resolve) => {
output.log(msg);
output.print(
`${table(certs.map(formatCertRow), {
align: ['l', 'r', 'l'],
hsep: ' '.repeat(6),
}).replace(/^(.*)/gm, ' $1')}\n`
}).replace(/^(.*)/gm, ' $1')}\n`,
);
output.print(
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`,
);
process.stdin
.on('data', d => {
.on('data', (d) => {
process.stdin.pause();
resolve(d.toString().trim().toLowerCase() === 'y');
})

View File

@@ -1,20 +1,20 @@
import { join, resolve, basename } from 'path';
import ms from 'ms';
import fs from 'fs-extra';
import bytes from 'bytes';
import chalk from 'chalk';
import { join, resolve, basename } from 'path';
import {
fileNameSymbol,
VALID_ARCHIVE_FORMATS,
VercelConfig,
} from '@vercel/client';
import { getPrettyError } from '@vercel/build-utils';
import code from '../../util/output/code';
import highlight from '../../util/output/highlight';
import { readLocalConfig } from '../../util/config/files';
import getArgs from '../../util/get-args';
import { handleError } from '../../util/error';
import Client from '../../util/client';
import { getPrettyError } from '@vercel/build-utils';
import toHumanPath from '../../util/humanize-path';
import Now from '../../util';
import stamp from '../../util/output/stamp';
@@ -47,9 +47,7 @@ import {
import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
import confirm from '../../util/input/confirm';
import editProjectSettings, {
PartialProjectSettings,
} from '../../util/input/edit-project-settings';
import editProjectSettings from '../../util/input/edit-project-settings';
import {
getLinkedProject,
linkFolderToProject,
@@ -64,15 +62,16 @@ import validatePaths, {
} from '../../util/validate-paths';
import { getCommandName } from '../../util/pkg-name';
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url';
import { Output } from '../../util/output';
import { help } from './args';
import type { Output } from '../../util/output';
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
import { help } from './args';
import parseTarget from '../../util/deploy/parse-target';
import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
import { createGitMeta } from '../../util/create-git-meta';
import { isValidArchive } from '../../util/deploy/validate-archive-format';
import { parseEnv } from '../../util/parse-env';
import { errorToString, isErrnoException, isError } from '../../util/is-error';
import { pickOverrides } from '../../util/projects/project-settings';
export default async (client: Client): Promise<number> => {
const { output } = client;
@@ -378,14 +377,14 @@ export default async (client: Client): Promise<number> => {
if (
rootDirectory &&
(await validateRootDirectory(
!(await validateRootDirectory(
output,
path,
sourcePath,
project
? `To change your Project Settings, go to https://vercel.com/${org?.slug}/${project.name}/settings`
: ''
)) === false
))
) {
return 1;
}
@@ -463,27 +462,27 @@ export default async (client: Client): Promise<number> => {
}
// build `meta`
const meta = Object.assign(
{},
parseMeta(localConfig.meta),
parseMeta(argv['--meta'])
);
const meta = {
...parseMeta(localConfig.meta),
...parseMeta(argv['--meta'])
};
const gitMetadata = await createGitMeta(path, output, project);
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
const deploymentEnv = Object.assign(
{},
parseEnv(localConfig.env),
parseEnv(argv['--env'])
);
const deploymentEnv = {
...parseEnv(localConfig.env),
...parseEnv(argv['--env'])
};
// Merge build env out of `build.env` from vercel.json, and `--build-env` args
const deploymentBuildEnv = Object.assign(
{},
parseEnv(localConfig.build && localConfig.build.env),
parseEnv(argv['--build-env'])
);
const deploymentBuildEnv = {
...parseEnv(localConfig.build && localConfig.build.env),
...parseEnv(argv['--build-env'])
};
// If there's any undefined values, then inherit them from this process
try {
@@ -501,7 +500,7 @@ export default async (client: Client): Promise<number> => {
.filter(Boolean);
const regions = regionFlag.length > 0 ? regionFlag : localConfig.regions;
const currentTeam = org?.type === 'team' ? org.id : undefined;
const currentTeam = org.type === 'team' ? org.id : undefined;
const now = new Now({
client,
currentTeam,
@@ -509,14 +508,7 @@ export default async (client: Client): Promise<number> => {
let deployStamp = stamp();
let deployment = null;
const localConfigurationOverrides: PartialProjectSettings = {
buildCommand: localConfig?.buildCommand,
devCommand: localConfig?.devCommand,
framework: localConfig?.framework,
commandForIgnoringBuildStep: localConfig?.ignoreCommand,
installCommand: localConfig?.installCommand,
outputDirectory: localConfig?.outputDirectory,
};
const localConfigurationOverrides = pickOverrides(localConfig);
try {
const createArgs: any = {
@@ -530,7 +522,12 @@ export default async (client: Client): Promise<number> => {
quiet,
wantsPublic: argv['--public'] || localConfig.public,
type: null,
nowConfig: localConfig,
nowConfig: {
...localConfig,
// `images` is allowed in "vercel.json" and processed
// by `vc build`, but don't send it to the API endpoint
images: undefined,
},
regions,
meta,
gitMetadata,
@@ -562,7 +559,7 @@ export default async (client: Client): Promise<number> => {
);
if (deployment.code === 'missing_project_settings') {
let { projectSettings, framework } = deployment;
const { projectSettings, framework } = deployment;
if (rootDirectory) {
projectSettings.rootDirectory = rootDirectory;
}
@@ -900,12 +897,12 @@ const printDeploymentStatus = async (
}
output.print(
prependEmoji(
`${prependEmoji(
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
previewUrl
)} ${deployStamp()}`,
emoji('success')
) + `\n`
) }\n`
);
}
@@ -919,7 +916,7 @@ const printDeploymentStatus = async (
}
const newline = '\n';
for (let indication of indications) {
for (const indication of indications) {
const message =
prependEmoji(chalk.dim(indication.payload), emoji(indication.type)) +
newline;

View File

@@ -1,29 +1,27 @@
import { resolve, join } from 'path';
import fs from 'fs-extra';
import DevServer from '../../util/dev/server';
import parseListen from '../../util/dev/parse-listen';
import { ProjectEnvVariable } from '../../types';
import Client from '../../util/client';
import { parseListen } from '../../util/dev/parse-listen';
import { getLinkedProject } from '../../util/projects/link';
import { getFrameworks } from '../../util/get-frameworks';
import { ProjectSettings } from '../../types';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
import setupAndLink from '../../util/link/setup-and-link';
import getSystemEnvValues from '../../util/env/get-system-env-values';
import { getCommandName } from '../../util/pkg-name';
import param from '../../util/output/param';
import { OUTPUT_DIR } from '../../util/build/write-build-result';
import type { ProjectSettings } from '../../types';
import type Client from '../../util/client';
import type { ProjectEnvVariable } from '../../types';
type Options = {
interface Options {
'--listen': string;
'--yes': boolean;
};
}
export default async function dev(
client: Client,
opts: Partial<Options>,
args: string[]
args: string[],
) {
const { output } = client;
const [dir = '.'] = args;
@@ -31,10 +29,7 @@ export default async function dev(
const listen = parseListen(opts['--listen'] || '3000');
// retrieve dev command
let [link, frameworks] = await Promise.all([
getLinkedProject(client, cwd),
getFrameworks(client),
]);
let link = await getLinkedProject(client, cwd);
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
link = await setupAndLink(client, cwd, {
@@ -53,14 +48,13 @@ export default async function dev(
if (link.reason === 'HEADLESS') {
client.output.error(
`Command ${getCommandName(
'dev'
)} requires confirmation. Use option ${param('--yes')} to confirm.`
'dev',
)} requires confirmation. Use option ${param('--yes')} to confirm.`,
);
}
return link.exitCode;
}
let devCommand: string | undefined;
let projectSettings: ProjectSettings | undefined;
let projectEnvs: ProjectEnvVariable[] = [];
let systemEnvValues: string[] = [];
@@ -70,19 +64,6 @@ export default async function dev(
projectSettings = project;
if (project.devCommand) {
devCommand = project.devCommand;
} else if (project.framework) {
const framework = frameworks.find(f => f.slug === project.framework);
if (framework) {
const defaults = framework.settings.devCommand.value;
if (defaults) {
devCommand = defaults;
}
}
}
if (project.rootDirectory) {
cwd = join(cwd, project.rootDirectory);
}
@@ -95,16 +76,17 @@ export default async function dev(
]);
}
// This is just for tests - can be removed once project settings
// are respected locally in `.vercel/project.json`
if (process.env.VERCEL_DEV_COMMAND) {
devCommand = process.env.VERCEL_DEV_COMMAND;
}
const devServer = new DevServer(cwd, {
output,
projectSettings,
projectEnvs,
systemEnvValues,
});
// 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) {
if (!devServer.devCommand) {
const outputDir = join(cwd, OUTPUT_DIR);
if (await fs.pathExists(outputDir)) {
output.log(`Removing ${OUTPUT_DIR}`);
@@ -112,13 +94,5 @@ export default async function dev(
}
}
const devServer = new DevServer(cwd, {
output,
devCommand,
projectSettings,
projectEnvs,
systemEnvValues,
});
await devServer.start(...listen);
}

View File

@@ -1,21 +1,20 @@
import path from 'path';
import chalk from 'chalk';
import { PackageJson } from '@vercel/build-utils';
import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import Client from '../../util/client';
import { NowError } from '../../util/now-error';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import cmd from '../../util/output/cmd';
import highlight from '../../util/output/highlight';
import dev from './dev';
import readConfig from '../../util/config/read-config';
import readJSONFile from '../../util/read-json-file';
import { getPkgName, getCommandName } from '../../util/pkg-name';
import { CantParseJSONFile } from '../../util/errors-ts';
import { isErrnoException } from '../../util/is-error';
import dev from './dev';
import type Client from '../../util/client';
import type { PackageJson } from '@vercel/build-utils';
const COMMAND_CONFIG = {
dev: ['dev'],
@@ -42,7 +41,7 @@ const help = () => {
${chalk.cyan(`$ ${getPkgName()} dev --listen 8080`)}
${chalk.gray(
''
'',
)} Make the \`vercel dev\` server bind to localhost on port 5000
${chalk.cyan(`$ ${getPkgName()} dev --listen 127.0.0.1:5000`)}
@@ -53,18 +52,17 @@ export default async function main(client: Client) {
if (process.env.__VERCEL_DEV_RUNNING) {
client.output.error(
`${cmd(
`${getPkgName()} dev`
`${getPkgName()} dev`,
)} must not recursively invoke itself. Check the Development Command in the Project Settings or the ${cmd(
'dev'
)} script in ${cmd('package.json')}`
'dev',
)} script in ${cmd('package.json')}`,
);
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
`Learn More: https://vercel.link/recursive-invocation-of-commands`,
);
return 1;
} else {
process.env.__VERCEL_DEV_RUNNING = '1';
}
process.env.__VERCEL_DEV_RUNNING = '1';
let argv;
let args;
@@ -125,13 +123,13 @@ export default async function main(client: Client) {
if (/\b(now|vercel)\b\W+\bdev\b/.test(pkg?.scripts?.dev || '')) {
client.output.error(
`${cmd(
`${getPkgName()} dev`
`${getPkgName()} dev`,
)} must not recursively invoke itself. Check the Development Command in the Project Settings or the ${cmd(
'dev'
)} script in ${cmd('package.json')}`
'dev',
)} script in ${cmd('package.json')}`,
);
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
`Learn More: https://vercel.link/recursive-invocation-of-commands`,
);
return 1;
}
@@ -153,8 +151,8 @@ export default async function main(client: Client) {
const hostname = matches[1];
output.error(
`The hostname ${highlight(
hostname
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
hostname,
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`,
);
}
if (typeof err.stack === 'string') {

View File

@@ -6,19 +6,19 @@ import {
DNSInvalidType,
} from '../../util/errors-ts';
import addDNSRecord from '../../util/dns/add-dns-record';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import parseAddDNSRecordArgs from '../../util/dns/parse-add-dns-record-args';
import stamp from '../../util/output/stamp';
import getDNSData from '../../util/dns/get-dns-data';
import { getCommandName } from '../../util/pkg-name';
import type Client from '../../util/client';
type Options = {};
interface Options {}
export default async function add(
client: Client,
opts: Options,
args: string[]
args: string[],
) {
const { output } = client;
const { contextName } = await getScope(client);
@@ -27,8 +27,8 @@ export default async function add(
if (!parsedParams) {
output.error(
`Invalid number of arguments. See: ${chalk.cyan(
`${getCommandName('dns --help')}`
)} for usage.`
`${getCommandName('dns --help')}`,
)} for usage.`,
);
return 1;
}
@@ -45,8 +45,8 @@ export default async function add(
if (record instanceof DomainNotFound) {
output.error(
`The domain ${domain} can't be found under ${chalk.bold(
contextName
)} ${chalk.gray(addStamp())}`
contextName,
)} ${chalk.gray(addStamp())}`,
);
return 1;
}
@@ -54,8 +54,8 @@ export default async function add(
if (record instanceof DNSPermissionDenied) {
output.error(
`You don't have permissions to add records to domain ${domain} under ${chalk.bold(
contextName
)} ${chalk.gray(addStamp())}`
contextName,
)} ${chalk.gray(addStamp())}`,
);
return 1;
}
@@ -63,8 +63,8 @@ export default async function add(
if (record instanceof DNSInvalidPort) {
output.error(
`Invalid <port> parameter. A number was expected ${chalk.gray(
addStamp()
)}`
addStamp(),
)}`,
);
return 1;
}
@@ -74,8 +74,8 @@ export default async function add(
`Invalid <type> parameter "${
record.meta.type
}". Expected one of A, AAAA, ALIAS, CAA, CNAME, MX, SRV, TXT ${chalk.gray(
addStamp()
)}`
addStamp(),
)}`,
);
return 1;
}
@@ -87,10 +87,10 @@ export default async function add(
console.log(
`${chalk.cyan('> Success!')} DNS record for domain ${chalk.bold(
domain
domain,
)} ${chalk.gray(`(${record.uid})`)} created under ${chalk.bold(
contextName
)} ${chalk.gray(addStamp())}`
contextName,
)} ${chalk.gray(addStamp())}`,
);
return 0;

View File

@@ -1,17 +1,17 @@
import chalk from 'chalk';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import { DomainNotFound, InvalidDomain } from '../../util/errors-ts';
import stamp from '../../util/output/stamp';
import importZonefile from '../../util/dns/import-zonefile';
import { getCommandName } from '../../util/pkg-name';
import type Client from '../../util/client';
type Options = {};
interface Options {}
export default async function add(
client: Client,
opts: Options,
args: string[]
args: string[],
) {
const { output } = client;
const { contextName } = await getScope(client);
@@ -19,8 +19,8 @@ export default async function add(
if (args.length !== 2) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('dns import <domain> <zonefile>')}`
)}`
`${getCommandName('dns import <domain> <zonefile>')}`,
)}`,
);
return 1;
}
@@ -32,13 +32,13 @@ export default async function add(
client,
contextName,
domain,
zonefilePath
zonefilePath,
);
if (recordIds instanceof DomainNotFound) {
output.error(
`The domain ${domain} can't be found under ${chalk.bold(
contextName
)} ${chalk.gray(addStamp())}`
contextName,
)} ${chalk.gray(addStamp())}`,
);
return 1;
}
@@ -46,8 +46,8 @@ export default async function add(
if (recordIds instanceof InvalidDomain) {
output.error(
`The domain ${domain} doesn't match with the one found in the Zone file ${chalk.gray(
addStamp()
)}`
addStamp(),
)}`,
);
return 1;
}
@@ -56,8 +56,8 @@ export default async function add(
`${chalk.cyan('> Success!')} ${
recordIds.length
} DNS records for domain ${chalk.bold(domain)} created under ${chalk.bold(
contextName
)} ${chalk.gray(addStamp())}`
contextName,
)} ${chalk.gray(addStamp())}`,
);
return 0;
}

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