Compare commits

..

10 Commits

Author SHA1 Message Date
Vercel Release Bot
c6d588201b Version Packages (#11322)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @vercel/build-utils@7.10.0

### Minor Changes

- Allow environment variables to be specified for `EdgeFunction`
([#11029](https://github.com/vercel/vercel/pull/11029))

## vercel@33.6.2

### Patch Changes

- Added sunset warning to secrets command.
([#11333](https://github.com/vercel/vercel/pull/11333))

- Swap jest for vitest in CLI unit tests
([#11302](https://github.com/vercel/vercel/pull/11302))

- Updated dependencies
\[[`988f7b75a`](988f7b75a2),
[`1825b58df`](1825b58df8)]:
    -   @vercel/remix-builder@2.1.5
    -   @vercel/build-utils@7.10.0
    -   @vercel/node@3.0.25
    -   @vercel/static-build@2.4.5

## @vercel/client@13.1.8

### Patch Changes

- Updated dependencies
\[[`1825b58df`](1825b58df8)]:
    -   @vercel/build-utils@7.10.0

## @vercel/frameworks@3.0.1

### Patch Changes

- Swap jest for vitest in CLI unit tests
([#11302](https://github.com/vercel/vercel/pull/11302))

## @vercel/fs-detectors@5.2.2

### Patch Changes

- Updated dependencies
\[[`9ed967034`](9ed967034d)]:
    -   @vercel/frameworks@3.0.1

## @vercel/gatsby-plugin-vercel-builder@2.0.23

### Patch Changes

- Updated dependencies
\[[`1825b58df`](1825b58df8)]:
    -   @vercel/build-utils@7.10.0

## @vercel/node@3.0.25

### Patch Changes

- Updated dependencies
\[[`1825b58df`](1825b58df8)]:
    -   @vercel/build-utils@7.10.0

## @vercel/remix-builder@2.1.5

### Patch Changes

- Add `mjs` and `mts` extensions to vite detection
([#11307](https://github.com/vercel/vercel/pull/11307))

## @vercel/static-build@2.4.5

### Patch Changes

-   Updated dependencies \[]:
    -   @vercel/gatsby-plugin-vercel-builder@2.0.23

## @vercel-internals/types@1.0.28

### Patch Changes

- Updated dependencies
\[[`1825b58df`](1825b58df8)]:
    -   @vercel/build-utils@7.10.0

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-28 08:43:15 -05:00
Ethan Arrowood
0b1ff4bf62 [next] add x-forwarded-port test (#11254)
This test reproduces the issue described in https://github.com/vercel/next.js/issues/61133 . Unfortunately, I can only reproduce this issue when deploying to Vercel. Any local server / test results in the header being defined. 

Related: https://github.com/vercel/next.js/issues/61133
2024-03-28 08:20:18 +00:00
dependabot[bot]
14102ddf56 [framework-fixtures]: Bump the core group in /packages/static-build/test/fixtures/astro-v4 with 1 update (#11335)
Bumps the core group in /packages/static-build/test/fixtures/astro-v4 with 1 update: [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro).

Updates `astro` from 4.2.0 to 4.5.10
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/withastro/astro/releases">astro's releases</a>.</em></p>
<blockquote>
<h2>astro@4.5.10</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10549">#10549</a> <a href="54c2f9707f"><code>54c2f9707f5d038630143f769e3075c698474654</code></a> Thanks <a href="https://github.com/admirsaheta"><code>@​admirsaheta</code></a>! - Updates the <code>HTMLAttributes</code> type exported from <code>astro</code> to allow data attributes</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10562">#10562</a> <a href="348c1ca132"><code>348c1ca1323d0516c2dcf8e963343cd12cb5407f</code></a> Thanks <a href="https://github.com/apetta"><code>@​apetta</code></a>! - Fixes minor type issues inside the built-in components of Astro</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10550">#10550</a> <a href="34fa8e131b"><code>34fa8e131b85531e6629390307108ffc4adb7ed1</code></a> Thanks <a href="https://github.com/Skn0tt"><code>@​Skn0tt</code></a>! - Fixes bug where server builds would include unneeded assets in SSR Function, potentially leading to upload errors on Vercel, Netlify because of size limits</p>
</li>
<li>
<p>Updated dependencies [<a href="c585528f44"><code>c585528f446ccca3d4c643f4af5d550b93c18902</code></a>]:</p>
<ul>
<li><code>@​astrojs/markdown-remark</code><a href="https://github.com/4"><code>@​4</code></a>.3.2</li>
</ul>
</li>
</ul>
<h2>astro@4.5.9</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10532">#10532</a> <a href="8306ce1ff7"><code>8306ce1ff7b71a2a0d7908336c9be462a54d395a</code></a> Thanks <a href="https://github.com/martrapp"><code>@​martrapp</code></a>! - Fixes a style issue of <code>client:only</code> components in DEV mode during view transitions.</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10473">#10473</a> <a href="627e47d67a"><code>627e47d67af4846cea2acf26a96b4124001b26fc</code></a> Thanks <a href="https://github.com/bluwy"><code>@​bluwy</code></a>! - Fixes and improves performance when rendering Astro JSX</p>
</li>
</ul>
<h2>astro@4.5.8</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10504">#10504</a> <a href="8e4e554cc2"><code>8e4e554cc211e59c329c0a5d110c839c886ff120</code></a> Thanks <a href="https://github.com/Princesseuh"><code>@​Princesseuh</code></a>! - Update Babel version to fix regression in Babel's <code>7.24.2</code>.</p>
</li>
<li>
<p>Updated dependencies [<a href="19e42c3681"><code>19e42c368184013fc30d1e46753b9e9383bb2bdf</code></a>]:</p>
<ul>
<li><code>@​astrojs/markdown-remark</code><a href="https://github.com/4"><code>@​4</code></a>.3.1</li>
</ul>
</li>
</ul>
<h2>astro@4.5.7</h2>
<h3>Patch Changes</h3>
<ul>
<li><a href="https://redirect.github.com/withastro/astro/pull/10493">#10493</a> <a href="e4a6462751"><code>e4a6462751725878bfe47632eeafa6854cad5bf2</code></a> Thanks <a href="https://github.com/firefoxic"><code>@​firefoxic</code></a>! - <code>&lt;link&gt;</code> tags created by astro for optimized stylesheets now do not include the closing forward slash. This slash is optional for void elements such as link, but made some html validation fail.</li>
</ul>
<h2>astro@4.5.6</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10455">#10455</a> <a href="c12666166d"><code>c12666166db724915e42e37a048483c99f88e6d9</code></a> Thanks <a href="https://github.com/lilnasy"><code>@​lilnasy</code></a>! - Adds a helpful error message that will be shown when an endpoint does not return a <code>Response</code>.</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10426">#10426</a> <a href="6a9a35ee15"><code>6a9a35ee15069541c3144012385366a3c689240a</code></a> Thanks <a href="https://github.com/markgaze"><code>@​markgaze</code></a>! - Fixes an issue with generating JSON schemas when the schema is a function</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10448">#10448</a> <a href="fcece36586"><code>fcece3658697248ab58f77b3d4a8b14d362f3c47</code></a> Thanks <a href="https://github.com/lilnasy"><code>@​lilnasy</code></a>! - Fixes an issue where multiple rendering errors resulted in a crash of the SSR app server.</p>
</li>
</ul>
<h2>astro@4.5.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10379">#10379</a> <a href="3776ecf0aa"><code>3776ecf0aa9e08a992d3ae76e90682fd04093721</code></a> Thanks <a href="https://github.com/1574242600"><code>@​1574242600</code></a>! - Fixes a routing issue with partially truncated dynamic segments.</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10442">#10442</a> <a href="f8e0ad3c52"><code>f8e0ad3c52a37b8a2175fe2f5ff2bd0cd738f499</code></a> Thanks <a href="https://github.com/Princesseuh"><code>@​Princesseuh</code></a>! - Fixes small rendering issues with the dev toolbar in certain contexts</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10438">#10438</a> <a href="5b48cc0fc8"><code>5b48cc0fc8383b0659a595afd3a6ee28b28779c3</code></a> Thanks <a href="https://github.com/bholmesdev"><code>@​bholmesdev</code></a>! - Generate Astro DB types when running <code>astro sync</code>.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md">astro's changelog</a>.</em></p>
<blockquote>
<h2>4.5.10</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10549">#10549</a> <a href="54c2f9707f"><code>54c2f9707f5d038630143f769e3075c698474654</code></a> Thanks <a href="https://github.com/admirsaheta"><code>@​admirsaheta</code></a>! - Updates the <code>HTMLAttributes</code> type exported from <code>astro</code> to allow data attributes</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10562">#10562</a> <a href="348c1ca132"><code>348c1ca1323d0516c2dcf8e963343cd12cb5407f</code></a> Thanks <a href="https://github.com/apetta"><code>@​apetta</code></a>! - Fixes minor type issues inside the built-in components of Astro</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10550">#10550</a> <a href="34fa8e131b"><code>34fa8e131b85531e6629390307108ffc4adb7ed1</code></a> Thanks <a href="https://github.com/Skn0tt"><code>@​Skn0tt</code></a>! - Fixes bug where server builds would include unneeded assets in SSR Function, potentially leading to upload errors on Vercel, Netlify because of size limits</p>
</li>
<li>
<p>Updated dependencies [<a href="c585528f44"><code>c585528f446ccca3d4c643f4af5d550b93c18902</code></a>]:</p>
<ul>
<li><code>@​astrojs/markdown-remark</code><a href="https://github.com/4"><code>@​4</code></a>.3.2</li>
</ul>
</li>
</ul>
<h2>4.5.9</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10532">#10532</a> <a href="8306ce1ff7"><code>8306ce1ff7b71a2a0d7908336c9be462a54d395a</code></a> Thanks <a href="https://github.com/martrapp"><code>@​martrapp</code></a>! - Fixes a style issue of <code>client:only</code> components in DEV mode during view transitions.</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10473">#10473</a> <a href="627e47d67a"><code>627e47d67af4846cea2acf26a96b4124001b26fc</code></a> Thanks <a href="https://github.com/bluwy"><code>@​bluwy</code></a>! - Fixes and improves performance when rendering Astro JSX</p>
</li>
</ul>
<h2>4.5.8</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10504">#10504</a> <a href="8e4e554cc2"><code>8e4e554cc211e59c329c0a5d110c839c886ff120</code></a> Thanks <a href="https://github.com/Princesseuh"><code>@​Princesseuh</code></a>! - Update Babel version to fix regression in Babel's <code>7.24.2</code>.</p>
</li>
<li>
<p>Updated dependencies [<a href="19e42c3681"><code>19e42c368184013fc30d1e46753b9e9383bb2bdf</code></a>]:</p>
<ul>
<li><code>@​astrojs/markdown-remark</code><a href="https://github.com/4"><code>@​4</code></a>.3.1</li>
</ul>
</li>
</ul>
<h2>4.5.7</h2>
<h3>Patch Changes</h3>
<ul>
<li><a href="https://redirect.github.com/withastro/astro/pull/10493">#10493</a> <a href="e4a6462751"><code>e4a6462751725878bfe47632eeafa6854cad5bf2</code></a> Thanks <a href="https://github.com/firefoxic"><code>@​firefoxic</code></a>! - <code>&lt;link&gt;</code> tags created by astro for optimized stylesheets now do not include the closing forward slash. This slash is optional for void elements such as link, but made some html validation fail.</li>
</ul>
<h2>4.5.6</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10455">#10455</a> <a href="c12666166d"><code>c12666166db724915e42e37a048483c99f88e6d9</code></a> Thanks <a href="https://github.com/lilnasy"><code>@​lilnasy</code></a>! - Adds a helpful error message that will be shown when an endpoint does not return a <code>Response</code>.</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10426">#10426</a> <a href="6a9a35ee15"><code>6a9a35ee15069541c3144012385366a3c689240a</code></a> Thanks <a href="https://github.com/markgaze"><code>@​markgaze</code></a>! - Fixes an issue with generating JSON schemas when the schema is a function</p>
</li>
<li>
<p><a href="https://redirect.github.com/withastro/astro/pull/10448">#10448</a> <a href="fcece36586"><code>fcece3658697248ab58f77b3d4a8b14d362f3c47</code></a> Thanks <a href="https://github.com/lilnasy"><code>@​lilnasy</code></a>! - Fixes an issue where multiple rendering errors resulted in a crash of the SSR app server.</p>
</li>
</ul>
<h2>4.5.5</h2>
<h3>Patch Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="bb7707e171"><code>bb7707e</code></a> [ci] release (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10556">#10556</a>)</li>
<li><a href="348c1ca132"><code>348c1ca</code></a> Patch for Astro VS Code Plugin type errors (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10562">#10562</a>)</li>
<li><a href="51a4ea5f72"><code>51a4ea5</code></a> fix(types): Fixes HTMLAttribute extend to allow everything (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10561">#10561</a>)</li>
<li><a href="34fa8e131b"><code>34fa8e1</code></a> fix: ensure outDir doesn't lead to bundlers bloating Lambda size (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10550">#10550</a>)</li>
<li><a href="54c2f9707f"><code>54c2f97</code></a> Fix: Type error on <code>\&lt;Image /&gt;</code> component when strict TypeScript rules are use...</li>
<li><a href="1cd2a74022"><code>1cd2a74</code></a> [ci] release (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10522">#10522</a>)</li>
<li><a href="8306ce1ff7"><code>8306ce1</code></a> Fixes a style issue of <code>client:only</code> components in DEV mode during view trans...</li>
<li><a href="8cac744746"><code>8cac744</code></a> Avoid generators in render head API (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10528">#10528</a>)</li>
<li><a href="ca5455af3d"><code>ca5455a</code></a> Avoid generators when crawling module graph (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10529">#10529</a>)</li>
<li><a href="627e47d67a"><code>627e47d</code></a> Improve Astro JSX rendering (<a href="https://github.com/withastro/astro/tree/HEAD/packages/astro/issues/10473">#10473</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/withastro/astro/commits/astro@4.5.10/packages/astro">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astro&package-manager=npm_and_yarn&previous-version=4.2.0&new-version=4.5.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2024-03-28 08:17:04 +00:00
Marc Greenstock
49f88975b9 IAM 1720 Added sunset warning to the secrets command (#11333)
This PR adds a warning to the secrets command that the command will be sunset on 1st May.
2024-03-28 08:13:50 +00:00
Vercel Release Bot
13f3db06a8 [tests] Upgrade Turbo to version 1.13.0 (#11325)
This auto-generated PR updates Turbo to version 1.13.0
2024-03-28 08:10:25 +00:00
Erika Rowland
78e2c012f9 [cli] extract isZeroConfigBuild into utility function (#11316)
An additional check was added to make sure that `projectSettings.nodeVersion` is sent when builds property is defined in `vercel.json`
2024-03-28 08:06:10 +00:00
kaifulee
f707f13821 chore: remove repetitive words (#11284) 2024-03-28 08:03:01 +00:00
Jeff See
9ed967034d [cli] Switch unit tests from jest to vitest (#11302) 2024-03-27 17:33:27 -07:00
Nathan Rajlich
1825b58df8 [build-utils] Allow environment variables to be specified for EdgeFunction (#11029) 2024-03-27 21:36:29 +00:00
Nathan Rajlich
c82a55c460 [tests] Use AL2 build container for relevant e2e tests (#11329)
* Forces the AL2 build container image for fixtures that depend on it,
via `engines.node` in package.json for most cases.
* The `testDeployment()` function was updated to send
`projectSettings.nodeVersion` in the POST body, to mimic the behavior in
CLI.
* For Go, Ruby, and Python tests, the `projectSettings.nodeVersion`
property is set "globally" in the Jest setup file, so that individual
fixtures didn't need to be adjusted.
2024-03-27 12:19:30 -07:00
207 changed files with 2534 additions and 2116 deletions

View File

@@ -1,2 +0,0 @@
---
---

View File

@@ -1,2 +0,0 @@
---
---

View File

@@ -1,2 +0,0 @@
---
---

View File

@@ -1,3 +0,0 @@
---
---

View File

@@ -1,5 +0,0 @@
---
'vercel': patch
---
Replace `inquirer` with `@inquirer/prompts`

View File

@@ -1,5 +0,0 @@
---
"@vercel/remix-builder": patch
---
Add `mjs` and `mts` extensions to vite detection

View File

@@ -1,2 +0,0 @@
---
---

View File

@@ -1,4 +0,0 @@
---
---
Update CODEOWNERS

View File

@@ -1,2 +0,0 @@
---
---

View File

@@ -4,7 +4,7 @@ import { lstatSync, readdirSync } from 'fs';
export async function deployExample(filename: string) {
const { testDeployment } = require('../../test/lib/deployment/test-deployment.js');
const example = basename(filename).replace(/\.test\.ts$/, '');
await testDeployment(join(filename, '..', '..', '..', example));
await testDeployment(join(process.cwd(), example));
}
export function getExamples() {

View File

@@ -8,6 +8,6 @@ menu:
main: {}
---
This is an example of a custom shortcode that you can put right into your content. You will need to add a form action to the the shortcode to make it work. Check out [Formspree](https://formspree.io/) for a simple, free form service.
This is an example of a custom shortcode that you can put right into your content. You will need to add a form action to the shortcode to make it work. Check out [Formspree](https://formspree.io/) for a simple, free form service.
{{< form-contact action="https://example.com" >}}

View File

@@ -9,7 +9,8 @@
},
"devDependencies": {
"@types/jest": "27.4.1",
"@vercel/frameworks": "3.0.0"
"@vercel/build-utils": "7.10.0",
"@vercel/frameworks": "3.0.1"
},
"version": null
}

View File

@@ -1,5 +1,12 @@
# @vercel-internals/types
## 1.0.28
### Patch Changes
- Updated dependencies [[`1825b58df`](https://github.com/vercel/vercel/commit/1825b58df8d783e79f0addf262618f422246f4b3)]:
- @vercel/build-utils@7.10.0
## 1.0.27
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@vercel-internals/types",
"version": "1.0.27",
"version": "1.0.28",
"types": "index.d.ts",
"main": "index.d.ts",
"files": [
@@ -10,7 +10,7 @@
"dependencies": {
"@types/node": "14.14.31",
"@vercel-internals/constants": "1.0.4",
"@vercel/build-utils": "7.9.1",
"@vercel/build-utils": "7.10.0",
"@vercel/routing-utils": "3.1.0"
},
"devDependencies": {

View File

@@ -33,7 +33,7 @@
"source-map-support": "0.5.12",
"ts-eager": "2.0.2",
"ts-jest": "29.1.0",
"turbo": "1.12.5",
"turbo": "1.13.0",
"typescript": "4.9.5"
},
"scripts": {

View File

@@ -1,5 +1,11 @@
# @vercel/build-utils
## 7.10.0
### Minor Changes
- Allow environment variables to be specified for `EdgeFunction` ([#11029](https://github.com/vercel/vercel/pull/11029))
## 7.9.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "7.9.1",
"version": "7.10.0",
"license": "Apache-2.0",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -1,4 +1,4 @@
import type { Files, FunctionFramework } from './types';
import type { Env, Files, FunctionFramework } from './types';
/**
* An Edge Functions output
@@ -23,6 +23,11 @@ export class EdgeFunction {
*/
entrypoint: string;
/**
* Environment variables for the edge function to use at runtime.
*/
environment?: Env;
/**
* The list of files to be included in the edge function bundle.
*/
@@ -48,5 +53,6 @@ export class EdgeFunction {
this.assets = params.assets;
this.regions = params.regions;
this.framework = params.framework;
this.environment = params.environment;
}
}

View File

@@ -5,11 +5,7 @@ import minimatch from 'minimatch';
import { readlink } from 'fs-extra';
import { isSymbolicLink, isDirectory } from './fs/download';
import streamToBuffer from './fs/stream-to-buffer';
import type { Files, Config, FunctionFramework } from './types';
interface Environment {
[key: string]: string;
}
import type { Config, Env, Files, FunctionFramework } from './types';
export type LambdaOptions = LambdaOptionsWithFiles | LambdaOptionsWithZipBuffer;
@@ -21,7 +17,7 @@ export interface LambdaOptionsBase {
architecture?: LambdaArchitecture;
memory?: number;
maxDuration?: number;
environment?: Environment;
environment?: Env;
allowQuery?: string[];
regions?: string[];
supportsMultiPayloads?: boolean;
@@ -69,7 +65,7 @@ export class Lambda {
architecture?: LambdaArchitecture;
memory?: number;
maxDuration?: number;
environment: Environment;
environment: Env;
allowQuery?: string[];
regions?: string[];
/**

View File

@@ -3,5 +3,8 @@
"version": "1.0.0",
"scripts": {
"build": "mkdir -p public && (printf \"npm version: \" && npm -v) > public/index.txt"
},
"engines": {
"node": "16.x"
}
}

View File

@@ -5,5 +5,8 @@
},
"dependencies": {
"once": "^1.4.0"
},
"engines": {
"node": "16.x"
}
}

View File

@@ -6,5 +6,8 @@
"dependencies": {
"exeggcute": "^1.0.0",
"once": "^1.4.0"
},
"engines": {
"node": "16.x"
}
}

View File

@@ -5,5 +5,8 @@
},
"dependencies": {
"sharp": "0.33.2"
},
"engines": {
"node": "16.x"
}
}

View File

@@ -1,5 +1,19 @@
# vercel
## 33.6.2
### Patch Changes
- Added sunset warning to secrets command. ([#11333](https://github.com/vercel/vercel/pull/11333))
- Swap jest for vitest in CLI unit tests ([#11302](https://github.com/vercel/vercel/pull/11302))
- Updated dependencies [[`988f7b75a`](https://github.com/vercel/vercel/commit/988f7b75a27387e84fce541b844f984d2c151980), [`1825b58df`](https://github.com/vercel/vercel/commit/1825b58df8d783e79f0addf262618f422246f4b3)]:
- @vercel/remix-builder@2.1.5
- @vercel/build-utils@7.10.0
- @vercel/node@3.0.25
- @vercel/static-build@2.4.5
## 33.6.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "33.6.1",
"version": "33.6.2",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -12,7 +12,7 @@
},
"scripts": {
"test": "jest --reporters=default --reporters=jest-junit --env node --verbose --bail",
"test-unit": "pnpm test test/unit/",
"test-unit": "pnpm vitest test/unit/",
"test-e2e": "rimraf test/fixtures/integration && pnpm test test/integration-1.test.ts test/integration-2.test.ts test/integration-3.test.ts",
"test-dev": "pnpm test test/dev/",
"coverage": "codecov",
@@ -31,25 +31,22 @@
"node": ">= 16"
},
"dependencies": {
"@vercel/build-utils": "7.9.1",
"@vercel/build-utils": "7.10.0",
"@vercel/fun": "1.1.0",
"@vercel/go": "3.0.5",
"@vercel/hydrogen": "1.0.2",
"@vercel/next": "4.1.6",
"@vercel/node": "3.0.24",
"@vercel/node": "3.0.25",
"@vercel/python": "4.1.1",
"@vercel/redwood": "2.0.8",
"@vercel/remix-builder": "2.1.4",
"@vercel/remix-builder": "2.1.5",
"@vercel/ruby": "2.0.5",
"@vercel/static-build": "2.4.4",
"@vercel/static-build": "2.4.5",
"chokidar": "3.3.1"
},
"devDependencies": {
"@alex_neo/jest-expect-message": "1.0.5",
"@edge-runtime/node-utils": "2.3.0",
"@inquirer/core": "7.1.0",
"@inquirer/prompts": "4.3.0",
"@inquirer/type": "1.2.1",
"@next/env": "11.1.2",
"@sentry/node": "5.5.0",
"@sindresorhus/slugify": "0.11.0",
@@ -91,12 +88,13 @@
"@types/yauzl-promise": "2.1.0",
"@vercel-internals/constants": "1.0.4",
"@vercel-internals/get-package-json": "1.0.0",
"@vercel-internals/types": "1.0.27",
"@vercel/client": "13.1.7",
"@vercel-internals/types": "1.0.28",
"@vercel/client": "13.1.8",
"@vercel/error-utils": "2.0.2",
"@vercel/frameworks": "3.0.0",
"@vercel/fs-detectors": "5.2.1",
"@vercel/frameworks": "3.0.1",
"@vercel/fs-detectors": "5.2.2",
"@vercel/routing-utils": "3.1.0",
"@vitest/expect": "1.4.0",
"ajv": "6.12.2",
"alpha-sort": "2.0.1",
"ansi-escapes": "4.3.2",
@@ -129,6 +127,7 @@
"glob": "7.1.2",
"http-proxy": "1.18.1",
"ini": "3.0.0",
"inquirer": "7.0.4",
"is-docker": "2.2.1",
"is-port-reachable": "3.1.0",
"is-url": "1.2.2",
@@ -167,6 +166,8 @@
"ts-node": "10.9.1",
"universal-analytics": "0.4.20",
"utility-types": "2.1.0",
"vite": "5.1.6",
"vitest": "1.3.1",
"which": "3.0.0",
"write-json-file": "2.2.0",
"xdg-app-paths": "5.1.0",

View File

@@ -278,7 +278,9 @@ export default async function bisect(client: Client): Promise<number> {
if (openEnabled) {
await open(testUrl);
}
action = await client.input.expand({
const answer = await client.prompt({
type: 'expand',
name: 'action',
message: 'Select an action:',
choices: [
{ key: 'g', name: 'Good', value: 'good' },
@@ -286,6 +288,7 @@ export default async function bisect(client: Client): Promise<number> {
{ key: 's', name: 'Skip', value: 'skip' },
],
});
action = answer.action;
}
if (action === 'good') {
@@ -339,7 +342,11 @@ function getCommit(deployment: Deployment) {
async function prompt(client: Client, message: string): Promise<string> {
// eslint-disable-next-line no-constant-condition
while (true) {
const val = await client.input.text({ message });
const { val } = await client.prompt({
type: 'input',
name: 'val',
message,
});
if (val) {
return val;
} else {

View File

@@ -4,7 +4,7 @@ import dotenv from 'dotenv';
import semver from 'semver';
import minimatch from 'minimatch';
import { join, normalize, relative, resolve, sep } from 'path';
import frameworks from '@vercel/frameworks';
import { frameworkList } from '@vercel/frameworks';
import {
getDiscontinuedNodeVersions,
normalizePath,
@@ -695,7 +695,7 @@ async function getFramework(
): Promise<{ version: string } | undefined> {
const detectedFramework = await detectFrameworkRecord({
fs: new LocalFileSystemDetector(cwd),
frameworkList: frameworks,
frameworkList,
});
if (!detectedFramework) {

View File

@@ -102,7 +102,7 @@ function readConfirmation(output: Output, msg: string, certs: Cert[]) {
}).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 => {

View File

@@ -3,6 +3,7 @@ import {
getSupportedNodeVersion,
scanParentDirs,
} from '@vercel/build-utils';
import { isZeroConfigBuild } from '../../util/is-zero-config-build';
import {
fileNameSymbol,
VALID_ARCHIVE_FORMATS,
@@ -564,7 +565,7 @@ export default async (client: Client): Promise<number> => {
autoAssignCustomDomains,
};
if (!localConfig.builds || localConfig.builds.length === 0) {
if (isZeroConfigBuild(localConfig.builds)) {
// Only add projectSettings for zero config deployments
createArgs.projectSettings = {
sourceFilesOutsideRootDirectory,

View File

@@ -75,7 +75,7 @@ function readConfirmation(
}).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 => {

View File

@@ -29,6 +29,9 @@ export default async function add(
args: string[],
output: Output
) {
// improve the way we show inquirer prompts
await import('../../util/input/patch-inquirer');
const stdInput = await readStandardInput(client.stdin);
let [envName, envTargetArg, envGitBranch] = args;
@@ -64,11 +67,15 @@ export default async function add(
}
while (!envName) {
envName = await client.input({
const { inputName } = await client.prompt({
type: 'input',
name: 'inputName',
message: `Whats the name of the variable?`,
});
if (!envName) {
envName = inputName;
if (!inputName) {
output.error('Name cannot be empty');
}
}
@@ -100,7 +107,9 @@ export default async function add(
if (stdInput) {
envValue = stdInput;
} else {
const inputValue = await client.input.text({
const { inputValue } = await client.prompt({
type: 'input',
name: 'inputValue',
message: `Whats the value of ${envName}?`,
});
@@ -108,12 +117,16 @@ export default async function add(
}
while (envTargets.length === 0) {
envTargets = await client.input.checkbox({
const { inputTargets } = await client.prompt({
name: 'inputTargets',
type: 'checkbox',
message: `Add ${envName} to which Environments (select multiple)?`,
choices,
});
if (envTargets.length === 0) {
envTargets = inputTargets;
if (inputTargets.length === 0) {
output.error('Please select at least one Environment');
}
}
@@ -124,7 +137,9 @@ export default async function add(
envTargets.length === 1 &&
envTargets[0] === 'preview'
) {
const inputValue = await client.input.text({
const { inputValue } = await client.prompt({
type: 'input',
name: 'inputValue',
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
});
envGitBranch = inputValue || '';

View File

@@ -29,6 +29,9 @@ export default async function rm(
args: string[],
output: Output
) {
// improve the way we show inquirer prompts
await import('../../util/input/patch-inquirer');
if (args.length > 3) {
output.error(
`Invalid number of arguments. Usage: ${getCommandName(
@@ -41,7 +44,9 @@ export default async function rm(
let [envName, envTarget, envGitBranch] = args;
while (!envName) {
const inputName = await client.input({
const { inputName } = await client.prompt({
type: 'input',
name: 'inputName',
message: `Whats the name of the variable?`,
});
@@ -81,7 +86,9 @@ export default async function rm(
}
while (envs.length > 1) {
const id = await client.select({
const { id } = await client.prompt({
name: 'id',
type: 'list',
message: `Remove ${envName} from which Environments?`,
choices: envs.map(env => ({ value: env.id, name: formatEnvTarget(env) })),
});

View File

@@ -277,7 +277,7 @@ function readConfirmation(
}
output.print(
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('(y/N) ')}`
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
);
process.stdin

View File

@@ -2,9 +2,14 @@ import { packageName, getCommandName } from '../../util/pkg-name';
export const secretsCommand = {
name: 'secrets',
description: `NOTE: The ${getCommandName(
'env'
)} command is recommended instead of ${getCommandName('secrets')}`,
description:
'WARNING: On May 1st, 2024 secrets will be automatically converted to sensitive Environment Variables for Preview and Production environments. Secrets attached to Development environments will not be migrated.\n' +
`The ${getCommandName(
'secrets'
)} command will be deprecated at this time, please use the ${getCommandName(
'env'
)} command instead.\n` +
'See https://vercel.com/changelog/legacy-environment-variable-secrets-are-being-sunset for more information.',
arguments: [
{
name: 'command',

View File

@@ -78,6 +78,12 @@ async function run({ output, contextName, currentTeam, client }) {
const commandName = getCommandName('secret ' + subcommand);
output.warn(
'On May 1st, 2024 secrets will be automatically converted to sensitive Environment Variables for Preview and Production environments.\n' +
'Secrets attached to Development environments will not be migrated.' +
'See https://vercel.com/changelog/legacy-environment-variable-secrets-are-being-sunset for more information.'
);
if (subcommand === 'ls' || subcommand === 'list') {
output.note(
`The ${getCommandName(

View File

@@ -1,11 +1,10 @@
import frameworkList from '@vercel/frameworks';
import { frameworkList } from '@vercel/frameworks';
export function sortBuilders<B extends { use: string }>(builds: B[]): B[] {
const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
);
const toNumber = (build: B) => (frontendRuntimeSet.has(build.use) ? 0 : 1);
return builds.sort((build1, build2) => {
return toNumber(build1) - toNumber(build2);
});

View File

@@ -1,9 +1,5 @@
import { bold } from 'chalk';
import input from './custom-input/input';
import select from './custom-input/select';
import checkbox from './custom-input/checkbox';
import expand from './custom-input/expand';
import confirm from './custom-input/confirm';
import inquirer from 'inquirer';
import { EventEmitter } from 'events';
import { URL } from 'url';
import { VercelConfig } from '@vercel/client';
@@ -70,8 +66,8 @@ export default class Client extends EventEmitter implements Stdio {
agent?: Agent;
localConfig?: VercelConfig;
localConfigPath?: string;
prompt!: inquirer.PromptModule;
requestIdCounter: number;
input;
constructor(opts: ClientOptions) {
super();
@@ -87,18 +83,7 @@ export default class Client extends EventEmitter implements Stdio {
this.localConfig = opts.localConfig;
this.localConfigPath = opts.localConfigPath;
this.requestIdCounter = 1;
this.input = {
text: (opts: Parameters<typeof input>[0]) =>
input(opts, { input: this.stdin, output: this.stderr }),
checkbox: <T>(opts: Parameters<typeof checkbox<T>>[0]) =>
checkbox<T>(opts, { input: this.stdin, output: this.stderr }),
expand: (opts: Parameters<typeof expand>[0]) =>
expand(opts, { input: this.stdin, output: this.stderr }),
confirm: (opts: Parameters<typeof confirm>[0]) =>
confirm(opts, { input: this.stdin, output: this.stderr }),
select: <T>(opts: Parameters<typeof select<T>>[0]) =>
select<T>(opts, { input: this.stdin, output: this.stderr }),
};
this._createPromptModule();
}
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
@@ -244,6 +229,13 @@ export default class Client extends EventEmitter implements Stdio {
this.output.debug(`Retrying: ${error}\n${error.stack}`);
};
_createPromptModule() {
this.prompt = inquirer.createPromptModule({
input: this.stdin as NodeJS.ReadStream,
output: this.stderr as NodeJS.WriteStream,
});
}
get cwd(): string {
return process.cwd();
}

View File

@@ -30,7 +30,7 @@ const getGlobalPathConfig = (): string => {
];
// The customPath flag is the preferred location,
// followed by the the vercel directory,
// followed by the vercel directory,
// followed by the now directory.
// If none of those exist, use the vercel directory.
return (

View File

@@ -1,272 +0,0 @@
import {
createPrompt,
useState,
useKeypress,
usePrefix,
usePagination,
useMemo,
makeTheme,
isUpKey,
isDownKey,
isSpaceKey,
isNumberKey,
isEnterKey,
ValidationError,
Separator,
type Theme,
} from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
import chalk from 'chalk';
import figures from './util/figures';
import ansiEscapes from 'ansi-escapes';
import isUnicodeSupported from './util/is-unicode-supported';
const unicode = isUnicodeSupported();
const s = (c: string, fallback: string) => (unicode ? c : fallback);
const S_STEP_ACTIVE = s('◆', '*');
const S_STEP_SUBMIT = s('◇', 'o');
const S_BAR = s('│', '|');
const S_CHECKBOX_SELECTED = s('◼', '[+]');
const S_CHECKBOX_INACTIVE = s('◻', '[ ]');
type Status = 'pending' | 'done';
const symbol = (state: Status) => {
switch (state) {
case 'pending':
return chalk.cyan(S_STEP_ACTIVE);
case 'done':
return chalk.green(S_STEP_SUBMIT);
}
};
type CheckboxTheme = {
icon: {
checked: string;
unchecked: string;
cursor: string;
};
style: {
disabledChoice: (text: string) => string;
renderSelectedChoices: <T>(
selectedChoices: ReadonlyArray<Choice<T>>,
allChoices: ReadonlyArray<Choice<T> | Separator>
) => string;
};
};
const checkboxTheme: CheckboxTheme = {
icon: {
checked: chalk.green(figures.circleFilled),
unchecked: figures.circle,
cursor: figures.pointer,
},
style: {
disabledChoice: (text: string) => chalk.dim(`- ${text}`),
renderSelectedChoices: selectedChoices =>
selectedChoices.map(choice => choice.name || choice.value).join(', '),
},
};
type Choice<Value> = {
name?: string;
value: Value;
disabled?: boolean | string;
checked?: boolean;
type?: never;
};
type Config<Value> = {
message: string;
prefix?: string;
pageSize?: number;
instructions?: string | boolean;
choices: ReadonlyArray<Choice<Value> | Separator>;
loop?: boolean;
required?: boolean;
validate?: (
items: ReadonlyArray<Item<Value>>
) => boolean | string | Promise<string | boolean>;
theme?: PartialDeep<Theme<CheckboxTheme>>;
};
type Item<Value> = Separator | Choice<Value>;
function isSelectable<Value>(item: Item<Value>): item is Choice<Value> {
return !Separator.isSeparator(item) && !item.disabled;
}
function isChecked<Value>(item: Item<Value>): item is Choice<Value> {
return isSelectable(item) && Boolean(item.checked);
}
function toggle<Value>(item: Item<Value>): Item<Value> {
return isSelectable(item) ? { ...item, checked: !item.checked } : item;
}
function check(checked: boolean) {
return function <Value>(item: Item<Value>): Item<Value> {
return isSelectable(item) ? { ...item, checked } : item;
};
}
export default createPrompt(
<Value>(config: Config<Value>, done: (value: Array<Value>) => void) => {
const {
instructions,
pageSize = 7,
loop = true,
choices,
required,
validate = () => true,
} = config;
const theme = makeTheme<CheckboxTheme>(checkboxTheme, config.theme);
const prefix = usePrefix({ theme });
const [status, setStatus] = useState<Status>('pending');
const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(
choices.map(choice => ({ ...choice }))
);
const bounds = useMemo(() => {
const first = items.findIndex(isSelectable);
// TODO: Replace with `findLastIndex` when it's available.
const last =
items.length - 1 - [...items].reverse().findIndex(isSelectable);
if (first < 0) {
throw new ValidationError(
'[checkbox prompt] No selectable choices. All choices are disabled.'
);
}
return { first, last };
}, [items]);
const [active, setActive] = useState(bounds.first);
const [showHelpTip, setShowHelpTip] = useState(true);
const [errorMsg, setError] = useState<string | undefined>(undefined);
useKeypress(async key => {
if (isEnterKey(key)) {
const selection = items.filter(isChecked);
const isValid = await validate([...selection]);
if (required && !items.some(isChecked)) {
setError('At least one choice must be selected');
} else if (isValid === true) {
setStatus('done');
done(selection.map(choice => choice.value));
} else {
setError(isValid || 'You must select a valid value');
}
} else if (isUpKey(key) || isDownKey(key)) {
if (
loop ||
(isUpKey(key) && active !== bounds.first) ||
(isDownKey(key) && active !== bounds.last)
) {
const offset = isUpKey(key) ? -1 : 1;
let next = active;
do {
next = (next + offset + items.length) % items.length;
} while (!isSelectable(items[next]!));
setActive(next);
}
} else if (isSpaceKey(key)) {
setError(undefined);
setShowHelpTip(false);
setItems(
items.map((choice, i) => (i === active ? toggle(choice) : choice))
);
} else if (key.name === 'a') {
const selectAll = Boolean(
items.find(choice => isSelectable(choice) && !choice.checked)
);
setItems(items.map(check(selectAll)));
} else if (key.name === 'i') {
setItems(items.map(toggle));
} else if (isNumberKey(key)) {
// Adjust index to start at 1
const position = Number(key.name) - 1;
const item = items[position];
if (item != null && isSelectable(item)) {
setActive(position);
setItems(
items.map((choice, i) => (i === position ? toggle(choice) : choice))
);
}
}
});
const message = theme.style.message(config.message);
const page = usePagination<Item<Value>>({
items,
active,
renderItem({ item, isActive }: { item: Item<Value>; isActive: boolean }) {
if (Separator.isSeparator(item)) {
return `${chalk.cyan(S_BAR)} ${item.separator}`;
}
const line = item.name || item.value;
if (item.disabled) {
const disabledLabel =
typeof item.disabled === 'string' ? item.disabled : '(disabled)';
return `${chalk.cyan(S_BAR)} ${theme.style.disabledChoice(
`${line} ${disabledLabel}`
)}`;
}
const checkbox = item.checked
? S_CHECKBOX_SELECTED
: S_CHECKBOX_INACTIVE;
const color = isActive ? theme.style.highlight : (x: string) => x;
// const cursor = isActive ? " " : " ";
return `${chalk.cyan(S_BAR)} ${color(`${checkbox} ${line}`)}`;
},
pageSize,
loop,
theme,
});
let helpTip = '';
if (showHelpTip && (instructions === undefined || instructions)) {
if (typeof instructions === 'string') {
helpTip = instructions;
} else {
const keys = [
`${theme.style.key('space')} to select`,
`${theme.style.key('a')} to toggle all`,
`${theme.style.key('i')} to invert selection`,
`and ${theme.style.key('enter')} to proceed`,
];
helpTip = ` (Press ${keys.join(', ')})`;
}
}
const title_basic = `${chalk.gray(S_BAR)}\n${symbol(
status
)} ${prefix} ${message}`;
const title_with_help = `${title_basic}${helpTip}\n`;
const title_without_help = `${title_basic}\n`;
if (status === 'done') {
const selection = items.filter(isChecked);
const answer = theme.style.answer(
theme.style.renderSelectedChoices(selection, items)
);
return `${title_without_help}${chalk.gray(S_BAR)} ${answer}`;
}
let error = '';
if (errorMsg) {
error = theme.style.error(errorMsg);
}
return `${title_with_help}${page}\n${error}${ansiEscapes.cursorHide}`;
}
);
export { Separator };

View File

@@ -1,128 +0,0 @@
import {
createPrompt,
useState,
useKeypress,
isEnterKey,
makeTheme,
isUpKey,
isDownKey,
type Theme,
type KeypressEvent,
} from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
import isUnicodeSupported from './util/is-unicode-supported';
import chalk from 'chalk';
import ansiEscapes from 'ansi-escapes';
const unicode = isUnicodeSupported();
const s = (c: string, fallback: string) => (unicode ? c : fallback);
const S_STEP_ACTIVE = s('◆', '*');
const S_STEP_SUBMIT = s('◇', 'o');
const S_BAR = s('│', '|');
const S_BAR_END = s('└', '—');
const S_RADIO_ACTIVE = s('●', '>');
const S_RADIO_INACTIVE = s('○', ' ');
type Status = 'pending' | 'done';
const symbol = (state: Status) => {
switch (state) {
case 'pending':
return chalk.cyan(S_STEP_ACTIVE);
case 'done':
return chalk.green(S_STEP_SUBMIT);
}
};
type ConfirmConfig = {
message: string;
default?: CursorState;
active?: string;
inactive?: string;
transformer?: (value: boolean) => string;
theme?: PartialDeep<Theme>;
};
const isLeftKey = (key: KeypressEvent): boolean =>
// The left key
key.name === 'left' ||
// Vim keybinding
key.name === 'h' ||
// Emacs keybinding
(key.ctrl && key.name === 'b');
const isRightKey = (key: KeypressEvent): boolean =>
// The right key
key.name === 'right' ||
// Vim keybinding
key.name === 'l' ||
// Emacs keybinding
(key.ctrl && key.name === 'f');
export type CursorState = 'yes' | 'no';
export default createPrompt<boolean, ConfirmConfig>((config, done) => {
const { transformer = answer => (answer ? 'yes' : 'no') } = config;
const [status, setStatus] = useState<Status>('pending');
const [value, setValue] = useState('');
const [cursorStatus, setCursorStatus] = useState<CursorState>(
(config.default ? 'yes' : 'no') ?? 'yes'
);
const theme = makeTheme(config.theme);
const active = config.active ?? 'Yes';
const inactive = config.active ?? 'No';
const toggle = (cursor_state: CursorState): CursorState => {
if (cursor_state === 'yes') {
return 'no';
} else {
return 'yes';
}
};
useKeypress(key => {
if (isEnterKey(key)) {
let answer = cursorStatus === 'yes';
setValue(transformer(answer));
setStatus('done');
done(answer);
} else if (
isUpKey(key) ||
isDownKey(key) ||
isLeftKey(key) ||
isRightKey(key)
) {
setCursorStatus(toggle(cursorStatus));
// setValue(rl.line);
}
});
const message = theme.style.message(config.message);
const title = `${chalk.gray(S_BAR)}\n${symbol(status)} ${message}\n`;
const output = (status: Status) => {
switch (status) {
case 'done':
return `${title}${chalk.gray(S_BAR)} ${chalk.dim(
value === 'yes' ? active : inactive
)}`;
default:
return `${title}${chalk.cyan(S_BAR)} ${
cursorStatus === 'yes'
? `${chalk.green(S_RADIO_ACTIVE)} ${active}`
: `${chalk.dim(S_RADIO_INACTIVE)} ${chalk.dim(active)}`
} ${chalk.dim('/')} ${
cursorStatus === 'no'
? `${chalk.green(S_RADIO_ACTIVE)} ${inactive}`
: `${chalk.dim(S_RADIO_INACTIVE)} ${chalk.dim(inactive)}`
} \n${chalk.cyan(S_BAR_END)} \n${ansiEscapes.cursorHide} `;
}
};
return output(status);
});

View File

@@ -1,161 +0,0 @@
import {
createPrompt,
useState,
useKeypress,
usePrefix,
isEnterKey,
makeTheme,
type Theme,
} from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
import chalk from 'chalk';
import isUnicodeSupported from './util/is-unicode-supported';
const unicode = isUnicodeSupported();
const s = (c: string, fallback: string) => (unicode ? c : fallback);
const S_STEP_ACTIVE = s('◆', '*');
const S_STEP_SUBMIT = s('◇', 'o');
const S_BAR = s('│', '|');
const S_BAR_END = s('└', '—');
type Status = 'pending' | 'done';
const symbol = (state: Status) => {
switch (state) {
case 'pending':
return chalk.cyan(S_STEP_ACTIVE);
case 'done':
return chalk.green(S_STEP_SUBMIT);
}
};
type ExpandChoice =
| { key: string; name: string }
| { key: string; value: string }
| { key: string; name: string; value: string };
type ExpandConfig = {
message: string;
choices: ReadonlyArray<ExpandChoice>;
default?: string;
expanded?: boolean;
theme?: PartialDeep<Theme>;
};
const helpChoice = {
key: 'h',
name: 'Help, list all options',
value: undefined,
};
function getChoiceKey(choice: ExpandChoice, key: 'name' | 'value'): string {
if (key === 'name') {
if ('name' in choice) return choice.name;
return choice.value;
}
if ('value' in choice) return choice.value;
return choice.name;
}
export default createPrompt<string, ExpandConfig>((config, done) => {
const {
choices,
default: defaultKey = 'h',
expanded: defaultExpandState = false,
} = config;
const [status, setStatus] = useState<string>('pending');
const [value, setValue] = useState<string>('');
const [expanded, setExpanded] = useState<boolean>(defaultExpandState);
const [errorMsg, setError] = useState<string | undefined>(undefined);
const theme = makeTheme(config.theme);
const prefix = usePrefix({ theme });
useKeypress((event, rl) => {
if (isEnterKey(event)) {
const answer = (value || defaultKey).toLowerCase();
if (answer === 'h' && !expanded) {
setExpanded(true);
} else {
const selectedChoice = choices.find(({ key }) => key === answer);
if (selectedChoice) {
const finalValue = getChoiceKey(selectedChoice, 'value');
setValue(finalValue);
setStatus('done');
done(finalValue);
} else if (value === '') {
setError('Please input a value');
} else {
setError(`"${chalk.red(value)}" isn't an available option`);
}
}
} else {
setValue(rl.line);
setError(undefined);
}
});
const message = theme.style.message(config.message);
const title = `${chalk.gray(S_BAR)}\n${symbol(status)} ${prefix} ${message}`;
if (status === 'done') {
// TODO: `value` should be the display name instead of the raw value.
return `${title}\n${chalk.gray(S_BAR)} ${theme.style.answer(value)}`;
}
const allChoices = expanded ? choices : [...choices, helpChoice];
// Collapsed display style
let longChoices = '';
let shortChoices = allChoices
.map(choice => {
if (choice.key === defaultKey) {
return choice.key.toUpperCase();
}
return choice.key;
})
.join('');
shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`;
// Expanded display style
if (expanded) {
shortChoices = '';
longChoices = allChoices
.map(choice => {
const line = ` ${choice.key}) ${getChoiceKey(choice, 'name')}`;
if (choice.key === value.toLowerCase()) {
return `${chalk.cyan(S_BAR)} ${theme.style.highlight(line)}`;
}
return `${chalk.cyan(S_BAR)} ${line}`;
})
.join('\n');
}
let helpTip = '';
const currentOption = allChoices.find(
({ key }) => key === value.toLowerCase()
);
if (currentOption) {
helpTip = `${chalk.cyan(S_BAR)} ${chalk.cyan('>>')} ${getChoiceKey(
currentOption,
'name'
)} `;
}
let error = '';
if (errorMsg) {
error = theme.style.error(errorMsg);
}
return [
`${title}${shortChoices} \n${chalk.cyan(S_BAR)} ${value}`,
`${[longChoices, helpTip, error, chalk.cyan(S_BAR_END)]
.filter(Boolean)
.join('\n')}`,
];
});

View File

@@ -1,129 +0,0 @@
import {
createPrompt,
useState,
useKeypress,
isEnterKey,
isBackspaceKey,
makeTheme,
type Theme,
} from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
import chalk from 'chalk';
import ansiEscapes from 'ansi-escapes';
import isUnicodeSupported from './util/is-unicode-supported';
const unicode = isUnicodeSupported();
const s = (c: string, fallback: string) => (unicode ? c : fallback);
const S_STEP_ACTIVE = s('◆', '*');
const S_STEP_CANCEL = s('■', 'x');
const S_STEP_SUBMIT = s('◇', 'o');
const S_BAR = s('│', '|');
const S_BAR_END = s('└', '—');
type InputConfig = {
message: string;
default?: string;
transformer?: (value: string, { isFinal }: { isFinal: boolean }) => string;
validate?: (value: string) => boolean | string | Promise<string | boolean>;
theme?: PartialDeep<Theme>;
};
type Status = 'pending' | 'loading' | 'done';
const symbol = (state: Status) => {
switch (state) {
case 'pending':
return chalk.cyan(S_STEP_ACTIVE);
case 'loading':
return chalk.red(S_STEP_CANCEL);
case 'done':
return chalk.green(S_STEP_SUBMIT);
}
};
export default createPrompt<string, InputConfig>((config, done) => {
const { validate = () => true } = config;
const theme = makeTheme(config.theme);
const [status, setStatus] = useState<Status>('pending');
const [cursorIndex, setCursorIndex] = useState<number>(0);
const [defaultValue = '', setDefaultValue] = useState<string>(
config.default || ''
);
const [errorMsg, setError] = useState<string | undefined>(undefined);
const [value, setValue] = useState<string>('');
useKeypress(async (key, rl) => {
// Ignore keypress while our prompt is doing other processing.
if (status !== 'pending') {
return;
}
setCursorIndex(rl.cursor);
if (isEnterKey(key)) {
const answer = value || defaultValue;
setStatus('loading');
const isValid = await validate(answer);
if (isValid === true) {
setValue(answer);
setStatus('done');
done(answer);
} else {
// Reset the readline line value to the previous value. On line event, the value
// get cleared, forcing the user to re-enter the value instead of fixing it.
rl.write(value);
setError(isValid || 'You must provide a valid value');
setStatus('pending');
}
} else if (isBackspaceKey(key) && !value) {
setDefaultValue('');
} else if (key.name === 'tab' && !value) {
setDefaultValue('');
rl.clearLine(0); // Remove the tab character.
rl.write(defaultValue);
setValue(defaultValue);
} else {
setValue(rl.line);
setError(undefined);
}
});
const message = theme.style.message(config.message);
let formattedValue = value;
if (typeof config.transformer === 'function') {
formattedValue = config.transformer(value, { isFinal: status === 'done' });
} else if (status === 'done') {
formattedValue = theme.style.answer(value);
}
let error = '';
if (errorMsg) {
error = theme.style.error(errorMsg);
}
let valueWithCursor: any;
if (cursorIndex && cursorIndex >= value.length) {
valueWithCursor = `${value}${chalk.inverse(chalk.hidden(' '))}`;
} else {
const s1 = value.slice(0, cursorIndex);
const s2 = value.slice(cursorIndex);
if (s2.length !== 0)
valueWithCursor = `${s1}${chalk.inverse(s2[0])}${s2.slice(1)}`;
else valueWithCursor = `${s1}${chalk.inverse(' ')}`;
}
const title = `${chalk.gray(S_BAR)}\n${symbol(status)} ${message}\n`;
const output = (status: any) => {
switch (status) {
case 'done':
return `${title}${chalk.gray(S_BAR)} ${formattedValue}`;
default:
return `${title}${chalk.cyan(S_BAR)} ${valueWithCursor}\n${chalk.cyan(
S_BAR_END
)}\n${ansiEscapes.cursorHide}`;
}
};
return [output(status), error];
});

View File

@@ -1,229 +0,0 @@
import {
createPrompt,
useState,
useKeypress,
usePrefix,
usePagination,
useRef,
useMemo,
isBackspaceKey,
isEnterKey,
isUpKey,
isDownKey,
isNumberKey,
Separator,
ValidationError,
makeTheme,
type Theme,
} from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
import chalk from 'chalk';
import figures from './util/figures';
import ansiEscapes from 'ansi-escapes';
import isUnicodeSupported from './util/is-unicode-supported';
const unicode = isUnicodeSupported();
const s = (c: string, fallback: string) => (unicode ? c : fallback);
const S_STEP_ACTIVE = s('◆', '*');
const S_STEP_SUBMIT = s('◇', 'o');
const S_BAR = s('│', '|');
const S_BAR_END = s('└', '—');
const S_RADIO_ACTIVE = s('●', '>');
const S_RADIO_INACTIVE = s('○', ' ');
type SelectTheme = {
icon: { cursor: string };
style: { disabled: (text: string) => string };
};
const selectTheme: SelectTheme = {
icon: { cursor: figures.pointer },
style: { disabled: (text: string) => chalk.dim(`- ${text}`) },
};
type Choice<Value> = {
value: Value;
name?: string;
description?: string;
disabled?: boolean | string;
type?: never;
};
type SelectConfig<Value> = {
message: string;
choices: ReadonlyArray<Choice<Value> | Separator>;
pageSize?: number;
loop?: boolean;
default?: unknown;
theme?: PartialDeep<Theme<SelectTheme>>;
};
type Item<Value> = Separator | Choice<Value>;
function isSelectable<Value>(item: Item<Value>): item is Choice<Value> {
return !Separator.isSeparator(item) && !item.disabled;
}
type Status = 'pending' | 'done';
const symbol = (state: Status) => {
switch (state) {
case 'pending':
return chalk.cyan(S_STEP_ACTIVE);
case 'done':
return chalk.green(S_STEP_SUBMIT);
}
};
export default createPrompt(
<Value>(
config: SelectConfig<Value>,
done: (value: Value) => void
): string => {
const { choices: items, loop = true, pageSize = 7 } = config;
const firstRender = useRef(true);
const theme = makeTheme<SelectTheme>(selectTheme, config.theme);
const prefix = usePrefix({ theme });
const [status, setStatus] = useState<Status>('pending');
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined
);
const bounds = useMemo(() => {
const first = items.findIndex(isSelectable);
// TODO: Replace with `findLastIndex` when it's available.
const last =
items.length - 1 - [...items].reverse().findIndex(isSelectable);
if (first < 0)
throw new ValidationError(
'[select prompt] No selectable choices. All choices are disabled.'
);
return { first, last };
}, [items]);
const defaultItemIndex = useMemo(() => {
if (!('default' in config)) return -1;
return items.findIndex(
item => isSelectable(item) && item.value === config.default
);
}, [config.default, items]);
const [active, setActive] = useState(
defaultItemIndex === -1 ? bounds.first : defaultItemIndex
);
// Safe to assume the cursor position always point to a Choice.
const selectedChoice = items[active] as Choice<Value>;
useKeypress((key, rl) => {
clearTimeout(searchTimeoutRef.current);
if (isEnterKey(key)) {
setStatus('done');
done(selectedChoice.value);
} else if (isUpKey(key) || isDownKey(key)) {
rl.clearLine(0);
if (
loop ||
(isUpKey(key) && active !== bounds.first) ||
(isDownKey(key) && active !== bounds.last)
) {
const offset = isUpKey(key) ? -1 : 1;
let next = active;
do {
next = (next + offset + items.length) % items.length;
} while (!isSelectable(items[next]!));
setActive(next);
}
} else if (isNumberKey(key)) {
rl.clearLine(0);
const position = Number(key.name) - 1;
const item = items[position];
if (item != null && isSelectable(item)) {
setActive(position);
}
} else if (isBackspaceKey(key)) {
rl.clearLine(0);
} else {
// Default to search
const searchTerm = rl.line.toLowerCase();
const matchIndex = items.findIndex(item => {
if (Separator.isSeparator(item) || !isSelectable(item)) return false;
return String(item.name || item.value)
.toLowerCase()
.startsWith(searchTerm);
});
if (matchIndex >= 0) {
setActive(matchIndex);
}
searchTimeoutRef.current = setTimeout(() => {
rl.clearLine(0);
}, 700);
}
});
const message = theme.style.message(config.message);
let helpTip;
if (firstRender.current && items.length <= pageSize) {
firstRender.current = false;
helpTip = theme.style.help('(Use arrow keys)');
}
const page = usePagination<Item<Value>>({
items,
active,
renderItem({ item, isActive }: { item: Item<Value>; isActive: boolean }) {
if (Separator.isSeparator(item)) {
return `${chalk.cyan(S_BAR)} ${item.separator}`;
}
const line = item.name || item.value;
if (item.disabled) {
const disabledLabel =
typeof item.disabled === 'string' ? item.disabled : '(disabled)';
return theme.style.disabled(`${line} ${disabledLabel}`);
}
const color = isActive ? theme.style.highlight : (x: string) => x;
const cursor = isActive ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
return `${chalk.cyan(S_BAR)} ${color(`${cursor} ${line}`)}`;
},
pageSize,
loop,
theme,
});
const title = `${chalk.gray(S_BAR)}\n${symbol(status)} ${[
prefix,
message,
helpTip,
]
.filter(Boolean)
.join(' ')}\n`;
if (status === 'done') {
const answer =
selectedChoice.name ||
// TODO: Could we enforce that at the type level? Name should be defined for non-string values.
String(selectedChoice.value);
return `${title}${chalk.gray(S_BAR)} ${theme.style.answer(answer)}`;
}
const choiceDescription = selectedChoice.description
? `\n${selectedChoice.description}`
: ``;
return `${title}${page}${choiceDescription}\n${chalk.cyan(S_BAR_END)}${
ansiEscapes.cursorHide
} `;
}
);
export { Separator };

View File

@@ -1,307 +0,0 @@
/*
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import isUnicodeSupported from './is-unicode-supported';
const common = {
circleQuestionMark: '(?)',
questionMarkPrefix: '(?)',
square: '█',
squareDarkShade: '▓',
squareMediumShade: '▒',
squareLightShade: '░',
squareTop: '▀',
squareBottom: '▄',
squareLeft: '▌',
squareRight: '▐',
squareCenter: '■',
bullet: '●',
dot: '',
ellipsis: '…',
pointerSmall: '',
triangleUp: '▲',
triangleUpSmall: '▴',
triangleDown: '▼',
triangleDownSmall: '▾',
triangleLeftSmall: '◂',
triangleRightSmall: '▸',
home: '⌂',
heart: '♥',
musicNote: '♪',
musicNoteBeamed: '♫',
arrowUp: '↑',
arrowDown: '↓',
arrowLeft: '←',
arrowRight: '→',
arrowLeftRight: '↔',
arrowUpDown: '↕',
almostEqual: '≈',
notEqual: '≠',
lessOrEqual: '≤',
greaterOrEqual: '≥',
identical: '≡',
infinity: '∞',
subscriptZero: '₀',
subscriptOne: '₁',
subscriptTwo: '₂',
subscriptThree: '₃',
subscriptFour: '₄',
subscriptFive: '₅',
subscriptSix: '₆',
subscriptSeven: '₇',
subscriptEight: '₈',
subscriptNine: '₉',
oneHalf: '½',
oneThird: '⅓',
oneQuarter: '¼',
oneFifth: '⅕',
oneSixth: '⅙',
oneEighth: '⅛',
twoThirds: '⅔',
twoFifths: '⅖',
threeQuarters: '¾',
threeFifths: '⅗',
threeEighths: '⅜',
fourFifths: '⅘',
fiveSixths: '⅚',
fiveEighths: '⅝',
sevenEighths: '⅞',
line: '─',
lineBold: '━',
lineDouble: '═',
lineDashed0: '┄',
lineDashed1: '┅',
lineDashed2: '┈',
lineDashed3: '┉',
lineDashed4: '╌',
lineDashed5: '╍',
lineDashed6: '╴',
lineDashed7: '╶',
lineDashed8: '╸',
lineDashed9: '╺',
lineDashed10: '╼',
lineDashed11: '╾',
lineDashed12: '',
lineDashed13: '',
lineDashed14: '',
lineDashed15: '',
lineVertical: '│',
lineVerticalBold: '┃',
lineVerticalDouble: '║',
lineVerticalDashed0: '┆',
lineVerticalDashed1: '┇',
lineVerticalDashed2: '┊',
lineVerticalDashed3: '┋',
lineVerticalDashed4: '╎',
lineVerticalDashed5: '╏',
lineVerticalDashed6: '╵',
lineVerticalDashed7: '╷',
lineVerticalDashed8: '╹',
lineVerticalDashed9: '╻',
lineVerticalDashed10: '╽',
lineVerticalDashed11: '╿',
lineDownLeft: '┐',
lineDownLeftArc: '╮',
lineDownBoldLeftBold: '┓',
lineDownBoldLeft: '┒',
lineDownLeftBold: '┑',
lineDownDoubleLeftDouble: '╗',
lineDownDoubleLeft: '╖',
lineDownLeftDouble: '╕',
lineDownRight: '┌',
lineDownRightArc: '╭',
lineDownBoldRightBold: '┏',
lineDownBoldRight: '┎',
lineDownRightBold: '┍',
lineDownDoubleRightDouble: '╔',
lineDownDoubleRight: '╓',
lineDownRightDouble: '╒',
lineUpLeft: '┘',
lineUpLeftArc: '╯',
lineUpBoldLeftBold: '┛',
lineUpBoldLeft: '┚',
lineUpLeftBold: '┙',
lineUpDoubleLeftDouble: '╝',
lineUpDoubleLeft: '╜',
lineUpLeftDouble: '╛',
lineUpRight: '└',
lineUpRightArc: '╰',
lineUpBoldRightBold: '┗',
lineUpBoldRight: '┖',
lineUpRightBold: '┕',
lineUpDoubleRightDouble: '╚',
lineUpDoubleRight: '╙',
lineUpRightDouble: '╘',
lineUpDownLeft: '┤',
lineUpBoldDownBoldLeftBold: '┫',
lineUpBoldDownBoldLeft: '┨',
lineUpDownLeftBold: '┥',
lineUpBoldDownLeftBold: '┩',
lineUpDownBoldLeftBold: '┪',
lineUpDownBoldLeft: '┧',
lineUpBoldDownLeft: '┦',
lineUpDoubleDownDoubleLeftDouble: '╣',
lineUpDoubleDownDoubleLeft: '╢',
lineUpDownLeftDouble: '╡',
lineUpDownRight: '├',
lineUpBoldDownBoldRightBold: '┣',
lineUpBoldDownBoldRight: '┠',
lineUpDownRightBold: '┝',
lineUpBoldDownRightBold: '┡',
lineUpDownBoldRightBold: '┢',
lineUpDownBoldRight: '┟',
lineUpBoldDownRight: '┞',
lineUpDoubleDownDoubleRightDouble: '╠',
lineUpDoubleDownDoubleRight: '╟',
lineUpDownRightDouble: '╞',
lineDownLeftRight: '┬',
lineDownBoldLeftBoldRightBold: '┳',
lineDownLeftBoldRightBold: '┯',
lineDownBoldLeftRight: '┰',
lineDownBoldLeftBoldRight: '┱',
lineDownBoldLeftRightBold: '┲',
lineDownLeftRightBold: '┮',
lineDownLeftBoldRight: '┭',
lineDownDoubleLeftDoubleRightDouble: '╦',
lineDownDoubleLeftRight: '╥',
lineDownLeftDoubleRightDouble: '╤',
lineUpLeftRight: '┴',
lineUpBoldLeftBoldRightBold: '┻',
lineUpLeftBoldRightBold: '┷',
lineUpBoldLeftRight: '┸',
lineUpBoldLeftBoldRight: '┹',
lineUpBoldLeftRightBold: '┺',
lineUpLeftRightBold: '┶',
lineUpLeftBoldRight: '┵',
lineUpDoubleLeftDoubleRightDouble: '╩',
lineUpDoubleLeftRight: '╨',
lineUpLeftDoubleRightDouble: '╧',
lineUpDownLeftRight: '┼',
lineUpBoldDownBoldLeftBoldRightBold: '╋',
lineUpDownBoldLeftBoldRightBold: '╈',
lineUpBoldDownLeftBoldRightBold: '╇',
lineUpBoldDownBoldLeftRightBold: '╊',
lineUpBoldDownBoldLeftBoldRight: '╉',
lineUpBoldDownLeftRight: '╀',
lineUpDownBoldLeftRight: '╁',
lineUpDownLeftBoldRight: '┽',
lineUpDownLeftRightBold: '┾',
lineUpBoldDownBoldLeftRight: '╂',
lineUpDownLeftBoldRightBold: '┿',
lineUpBoldDownLeftBoldRight: '╃',
lineUpBoldDownLeftRightBold: '╄',
lineUpDownBoldLeftBoldRight: '╅',
lineUpDownBoldLeftRightBold: '╆',
lineUpDoubleDownDoubleLeftDoubleRightDouble: '╬',
lineUpDoubleDownDoubleLeftRight: '╫',
lineUpDownLeftDoubleRightDouble: '╪',
lineCross: '',
lineBackslash: '╲',
lineSlash: '',
};
const specialMainSymbols = {
tick: '✔',
info: '',
warning: '⚠',
cross: '✘',
squareSmall: '◻',
squareSmallFilled: '◼',
circle: '◯',
circleFilled: '◉',
circleDotted: '◌',
circleDouble: '◎',
circleCircle: 'ⓞ',
circleCross: 'ⓧ',
circlePipe: 'Ⓘ',
radioOn: '◉',
radioOff: '◯',
checkboxOn: '☒',
checkboxOff: '☐',
checkboxCircleOn: 'ⓧ',
checkboxCircleOff: 'Ⓘ',
pointer: '',
triangleUpOutline: '△',
triangleLeft: '◀',
triangleRight: '▶',
lozenge: '◆',
lozengeOutline: '◇',
hamburger: '☰',
smiley: '㋡',
mustache: '෴',
star: '★',
play: '▶',
nodejs: '⬢',
oneSeventh: '⅐',
oneNinth: '⅑',
oneTenth: '⅒',
};
const specialFallbackSymbols = {
tick: '√',
info: 'i',
warning: '‼',
cross: '×',
squareSmall: '□',
squareSmallFilled: '■',
circle: '( )',
circleFilled: '(*)',
circleDotted: '( )',
circleDouble: '( )',
circleCircle: '(○)',
circleCross: '(×)',
circlePipe: '(│)',
radioOn: '(*)',
radioOff: '( )',
checkboxOn: '[×]',
checkboxOff: '[ ]',
checkboxCircleOn: '(×)',
checkboxCircleOff: '( )',
pointer: '>',
triangleUpOutline: '∆',
triangleLeft: '◄',
triangleRight: '►',
lozenge: '♦',
lozengeOutline: '◊',
hamburger: '≡',
smiley: '☺',
mustache: '┌─┐',
star: '✶',
play: '►',
nodejs: '♦',
oneSeventh: '1/7',
oneNinth: '1/9',
oneTenth: '1/10',
};
export const mainSymbols = { ...common, ...specialMainSymbols };
export const fallbackSymbols = { ...common, ...specialFallbackSymbols };
const shouldUseMain = isUnicodeSupported();
const figures = shouldUseMain ? mainSymbols : fallbackSymbols;
export default figures;
const replacements = Object.entries(specialMainSymbols);
// On terminals which do not support Unicode symbols, substitute them to other symbols
export const replaceSymbols = (
string,
{ useFallback = !shouldUseMain } = {}
) => {
if (useFallback) {
for (const [key, mainSymbol] of replacements) {
string = string.replaceAll(mainSymbol, fallbackSymbols[key]);
}
}
return string;
};

View File

@@ -1,28 +0,0 @@
/*
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export default function isUnicodeSupported() {
if (process.platform !== 'win32') {
return process.env.TERM !== 'linux'; // Linux console (kernel)
}
return (
Boolean(process.env.WT_SESSION) || // Windows Terminal
Boolean(process.env.TERMINUS_SUBLIME) || // Terminus (<0.2.27)
process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
process.env.TERM_PROGRAM === 'Terminus-Sublime' ||
process.env.TERM_PROGRAM === 'vscode' ||
process.env.TERM === 'xterm-256color' ||
process.env.TERM === 'alacritty' ||
process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
);
}

View File

@@ -45,7 +45,7 @@ import {
detectApiExtensions,
isOfficialRuntime,
} from '@vercel/fs-detectors';
import frameworkList from '@vercel/frameworks';
import { frameworkList } from '@vercel/frameworks';
import cmd from '../output/cmd';
import link from '../output/link';

View File

@@ -5,8 +5,14 @@ export default async function confirm(
message: string,
preferred: boolean
): Promise<boolean> {
return client.input.confirm({
await import('./patch-inquirer');
const answers = await client.prompt({
type: 'confirm',
name: 'value',
message,
default: preferred,
});
return answers.value;
}

View File

@@ -1,6 +1,7 @@
import inquirer from 'inquirer';
import confirm from './confirm';
import chalk from 'chalk';
import frameworkList, { Framework } from '@vercel/frameworks';
import { frameworkList, Framework } from '@vercel/frameworks';
import Client from '../client';
import { isSettingValue } from '../is-setting-value';
import type { ProjectSettings } from '@vercel-internals/types';
@@ -139,16 +140,33 @@ export default async function editProjectSettings(
[]
);
const settingFields = (await client.input.checkbox({
const { settingFields } = await inquirer.prompt<{
settingFields: Array<
Exclude<
ConfigKeys,
'framework' | 'commandForIgnoringBuildStep' | 'installCommand'
>
>;
}>({
name: 'settingFields',
type: 'checkbox',
message: 'Which settings would you like to overwrite (select multiple)?',
choices,
})) as ConfigKeys[];
});
for (let setting of settingFields) {
const field = settingMap[setting];
settings[setting] = await client.input({
const answers = await inquirer.prompt<{
[k in Exclude<
ConfigKeys,
'framework' | 'commandForIgnoringBuildStep' | 'installCommand'
>]: string;
}>({
type: 'input',
name: setting,
message: `What's your ${chalk.bold(field)}?`,
});
settings[setting] = answers[setting];
}
return settings;
}

View File

@@ -78,9 +78,12 @@ export default async function inputProject(
let project: Project | ProjectNotFound | null = null;
while (!project || project instanceof ProjectNotFound) {
const projectName = await client.input({
message: 'Whats the name of your existing project?',
const answers = await client.prompt({
type: 'input',
name: 'existingProjectName',
message: `Whats the name of your existing project?`,
});
const projectName = answers.existingProjectName as string;
if (!projectName) {
output.error(`Project name cannot be empty`);
@@ -106,10 +109,13 @@ export default async function inputProject(
let newProjectName: string | null = null;
while (!newProjectName) {
newProjectName = await client.input({
const answers = await client.prompt({
type: 'input',
name: 'newProjectName',
message: `Whats your projects name?`,
default: !detectedProject ? slugifiedName : undefined,
});
newProjectName = answers.newProjectName as string;
if (!newProjectName) {
output.error(`Project name cannot be empty`);

View File

@@ -14,7 +14,9 @@ export async function inputRootDirectory(
// eslint-disable-next-line no-constant-condition
while (true) {
const rootDirectory = await client.input({
const { rootDirectory } = await client.prompt({
type: 'input',
name: 'rootDirectory',
message: `In which directory is your code located?`,
transformer: (input: string) => {
return `${chalk.dim(`./`)}${input}`;

View File

@@ -1,4 +1,4 @@
import { Separator } from '@inquirer/prompts';
import inquirer from 'inquirer';
import stripAnsi from 'strip-ansi';
import Client from '../client';
import eraseLines from '../output/erase-lines';
@@ -14,7 +14,7 @@ interface ListSeparator {
separator: string;
}
export type ListChoice = ListEntry | ListSeparator | typeof Separator;
export type ListChoice = ListEntry | ListSeparator | typeof inquirer.Separator;
interface ListOptions {
message: string;
@@ -54,6 +54,8 @@ export default async function list(
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
}: ListOptions
): Promise<string> {
await import('./patch-inquirer');
let biggestLength = 0;
let selected: string | undefined;
@@ -68,14 +70,14 @@ export default async function list(
}
const choices = _choices.map(choice => {
if (choice instanceof Separator) {
if (choice instanceof inquirer.Separator) {
return choice;
}
if ('separator' in choice) {
const prefix = `── ${choice.separator} `;
const suffix = '─'.repeat(biggestLength - getLength(prefix));
return new Separator(`${prefix}${suffix}`);
return new inquirer.Separator(`${prefix}${suffix}`);
}
if ('short' in choice) {
@@ -91,11 +93,11 @@ export default async function list(
if (separator) {
for (let i = 0; i < choices.length; i += 2) {
choices.splice(i, 0, new Separator(' '));
choices.splice(i, 0, new inquirer.Separator(' '));
}
}
const cancelSeparator = new Separator('─'.repeat(biggestLength));
const cancelSeparator = new inquirer.Separator('─'.repeat(biggestLength));
const _cancel = {
name: 'Cancel',
value: '',
@@ -108,16 +110,18 @@ export default async function list(
choices.push(cancelSeparator, _cancel);
}
const answer = await client.select({
const answer = await client.prompt({
name: 'value',
type: 'list',
default: selected,
message,
choices,
pageSize,
default: selected,
});
if (eraseFinalAnswer === true) {
process.stdout.write(eraseLines(2));
}
return answer;
return answer.value;
}

View File

@@ -0,0 +1,210 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import Prompt from 'inquirer/lib/prompts/base';
import Choice from 'inquirer/lib/objects/choice';
import Separator from 'inquirer/lib/objects/separator';
/**
* Here we patch inquirer with some tweaks:
* - update "list" to use ● and ○ and hide tips
* - update "checkbox" to use ◻︎ and ◼︎ and hide tips
* - use '?' before questions
* - do not apply color to question's answer
*/
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/base.js#L126
const getQuestion = function (this: Prompt) {
let message = `${chalk.gray('?')} ${this.opt.message} `;
if (this.opt.type === 'confirm') {
if (this.opt.default === 'y/N') {
message += `[y/${chalk.bold('N')}] `;
} else {
message += `[${chalk.bold('Y')}/n] `;
}
}
// Append the default if available, and if question isn't answered
else if (this.opt.default != null && this.status !== 'answered') {
message += chalk.dim(`(${this.opt.default}) `);
}
return message;
};
inquirer.prompt.prompts.list.prototype.getQuestion = getQuestion;
inquirer.prompt.prompts.checkbox.prototype.getQuestion = getQuestion;
inquirer.prompt.prompts.input.prototype.getQuestion = getQuestion;
inquirer.prompt.prompts.confirm.prototype.getQuestion = getQuestion;
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/list.js#L80
inquirer.prompt.prompts.list.prototype.render = function () {
// Render question
let message = this.getQuestion();
// Render choices or answer depending on the state
if (this.status === 'answered') {
message += this.opt.choices.getChoice(this.selected).short;
} else {
let choicesStr = listRender(this.opt.choices, this.selected);
let indexPosition = this.opt.choices.indexOf(
this.opt.choices.getChoice(this.selected)
);
message +=
'\n' +
this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize);
}
this.firstRender = false;
this.screen.render(message);
};
function listRender(choices: (Choice | Separator)[], pointer: number) {
let output = '';
let separatorOffset = 0;
choices.forEach((choice, i) => {
if (choice.type === 'separator') {
separatorOffset++;
output += ' ' + choice + '\n';
return;
}
if (choice.disabled) {
separatorOffset++;
output += ' - ' + choice.name;
output +=
' (' +
(typeof choice.disabled === 'string' ? choice.disabled : 'Disabled') +
')';
output += '\n';
return;
}
let isSelected = i - separatorOffset === pointer;
let line = (isSelected ? '● ' : '○ ') + choice.name;
if (isSelected) {
line = chalk.cyan(line);
}
output += line + ' \n';
});
return output.replace(/\n$/, '');
}
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/checkbox.js#L84
inquirer.prompt.prompts.checkbox.prototype.render = function (error?: string) {
// Render question
let message = this.getQuestion();
let bottomContent = '';
if (!this.spaceKeyPressed) {
message +=
'(Press ' +
chalk.cyan.bold('<space>') +
' to select, ' +
chalk.cyan.bold('<a>') +
' to toggle all, ' +
chalk.cyan.bold('<i>') +
' to invert selection)';
}
// Render choices or answer depending on the state
if (this.status === 'answered') {
message += this.selection.length > 0 ? this.selection.join(', ') : 'None';
} else {
let choicesStr = renderChoices(this.opt.choices, this.pointer);
let indexPosition = this.opt.choices.indexOf(
this.opt.choices.getChoice(this.pointer)
);
message +=
'\n' +
this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize);
}
if (error) {
bottomContent = chalk.red('>> ') + error;
}
this.screen.render(message, bottomContent);
};
function renderChoices(choices: (Choice | Separator)[], pointer: number) {
let output = '';
let separatorOffset = 0;
choices.forEach(function (choice, i) {
if (choice.type === 'separator') {
separatorOffset++;
output += '' + choice + '\n';
return;
}
if (choice.disabled) {
separatorOffset++;
output += '- ' + choice.name;
output +=
' (' +
(typeof choice.disabled === 'string' ? choice.disabled : 'Disabled') +
')';
} else {
if (i - separatorOffset === pointer) {
output += chalk.cyan(
(choice.checked ? ' ▪︎' : ' ▫︎') + ' ' + choice.name
);
} else {
output += chalk.cyan(
(choice.checked ? ' ▪︎' : ' ▫︎') + ' ' + choice.name
);
}
}
output += '\n';
});
return output.replace(/\n$/, '');
}
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/input.js#L44
inquirer.prompt.prompts.input.prototype.render = function (error?: string) {
let bottomContent = '';
let appendContent = '';
let message = this.getQuestion();
let transformer = this.opt.transformer;
let isFinal = this.status === 'answered';
if (isFinal) {
appendContent = this.answer;
} else {
appendContent = this.rl.line;
}
if (transformer) {
message += transformer(appendContent, this.answers, { isFinal });
} else {
message += appendContent;
}
if (error) {
bottomContent = chalk.red('>> ') + error;
}
this.screen.render(message, bottomContent);
};
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/confirm.js#L64
inquirer.prompt.prompts.confirm.prototype.render = function (answer?: boolean) {
let message = this.getQuestion();
if (this.status === 'answered') {
message += answer ? 'y' : 'n';
} else {
message += this.rl.line;
}
this.screen.render(message);
return this;
};

View File

@@ -10,6 +10,7 @@ export default async function selectOrg(
question: string,
autoConfirm?: boolean
): Promise<Org> {
await import('./patch-inquirer');
const {
output,
config: { currentTeam },
@@ -51,9 +52,14 @@ export default async function selectOrg(
return choices[defaultChoiceIndex].value;
}
return await client.select({
const answers = await client.prompt({
type: 'list',
name: 'org',
message: question,
choices,
default: choices[defaultChoiceIndex].value,
default: defaultChoiceIndex,
});
const org = answers.org;
return org;
}

View File

@@ -77,7 +77,7 @@ export default function text({
// Tab
// Right arrow
autoCompleteChars = new Set(['\t', '\x1b[C']),
// If true, converts everything the user types to to lowercase
// If true, converts everything the user types to lowercase
forceLowerCase = false,
}: TextParams = {}): Promise<string> {
return new Promise((resolve, reject) => {

View File

@@ -0,0 +1,11 @@
import { Builder } from '@vercel/build-utils';
export function isZeroConfigBuild(builds?: Builder[]): boolean {
return (
!builds ||
builds.length === 0 ||
// If the zeroConfig property is set on all builds, still consider it as
// zero config deployment
builds.every(buildConfig => buildConfig.config?.zeroConfig)
);
}

View File

@@ -1,5 +1,5 @@
import chalk from 'chalk';
import { Separator } from '@inquirer/prompts';
import inquirer from 'inquirer';
import pluralize from 'pluralize';
import { homedir } from 'os';
import slugify from '@sindresorhus/slugify';
@@ -144,13 +144,16 @@ export async function ensureRepoLink(
if (yes) {
remoteName = defaultRemote;
} else {
remoteName = await client.select({
const answer = await client.prompt({
type: 'list',
name: 'value',
message: 'Which Git remote should be used?',
choices: remoteNames.map(name => {
return { name: name, value: name };
}),
default: defaultRemote,
});
remoteName = answer.value;
}
}
const repoUrl = remoteUrls[remoteName];
@@ -219,13 +222,15 @@ export async function ensureRepoLink(
selected = projects;
} else {
const addSeparators = projects.length > 0 && detectedProjectsCount > 0;
selected = await client.input.checkbox({
const answer = await client.prompt({
type: 'checkbox',
name: 'selected',
message: `Which Projects should be ${
projects.length ? 'linked to' : 'created'
}?`,
choices: [
...(addSeparators
? [new Separator('----- Existing Projects -----')]
? [new inquirer.Separator('----- Existing Projects -----')]
: []),
...projects.map(project => {
return {
@@ -235,7 +240,7 @@ export async function ensureRepoLink(
};
}),
...(addSeparators
? [new Separator('----- New Projects to be created -----')]
? [new inquirer.Separator('----- New Projects to be created -----')]
: []),
...Array.from(detectedProjects.entries()).flatMap(
([rootDirectory, frameworks]) =>
@@ -262,16 +267,9 @@ export async function ensureRepoLink(
};
})
),
] as (
| Separator
| {
newProject?: boolean;
name: string;
value: Project | NewProject;
checked: boolean;
}
)[],
],
});
selected = answer.selected;
}
if (selected.length === 0) {

View File

@@ -2,6 +2,7 @@ import { join, basename } from 'path';
import chalk from 'chalk';
import { remove } from 'fs-extra';
import { ProjectLinkResult, ProjectSettings } from '@vercel-internals/types';
import { isZeroConfigBuild } from '../is-zero-config-build';
import {
getLinkedProject,
linkFolderToProject,
@@ -160,8 +161,7 @@ export default async function setupAndLink(
}
config.currentTeam = org.type === 'team' ? org.id : undefined;
const isZeroConfig =
!localConfig || !localConfig.builds || localConfig.builds.length === 0;
const isZeroConfig = !localConfig || isZeroConfigBuild(localConfig.builds);
try {
let settings: ProjectSettings = {};

View File

@@ -62,7 +62,12 @@ export async function readInput(
while (!input) {
try {
input = await client.input({ message });
const { val } = await client.prompt({
type: 'input',
name: 'val',
message,
});
input = val;
} catch (err: any) {
console.log(); // \n

View File

@@ -1,5 +1,5 @@
import { join } from 'path';
import frameworkList, { Framework } from '@vercel/frameworks';
import { frameworkList, Framework } from '@vercel/frameworks';
import {
detectFrameworks,
getWorkspacePackagePaths,

View File

@@ -97,13 +97,16 @@ async function getProjectLinkFromRepoLink(
} else {
const selectableProjects =
projects.length > 0 ? projects : repoLink.repoConfig.projects;
project = await client.select({
const { p } = await client.prompt({
name: 'p',
type: 'list',
message: `Please select a Project:`,
choices: selectableProjects.map(p => ({
value: p,
name: p.name,
})),
});
project = p;
}
if (project) {
return {

View File

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

View File

@@ -10,6 +10,6 @@ menu:
---
This is an example of a custom shortcode that you can put right into your content. You will need to add a form action to the the shortcode to make it work. Check out [Formspree](https://formspree.io/) for a simple, free form service.
This is an example of a custom shortcode that you can put right into your content. You will need to add a form action to the shortcode to make it work. Check out [Formspree](https://formspree.io/) for a simple, free form service.
{{< form-contact action="https://example.com" >}}

View File

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

View File

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

View File

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

View File

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

View File

@@ -119,7 +119,6 @@ test(
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
await testPath(200, `/api/headers`, (body: any, res: any) => {
// @ts-ignore
const { host } = new URL(res.url);
expect(body).toBe(host);
});
@@ -198,7 +197,6 @@ test(
);
await testPath(200, `/api/dump`, (body: any, res: any, isDev: any) => {
// @ts-ignore
const { host } = new URL(res.url);
const { env, headers } = JSON.parse(body);

View File

@@ -7,5 +7,8 @@
"dependencies": {
"exeggcute": "^1.0.0",
"once": "^1.4.0"
},
"engines": {
"node": "18.x"
}
}

View File

@@ -348,6 +348,9 @@ module.exports = async function prepare(session, binaryPath, tmpFixturesDir) {
},
'lambda-with-php-runtime': {
'api/test.php': `<?php echo 'Hello from PHP'; ?>`,
'package.json': JSON.stringify({
engines: { node: '18.x' },
}),
'vercel.json': JSON.stringify({
functions: {
'api/**/*.php': {

View File

@@ -1,3 +1,4 @@
import { afterAll } from 'vitest';
import findUp from 'find-up';
import fs from 'fs-extra';
import path from 'path';

View File

@@ -1,4 +1,3 @@
import stripAnsi from 'strip-ansi';
import type { CLIProcess } from './types';
function getPromptErrorDetails(
@@ -55,7 +54,7 @@ export default async function waitForPrompt(
};
const onData = (rawChunk: Buffer) => {
const chunk = stripAnsi(rawChunk.toString());
const chunk = rawChunk.toString();
mostRecentChunk = chunk;
console.log('> ' + chunk);

View File

@@ -617,7 +617,7 @@ test('ensure we render a prompt when deploying home directory', async () => {
binaryPath,
[directory, '--public', '--name', session, '--force'],
{
input: 'N\n',
input: 'N',
}
);
@@ -625,7 +625,7 @@ test('ensure we render a prompt when deploying home directory', async () => {
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
expect(stderr).toContain(
'You are deploying your home directory. Do you want to continue? (y/N)'
'You are deploying your home directory. Do you want to continue? [y/N]'
);
expect(stderr).toContain('Canceled');
});

View File

@@ -1,4 +1,5 @@
const originalCwd = process.cwd();
import { afterAll, beforeAll, afterEach } from 'vitest';
// Register Jest matcher extensions for CLI unit tests
import './matchers';
@@ -93,6 +94,8 @@ export class MockClient extends Client {
this.stderr.pause();
this.stderr.isTTY = true;
this._createPromptModule();
this.output = new Output(this.stderr);
this.argv = [];

View File

@@ -1,3 +1,4 @@
import { beforeEach } from 'vitest';
import { URL } from 'url';
import chance from 'chance';
import { client } from './client';

View File

@@ -1,63 +1,13 @@
/**
* This file registers the custom Jest "matchers" that are useful for
* writing CLI unit tests, and sets them up to be recognized by TypeScript.
*
* References:
* - https://haspar.us/notes/adding-jest-custom-matchers-in-typescript
* - https://gist.github.com/hasparus/4ebaa17ec5d3d44607f522bcb1cda9fb
*/
import { expect } from 'vitest';
import { toOutput } from './matchers';
/// <reference types="@types/jest" />
import * as matchers from './matchers';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type Tail<T extends unknown[]> = T extends [infer _Head, ...infer Tail]
? Tail
: never;
type AnyFunction = (...args: any[]) => any;
type PromiseFunction = (...args: any[]) => Promise<any>;
type GetMatcherType<TP, TResult> = TP extends PromiseFunction
? (...args: Tail<Parameters<TP>>) => Promise<TResult>
: TP extends AnyFunction
? (...args: Tail<Parameters<TP>>) => TResult
: TP;
type GetMatchersType<TMatchers, TResult> = {
[P in keyof TMatchers]: GetMatcherType<TMatchers[P], TResult>;
};
type FirstParam<T extends AnyFunction> = Parameters<T>[0];
type OnlyMethodsWhereFirstArgIsOfType<TObject, TWantedFirstArg> = {
[P in keyof TObject]: TObject[P] extends AnyFunction
? TWantedFirstArg extends FirstParam<TObject[P]>
? TObject[P]
: [
`Error: this function is present only when received is:`,
FirstParam<TObject[P]>
]
: TObject[P];
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Matchers<R, T = {}>
extends GetMatchersType<
OnlyMethodsWhereFirstArgIsOfType<typeof matchers, T>,
R
> {}
}
interface ToOutputMatchers<R = unknown> {
toOutput: (test: string, timeout?: number) => Promise<R>;
}
const jestExpect = (global as any).expect;
if (jestExpect !== undefined) {
jestExpect.extend(matchers);
} else {
console.error("Couldn't find Jest's global expect.");
declare module 'vitest' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Assertion<T = any> extends ToOutputMatchers<T> {}
}
expect.extend({ toOutput });

View File

@@ -5,16 +5,18 @@ import {
printReceived,
} from 'jest-matcher-utils';
import type { Readable } from 'stream';
import type { MatcherState } from 'expect';
import type { MatcherState } from '@vitest/expect';
import type { MatcherHintOptions } from 'jest-matcher-utils';
import stripAnsi from 'strip-ansi';
export async function toOutput(
this: MatcherState,
stream: Readable,
test: string,
timeout = 3000
) {
): Promise<{
pass: boolean;
message: () => string;
}> {
const { isNot } = this;
const matcherName = 'toOutput';
const matcherHintOptions: MatcherHintOptions = {
@@ -28,7 +30,7 @@ export async function toOutput(
matcherHint(matcherName, 'stream', 'test', matcherHintOptions) + '\n\n';
function onData(data: string) {
output += stripAnsi(data);
output += data;
if (output.includes(test)) {
cleanup();
resolve({

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`help command help output snapshots column width 40 1`] = `
exports[`help command > help output snapshots > column width 40 1`] = `
"
vercel deploy [project-path] [options]
@@ -186,7 +186,7 @@ exports[`help command help output snapshots column width 40 1`] = `
"
`;
exports[`help command help output snapshots column width 80 1`] = `
exports[`help command > help output snapshots > column width 80 1`] = `
"
vercel deploy [project-path] [options]
@@ -260,7 +260,7 @@ exports[`help command help output snapshots column width 80 1`] = `
"
`;
exports[`help command help output snapshots column width 120 1`] = `
exports[`help command > help output snapshots > column width 120 1`] = `
"
vercel deploy [project-path] [options]

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import alias from '../../../src/commands/alias';
import { useUser } from '../../mocks/user';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
import bisect from '../../../src/commands/bisect';

View File

@@ -1,3 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import fs from 'fs-extra';
import { join } from 'path';
import { getWriteableDirectory } from '@vercel/build-utils';
@@ -7,8 +8,9 @@ import { defaultProject, useProject } from '../../../mocks/project';
import { useTeams } from '../../../mocks/team';
import { useUser } from '../../../mocks/user';
import { execSync } from 'child_process';
import { vi } from 'vitest';
jest.setTimeout(6 * 60 * 1000);
vi.setConfig({ testTimeout: 6 * 60 * 1000 });
const fixture = (name: string) =>
join(__dirname, '../../../fixtures/unit/commands/build', name);
@@ -932,7 +934,7 @@ describe('build', () => {
name: 'Error',
message:
'Invalid vercel.json - `rewrites[2]` should NOT have additional property `src`. Did you mean `source`?',
stack: expect.stringContaining('at validateConfig'),
stack: expect.stringContaining('at Module.validateConfig'),
hideStackTrace: true,
code: 'INVALID_VERCEL_CONFIG',
link: 'https://vercel.com/docs/concepts/projects/project-configuration#rewrites',

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import certs from '../../../src/commands/certs';
import { useUser } from '../../mocks/user';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import bytes from 'bytes';
import fs from 'fs-extra';
import { join } from 'path';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import dns from '../../../src/commands/dns';
import { useUser } from '../../mocks/user';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import domains from '../../../src/commands/domains';
import { useUser } from '../../mocks/user';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import fs from 'fs-extra';
import path from 'path';
import { parse } from 'dotenv';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { join } from 'path';
import fs from 'fs-extra';
import { useUser } from '../../mocks/user';
@@ -79,7 +80,7 @@ describe('git', () => {
const gitPromise = git(client);
await expect(client.stderr).toOutput('Set up');
client.stdin.write('\n');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
'Which scope should contain your project?'
@@ -87,12 +88,12 @@ describe('git', () => {
client.stdin.write('\r');
await expect(client.stderr).toOutput('Found project');
client.stdin.write('\n');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Do you still want to connect https://github.com/user2/repo2?`
);
client.stdin.write('h\n'); // vim left to switch from default no
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Connecting Git remote: https://github.com/user2/repo2`
@@ -425,7 +426,7 @@ describe('git', () => {
await expect(client.stderr).toOutput(
`Are you sure you want to disconnect user/repo from your project?`
);
client.stdin.write('h\n'); // vim left to switch from default no
client.stdin.write('y\n');
await expect(client.stderr).toOutput('Disconnected user/repo.');
const newProjectData: Project = await client.fetch(
@@ -526,9 +527,9 @@ describe('git', () => {
`Found a repository in your local Git Config: https://github.com/user/repo`
);
await expect(client.stderr).toOutput(
`Do you still want to connect https://github.com/user2/repo2?`
`Do you still want to connect https://github.com/user2/repo2? [y/N]`
);
client.stdin.write('h\n'); // vim left to switch from default no
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Connecting Git remote: https://github.com/user2/repo2`
);
@@ -574,9 +575,9 @@ describe('git', () => {
`Found multiple Git repositories in your local Git config:\n • origin: https://github.com/user/repo.git\n • secondary: https://github.com/user/repo2.git`
);
await expect(client.stderr).toOutput(
`Do you still want to connect https://github.com/user3/repo3?`
`Do you still want to connect https://github.com/user3/repo3? [y/N]`
);
client.stdin.write('h\n'); // vim left to switch away from default no
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Connecting Git remote: https://github.com/user3/repo3`

View File

@@ -1,3 +1,4 @@
import { describe, expect, test } from 'vitest';
import {
help,
lineToString,

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
import { useDeployment } from '../../mocks/deployment';

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { basename, join } from 'path';
import { readJSON } from 'fs-extra';
import link from '../../../src/commands/link';
@@ -26,17 +27,16 @@ describe('link', () => {
client.cwd = cwd;
const exitCodePromise = link(client);
client.stderr.pipe(process.stderr);
await expect(client.stderr).toOutput('Set up');
client.stdin.write('\n');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
'Which scope should contain your project?'
);
client.stdin.write('\n');
client.stdin.write('y\n');
await expect(client.stderr).toOutput('Link to it?');
client.stdin.write('\n');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Linked to ${user.username}/${project.name} (created .vercel and added it to .gitignore)`

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import createLineIterator from 'line-async-iterator';
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';

View File

@@ -1,9 +1,11 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import login from '../../../src/commands/login';
import { emoji } from '../../../src/util/emoji';
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
import { vi } from 'vitest';
jest.setTimeout(10000);
vi.setConfig({ testTimeout: 10000 });
describe('login', () => {
it('should not allow the `--token` flag', async () => {
@@ -54,7 +56,7 @@ describe('login', () => {
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('Enter your email address:');
await expect(client.stderr).toOutput('? Enter your email address:');
client.stdin.write(`${user.email}\n`);
@@ -77,7 +79,7 @@ describe('login', () => {
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('Enter your email address:');
await expect(client.stderr).toOutput('? Enter your email address:');
client.stdin.write(`${user.email}\n`);
@@ -119,7 +121,7 @@ describe('login', () => {
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('Enter your email address:');
await expect(client.stderr).toOutput('? Enter your email address:');
client.stdin.write(`${user.email}\n`);
@@ -162,7 +164,7 @@ describe('login', () => {
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('Enter your email address:');
await expect(client.stderr).toOutput('? Enter your email address:');
client.stdin.write(`${user.email}\n`);

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import createLineIterator from 'line-async-iterator';
import projects from '../../../src/commands/project';
import { useUser } from '../../mocks/user';
@@ -6,6 +7,7 @@ import { defaultProject, useProject } from '../../mocks/project';
import { client } from '../../mocks/client';
import type { Project } from '@vercel-internals/types';
import { parseSpacedTableRow } from '../../helpers/parse-table';
import { vi } from 'vitest';
describe('project', () => {
describe('list', () => {
@@ -45,6 +47,55 @@ describe('project', () => {
expect(data).toEqual([project.project.name, 'https://foobar.com']);
});
it('should list projects running on an soon-to-be-deprecated Node.js version', async () => {
vi.useFakeTimers().setSystemTime(new Date('2023-12-08'));
const user = useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
nodeVersion: '16.x',
});
client.setArgv('project', 'ls', '--update-required');
await projects(client);
const lines = createLineIterator(client.stderr);
let line = await lines.next();
expect(line.value).toEqual(`Fetching projects in ${user.username}`);
line = await lines.next();
expect(line.value).toEqual(
'WARN! The following Node.js versions will be deprecated soon: 16.x. Please upgrade your projects immediately.'
);
line = await lines.next();
expect(line.value).toEqual(
'> For more information visit: https://vercel.com/docs/functions/serverless-functions/runtimes/node-js#node.js-version'
);
line = await lines.next();
expect(line.value).toContain(user.username);
// empty line
line = await lines.next();
expect(line.value).toEqual('');
line = await lines.next();
const header = parseSpacedTableRow(line.value!);
expect(header).toEqual([
'Project Name',
'Latest Production URL',
'Updated',
]);
line = await lines.next();
const data = parseSpacedTableRow(line.value!);
data.pop();
expect(data).toEqual([project.project.name, 'https://foobar.com']);
vi.clearAllTimers();
});
it('should list projects when there is no production deployment', async () => {
const user = useUser();
useTeams('team_dummy');

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import chalk from 'chalk';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
@@ -9,8 +10,9 @@ import { useDeployment } from '../../mocks/deployment';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
import sleep from '../../../src/util/sleep';
import { vi } from 'vitest';
jest.setTimeout(60000);
vi.setConfig({ testTimeout: 60000 });
describe('promote', () => {
it('should error if timeout is invalid', async () => {
@@ -113,7 +115,7 @@ describe('promote', () => {
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput(
'This deployment does not target production, therefore promotion will not \napply production environment variables. Are you sure you want to continue?'
'? This deployment does not target production, therefore promotion will not apply\n production environment variables. Are you sure you want to continue?'
);
// say "no" to the prompt
@@ -136,7 +138,7 @@ describe('promote', () => {
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
);
await expect(client.stderr).toOutput(
'This deployment does not target production, therefore promotion will not \napply production environment variables. Are you sure you want to continue?'
'? This deployment does not target production, therefore promotion will not apply\n production environment variables. Are you sure you want to continue?'
);
// say "yes" to the prompt

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import fs from 'fs-extra';
import path from 'path';
import pull from '../../../src/commands/pull';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
import redeploy from '../../../src/commands/redeploy';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import {
defaultProject,

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import chalk from 'chalk';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
@@ -9,8 +10,9 @@ import { useDeployment } from '../../mocks/deployment';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
import sleep from '../../../src/util/sleep';
import { vi } from 'vitest';
jest.setTimeout(60000);
vi.setConfig({ testTimeout: 60000 });
describe('rollback', () => {
it('should error if timeout is invalid', async () => {

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import secrets from '../../../src/commands/secrets';
import { useSecrets } from '../../mocks/secrets';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import teamsSwitch from '../../../src/commands/teams/switch';
import { useUser } from '../../mocks/user';

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { client } from '../../mocks/client';
import teamsList from '../../../src/commands/teams/list';
import { useUser } from '../../mocks/user';

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