Compare commits
122 Commits
@vercel/py
...
@vercel/ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb8b16cbd | ||
|
|
226bf02be2 | ||
|
|
8505872f55 | ||
|
|
7db6436797 | ||
|
|
e2d76e9c92 | ||
|
|
337cb21d67 | ||
|
|
6bfff3e9eb | ||
|
|
ac5b259c11 | ||
|
|
bfc553db11 | ||
|
|
2b101d4692 | ||
|
|
3316f38cb4 | ||
|
|
7837387127 | ||
|
|
f478200dd3 | ||
|
|
c29de8206a | ||
|
|
a2df3b5463 | ||
|
|
73446e544a | ||
|
|
21ff4a58c3 | ||
|
|
2b9eb02b8c | ||
|
|
4ef4722460 | ||
|
|
be5308b137 | ||
|
|
08a83a94f8 | ||
|
|
543ffdfe5c | ||
|
|
c11527e904 | ||
|
|
d296064386 | ||
|
|
400a6c42bd | ||
|
|
71b3ded398 | ||
|
|
fc3fa61b59 | ||
|
|
1f98c4fee7 | ||
|
|
1cb5a91727 | ||
|
|
e8c7db59cf | ||
|
|
57b230e25f | ||
|
|
ab3fb25790 | ||
|
|
88d98f7497 | ||
|
|
90c1895949 | ||
|
|
46a1f3670b | ||
|
|
4b025fee92 | ||
|
|
dc8293dc13 | ||
|
|
78883dea23 | ||
|
|
b5b792e42f | ||
|
|
8993a3c4af | ||
|
|
57241aad81 | ||
|
|
4773ff5efd | ||
|
|
d8c7308eb6 | ||
|
|
5df1c89138 | ||
|
|
f5d879143c | ||
|
|
9a55809515 | ||
|
|
56adf15823 | ||
|
|
1acab3d06c | ||
|
|
081b38466b | ||
|
|
c397fd1856 | ||
|
|
afd303b94a | ||
|
|
b12387034a | ||
|
|
5af65d5a24 | ||
|
|
1ee9a96a62 | ||
|
|
76130faf26 | ||
|
|
fb3601d178 | ||
|
|
aebfb6812d | ||
|
|
73999e7253 | ||
|
|
989dad5570 | ||
|
|
68c2dea601 | ||
|
|
63f2da2f68 | ||
|
|
57e5f81361 | ||
|
|
fd5e440533 | ||
|
|
2a45805b26 | ||
|
|
5523383e50 | ||
|
|
0ecbb24cab | ||
|
|
922223bd19 | ||
|
|
0ad7fd34f4 | ||
|
|
3d3774ee7e | ||
|
|
50f8eec7cb | ||
|
|
45374e2f90 | ||
|
|
fd9142b6f3 | ||
|
|
8cf67b549b | ||
|
|
5dc6f48e44 | ||
|
|
66c8544e8f | ||
|
|
0140db38fa | ||
|
|
e5421c27e8 | ||
|
|
5afc527233 | ||
|
|
de9518b010 | ||
|
|
c322d1dbba | ||
|
|
18c19ead76 | ||
|
|
9d80c27382 | ||
|
|
bef1aec766 | ||
|
|
4f4a42813f | ||
|
|
181a492d91 | ||
|
|
1be7a80bb8 | ||
|
|
0428d4744e | ||
|
|
2a929a4bb9 | ||
|
|
accd308dc5 | ||
|
|
e2d4efab08 | ||
|
|
7e0dd6f808 | ||
|
|
8971e02e49 | ||
|
|
10c91c8579 | ||
|
|
bfdbe58675 | ||
|
|
7bdaf107b7 | ||
|
|
8de100f0e1 | ||
|
|
38a6785859 | ||
|
|
c67d1a8525 | ||
|
|
c5a7c574a2 | ||
|
|
d2f8d178f7 | ||
|
|
f9a747764c | ||
|
|
27d80f13cd | ||
|
|
8c668c925d | ||
|
|
4b1b33c143 | ||
|
|
a8d4147554 | ||
|
|
09339f494d | ||
|
|
ee4d772ae9 | ||
|
|
61e8103404 | ||
|
|
fb4f477325 | ||
|
|
016bff848e | ||
|
|
183e411f7c | ||
|
|
070e300148 | ||
|
|
cbdf9b4a88 | ||
|
|
ec9b55dc81 | ||
|
|
06829bc21a | ||
|
|
628071f659 | ||
|
|
5a7461dfe3 | ||
|
|
599f8f675c | ||
|
|
0a8bc494fc | ||
|
|
34e008f42e | ||
|
|
037633b3f1 | ||
|
|
1a6f3c0270 |
1
.github/CONTRIBUTING.md
vendored
@@ -12,6 +12,7 @@ To get started, execute the following:
|
||||
|
||||
```
|
||||
git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
yarn install
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
|
||||
10
.github/workflows/test.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tests: ${{ steps['set-tests'].outputs['tests'] }}
|
||||
dplUrl: ${{ steps.waitForTarball.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: git --version
|
||||
@@ -32,6 +33,12 @@ jobs:
|
||||
echo "Files to test:"
|
||||
echo "$TESTS_ARRAY"
|
||||
echo "::set-output name=tests::$TESTS_ARRAY"
|
||||
- uses: patrickedqvist/wait-for-vercel-preview@ae34b392ef30297f2b672f9afb3c329bde9bd487
|
||||
id: waitForTarball
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
max_timeout: 360
|
||||
check_interval: 5
|
||||
|
||||
test:
|
||||
timeout-minutes: 120
|
||||
@@ -69,13 +76,14 @@ jobs:
|
||||
- run: yarn install --network-timeout 1000000
|
||||
|
||||
- name: Build ${{matrix.packageName}} and all its dependencies
|
||||
run: yarn turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
run: node_modules/.bin/turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
env:
|
||||
FORCE_COLOR: '1'
|
||||
- name: Test ${{matrix.packageName}}
|
||||
run: node_modules/.bin/turbo run test --cache-dir=".turbo" --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_CLI_VERSION: ${{ needs.setup.outputs.dplUrl }}/tarballs/vercel.tgz
|
||||
VERCEL_TEAM_TOKEN: ${{ secrets.VERCEL_TEAM_TOKEN }}
|
||||
VERCEL_REGISTRATION_URL: ${{ secrets.VERCEL_REGISTRATION_URL }}
|
||||
FORCE_COLOR: '1'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# https://prettier.io/docs/en/ignore.html
|
||||
|
||||
# ignore this file with an intentional syntax error
|
||||
# ignore these files with an intentional syntax error
|
||||
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js
|
||||
packages/cli/test/fixtures/unit/commands/build/node-error/api/typescript.ts
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Runtime Developer Reference
|
||||
|
||||
The following page is a reference for how to create a Runtime by implementing
|
||||
the Runtime API interface.
|
||||
the Runtime API interface. It's a way to add support for a new programming language to Vercel.
|
||||
|
||||
> Note: If you're the author of a web framework, please use the [Build Output API](https://vercel.com/docs/build-output-api/v3) instead to make your framework compatible with Vercel.
|
||||
|
||||
A Runtime is an npm module that implements the following interface:
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
"description": "API for the vercel/vercel repo",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"vercel-build": "node ../utils/run.js build all"
|
||||
"//TODO": "We should add this pkg to yarn workspaces"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "5.11.1",
|
||||
"got": "10.2.1",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"parse-github-url": "1.0.2",
|
||||
"tar-fs": "2.0.0",
|
||||
"unzip-stream": "0.3.0"
|
||||
|
||||
@@ -15,5 +15,5 @@ _Live Example: https://docusaurus-2-template.vercel.app_
|
||||
To get started with Docusaurus on Vercel, you can use the [Docusaurus CLI](https://v2.docusaurus.io/docs/installation#scaffold-project-website) to initialize the project:
|
||||
|
||||
```shell
|
||||
$ npx @docusaurus/init@next init my-website classic
|
||||
npx create-docusaurus@latest my-website classic
|
||||
```
|
||||
|
||||
3
examples/docusaurus-2/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
id: hola
|
||||
title: Hola
|
||||
author: Gao Wei
|
||||
author_title: Docusaurus Core Team
|
||||
author_url: https://github.com/wgao19
|
||||
author_image_url: https://avatars1.githubusercontent.com/u/2055384?v=4
|
||||
slug: first-blog-post
|
||||
title: First Blog Post
|
||||
authors:
|
||||
name: Gao Wei
|
||||
title: Docusaurus Core Team
|
||||
url: https://github.com/wgao19
|
||||
image_url: https://github.com/wgao19.png
|
||||
tags: [hola, docusaurus]
|
||||
---
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
id: hello-world
|
||||
title: Hello
|
||||
author: Endilie Yacop Sucipto
|
||||
author_title: Maintainer of Docusaurus
|
||||
author_url: https://github.com/endiliey
|
||||
author_image_url: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
Welcome to this blog. This blog is created with [**Docusaurus 2 alpha**](https://v2.docusaurus.io/).
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
This is a test post.
|
||||
|
||||
A whole bunch of other information.
|
||||
44
examples/docusaurus-2/blog/2019-05-29-long-blog-post.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
slug: long-blog-post
|
||||
title: Long Blog Post
|
||||
authors: endi
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
This is the summary of a very long blog post,
|
||||
|
||||
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
id: welcome
|
||||
title: Welcome
|
||||
author: Yangshun Tay
|
||||
author_title: Front End Engineer @ Facebook
|
||||
author_url: https://github.com/yangshun
|
||||
author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well!
|
||||
|
||||
Delete the whole directory if you don't want the blog features. As simple as that!
|
||||
20
examples/docusaurus-2/blog/2021-08-01-mdx-blog-post.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
slug: mdx-blog-post
|
||||
title: MDX Blog Post
|
||||
authors: [slorber]
|
||||
tags: [docusaurus]
|
||||
---
|
||||
|
||||
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||
|
||||
:::tip
|
||||
|
||||
Use the power of React to create interactive blog posts.
|
||||
|
||||
```js
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
```
|
||||
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
|
||||
:::
|
||||
|
After Width: | Height: | Size: 94 KiB |
25
examples/docusaurus-2/blog/2021-08-26-welcome/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
slug: welcome
|
||||
title: Welcome
|
||||
authors: [slorber, yangshun]
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||
|
||||
Simply add Markdown files (or folders) to the `blog` directory.
|
||||
|
||||
Regular blog authors can be added to `authors.yml`.
|
||||
|
||||
The blog post date can be extracted from filenames, such as:
|
||||
|
||||
- `2019-05-30-welcome.md`
|
||||
- `2019-05-30-welcome/index.md`
|
||||
|
||||
A blog post folder can be convenient to co-locate blog post images:
|
||||
|
||||

|
||||
|
||||
The blog supports tags as well!
|
||||
|
||||
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
||||
17
examples/docusaurus-2/blog/authors.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
endi:
|
||||
name: Endilie Yacop Sucipto
|
||||
title: Maintainer of Docusaurus
|
||||
url: https://github.com/endiliey
|
||||
image_url: https://github.com/endiliey.png
|
||||
|
||||
yangshun:
|
||||
name: Yangshun Tay
|
||||
title: Front End Engineer @ Facebook
|
||||
url: https://github.com/yangshun
|
||||
image_url: https://github.com/yangshun.png
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
id: doc1
|
||||
title: Style Guide
|
||||
sidebar_label: Style Guide
|
||||
---
|
||||
|
||||
You can write content using [GitHub-flavored Markdown syntax](https://github.github.com/gfm/).
|
||||
|
||||
## Markdown Syntax
|
||||
|
||||
To serve as an example page when styling markdown based Docusaurus sites.
|
||||
|
||||
## Headers
|
||||
|
||||
# H1 - Create the best documentation
|
||||
|
||||
## H2 - Create the best documentation
|
||||
|
||||
### H3 - Create the best documentation
|
||||
|
||||
#### H4 - Create the best documentation
|
||||
|
||||
##### H5 - Create the best documentation
|
||||
|
||||
###### H6 - Create the best documentation
|
||||
|
||||
---
|
||||
|
||||
## Emphasis
|
||||
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
---
|
||||
|
||||
## Lists
|
||||
|
||||
1. First ordered list item
|
||||
1. Another item ⋅⋅\* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number ⋅⋅1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ ⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅ ⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
- Unordered list can use asterisks
|
||||
|
||||
* Or minuses
|
||||
|
||||
- Or pluses
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][arbitrary case-insensitive reference text]
|
||||
|
||||
[I'm a relative reference to a repository file](../blob/master/LICENSE)
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links. http://www.example.com or <http://www.example.com> and sometimes example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
---
|
||||
|
||||
## Images
|
||||
|
||||
Here's our logo (hover to see the title text):
|
||||
|
||||
Inline-style: 
|
||||
|
||||
Reference-style: ![alt text][logo]
|
||||
|
||||
[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png 'Logo Title Text 2'
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
var s = 'JavaScript syntax highlighting';
|
||||
alert(s);
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
print(s)
|
||||
```
|
||||
|
||||
```
|
||||
No language indicated, so no syntax highlighting.
|
||||
But let's throw in a <b>tag</b>.
|
||||
```
|
||||
|
||||
```js {2}
|
||||
function highlightMe() {
|
||||
console.log('This line can be highlighted!');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- | :-----------: | -----: |
|
||||
| col 3 is | right-aligned | \$1600 |
|
||||
| col 2 is | centered | \$12 |
|
||||
| zebra stripes | are neat | \$1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
| Markdown | Less | Pretty |
|
||||
| -------- | --------- | ---------- |
|
||||
| _Still_ | `renders` | **nicely** |
|
||||
| 1 | 2 | 3 |
|
||||
|
||||
---
|
||||
|
||||
## Blockquotes
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text. This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can _put_ **Markdown** into a blockquote.
|
||||
|
||||
---
|
||||
|
||||
## Inline HTML
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
|
||||
---
|
||||
|
||||
## Line Breaks
|
||||
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a _separate paragraph_.
|
||||
|
||||
This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the _same paragraph_.
|
||||
|
||||
---
|
||||
|
||||
## Admonitions
|
||||
|
||||
:::note
|
||||
|
||||
This is a note
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
|
||||
This is a tip
|
||||
|
||||
:::
|
||||
|
||||
:::important
|
||||
|
||||
This is important
|
||||
|
||||
:::
|
||||
|
||||
:::caution
|
||||
|
||||
This is a caution
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
This is a warning
|
||||
|
||||
:::
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
id: doc2
|
||||
title: Document Number 2
|
||||
---
|
||||
|
||||
This is a link to [another document.](doc3.md) This is a link to an [external page.](http://www.example.com)
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
id: doc3
|
||||
title: This is Document Number 3
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ac euismod odio, eu consequat dui. Nullam molestie consectetur risus id imperdiet. Proin sodales ornare turpis, non mollis massa ultricies id. Nam at nibh scelerisque, feugiat ante non, dapibus tortor. Vivamus volutpat diam quis tellus elementum bibendum. Praesent semper gravida velit quis aliquam. Etiam in cursus neque. Nam lectus ligula, malesuada et mauris a, bibendum faucibus mi. Phasellus ut interdum felis. Phasellus in odio pulvinar, porttitor urna eget, fringilla lectus. Aliquam sollicitudin est eros. Mauris consectetur quam vitae mauris interdum hendrerit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
|
||||
Duis et egestas libero, imperdiet faucibus ipsum. Sed posuere eget urna vel feugiat. Vivamus a arcu sagittis, fermentum urna dapibus, congue lectus. Fusce vulputate porttitor nisl, ac cursus elit volutpat vitae. Nullam vitae ipsum egestas, convallis quam non, porta nibh. Morbi gravida erat nec neque bibendum, eu pellentesque velit posuere. Fusce aliquam erat eu massa eleifend tristique.
|
||||
|
||||
Sed consequat sollicitudin ipsum eget tempus. Integer a aliquet velit. In justo nibh, pellentesque non suscipit eget, gravida vel lacus. Donec odio ante, malesuada in massa quis, pharetra tristique ligula. Donec eros est, tristique eget finibus quis, semper non nisl. Vivamus et elit nec enim ornare placerat. Sed posuere odio a elit cursus sagittis.
|
||||
|
||||
Phasellus feugiat purus eu tortor ultrices finibus. Ut libero nibh, lobortis et libero nec, dapibus posuere eros. Sed sagittis euismod justo at consectetur. Nulla finibus libero placerat, cursus sapien at, eleifend ligula. Vivamus elit nisl, hendrerit ac nibh eu, ultrices tempus dui. Nam tellus neque, commodo non rhoncus eu, gravida in risus. Nullam id iaculis tortor.
|
||||
|
||||
Nullam at odio in sem varius tempor sit amet vel lorem. Etiam eu hendrerit nisl. Fusce nibh mauris, vulputate sit amet ex vitae, congue rhoncus nisl. Sed eget tellus purus. Nullam tempus commodo erat ut tristique. Cras accumsan massa sit amet justo consequat eleifend. Integer scelerisque vitae tellus id consectetur.
|
||||
47
examples/docusaurus-2/docs/intro.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Tutorial Intro
|
||||
|
||||
Let's discover **Docusaurus in less than 5 minutes**.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Get started by **creating a new site**.
|
||||
|
||||
Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) version 16.14 or above:
|
||||
- When installing Node.js, you are recommended to check all checkboxes related to dependencies.
|
||||
|
||||
## Generate a new site
|
||||
|
||||
Generate a new Docusaurus site using the **classic template**.
|
||||
|
||||
The classic template will automatically be added to your project after you run the command:
|
||||
|
||||
```bash
|
||||
npm init docusaurus@latest my-website classic
|
||||
```
|
||||
|
||||
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
|
||||
|
||||
The command also installs all necessary dependencies you need to run Docusaurus.
|
||||
|
||||
## Start your site
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
cd my-website
|
||||
npm run start
|
||||
```
|
||||
|
||||
The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.
|
||||
|
||||
The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.
|
||||
|
||||
Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
id: mdx
|
||||
title: Powered by MDX
|
||||
---
|
||||
|
||||
You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/).
|
||||
|
||||
export const Highlight = ({children, color}) => ( <span style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '2px',
|
||||
color: '#fff',
|
||||
padding: '0.2rem',
|
||||
}}> {children} </span> );
|
||||
|
||||
<Highlight color="#25c2a0">Docusaurus green</Highlight> and <Highlight color="#1877F2">Facebook blue</Highlight> are my favorite colors.
|
||||
|
||||
I can write **Markdown** alongside my _JSX_!
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Tutorial - Basics",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "5 minutes to learn the most important Docusaurus concepts."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Congratulations!
|
||||
|
||||
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
|
||||
|
||||
Docusaurus has **much more to offer**!
|
||||
|
||||
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
|
||||
|
||||
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
|
||||
|
||||
## What's next?
|
||||
|
||||
- Read the [official documentation](https://docusaurus.io/).
|
||||
- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout)
|
||||
- Add a [search bar](https://docusaurus.io/docs/search)
|
||||
- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase)
|
||||
- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Create a Blog Post
|
||||
|
||||
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
|
||||
|
||||
## Create your first Post
|
||||
|
||||
Create a file at `blog/2021-02-28-greetings.md`:
|
||||
|
||||
```md title="blog/2021-02-28-greetings.md"
|
||||
---
|
||||
slug: greetings
|
||||
title: Greetings!
|
||||
authors:
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
- name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
tags: [greetings]
|
||||
---
|
||||
|
||||
Congratulations, you have made your first post!
|
||||
|
||||
Feel free to play around and edit this post as much you like.
|
||||
```
|
||||
|
||||
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Create a Document
|
||||
|
||||
Documents are **groups of pages** connected through:
|
||||
|
||||
- a **sidebar**
|
||||
- **previous/next navigation**
|
||||
- **versioning**
|
||||
|
||||
## Create your first Doc
|
||||
|
||||
Create a Markdown file at `docs/hello.md`:
|
||||
|
||||
```md title="docs/hello.md"
|
||||
# Hello
|
||||
|
||||
This is my **first Docusaurus document**!
|
||||
```
|
||||
|
||||
A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello).
|
||||
|
||||
## Configure the Sidebar
|
||||
|
||||
Docusaurus automatically **creates a sidebar** from the `docs` folder.
|
||||
|
||||
Add metadata to customize the sidebar label and position:
|
||||
|
||||
```md title="docs/hello.md" {1-4}
|
||||
---
|
||||
sidebar_label: 'Hi!'
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
This is my **first Docusaurus document**!
|
||||
```
|
||||
|
||||
It is also possible to create your sidebar explicitly in `sidebars.js`:
|
||||
|
||||
```js title="sidebars.js"
|
||||
module.exports = {
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
// highlight-next-line
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
43
examples/docusaurus-2/docs/tutorial-basics/create-a-page.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Create a Page
|
||||
|
||||
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
|
||||
|
||||
- `src/pages/index.js` → `localhost:3000/`
|
||||
- `src/pages/foo.md` → `localhost:3000/foo`
|
||||
- `src/pages/foo/bar.js` → `localhost:3000/foo/bar`
|
||||
|
||||
## Create your first React Page
|
||||
|
||||
Create a file at `src/pages/my-react-page.js`:
|
||||
|
||||
```jsx title="src/pages/my-react-page.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
export default function MyReactPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>My React page</h1>
|
||||
<p>This is a React page</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page).
|
||||
|
||||
## Create your first Markdown Page
|
||||
|
||||
Create a file at `src/pages/my-markdown-page.md`:
|
||||
|
||||
```mdx title="src/pages/my-markdown-page.md"
|
||||
# My Markdown page
|
||||
|
||||
This is a Markdown page
|
||||
```
|
||||
|
||||
A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Deploy your site
|
||||
|
||||
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
|
||||
|
||||
It builds your site as simple **static HTML, JavaScript and CSS files**.
|
||||
|
||||
## Build your site
|
||||
|
||||
Build your site **for production**:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The static files are generated in the `build` folder.
|
||||
|
||||
## Deploy your site
|
||||
|
||||
Test your production build locally:
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**).
|
||||
146
examples/docusaurus-2/docs/tutorial-basics/markdown-features.mdx
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Markdown Features
|
||||
|
||||
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
|
||||
|
||||
## Front Matter
|
||||
|
||||
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
|
||||
|
||||
```text title="my-doc.md"
|
||||
// highlight-start
|
||||
---
|
||||
id: my-doc-id
|
||||
title: My document title
|
||||
description: My document description
|
||||
slug: /my-custom-url
|
||||
---
|
||||
// highlight-end
|
||||
|
||||
## Markdown heading
|
||||
|
||||
Markdown text with [links](./hello.md)
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
Regular Markdown links are supported, using url paths or relative file paths.
|
||||
|
||||
```md
|
||||
Let's see how to [Create a page](/create-a-page).
|
||||
```
|
||||
|
||||
```md
|
||||
Let's see how to [Create a page](./create-a-page.md).
|
||||
```
|
||||
|
||||
**Result:** Let's see how to [Create a page](./create-a-page.md).
|
||||
|
||||
## Images
|
||||
|
||||
Regular Markdown images are supported.
|
||||
|
||||
You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`):
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||

|
||||
|
||||
You can reference images relative to the current file as well, as shown in [the extra guides](../tutorial-extras/manage-docs-versions.md).
|
||||
|
||||
## Code Blocks
|
||||
|
||||
Markdown code blocks are supported with Syntax highlighting.
|
||||
|
||||
```jsx title="src/components/HelloDocusaurus.js"
|
||||
function HelloDocusaurus() {
|
||||
return (
|
||||
<h1>Hello, Docusaurus!</h1>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="src/components/HelloDocusaurus.js"
|
||||
function HelloDocusaurus() {
|
||||
return <h1>Hello, Docusaurus!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Admonitions
|
||||
|
||||
Docusaurus has a special syntax to create admonitions and callouts:
|
||||
|
||||
:::tip My tip
|
||||
|
||||
Use this awesome feature option
|
||||
|
||||
:::
|
||||
|
||||
:::danger Take care
|
||||
|
||||
This action is dangerous
|
||||
|
||||
:::
|
||||
|
||||
:::tip My tip
|
||||
|
||||
Use this awesome feature option
|
||||
|
||||
:::
|
||||
|
||||
:::danger Take care
|
||||
|
||||
This action is dangerous
|
||||
|
||||
:::
|
||||
|
||||
## MDX and React Components
|
||||
|
||||
[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**:
|
||||
|
||||
```jsx
|
||||
export const Highlight = ({children, color}) => (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '20px',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
alert(`You clicked the color ${color} with label ${children}`)
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
|
||||
|
||||
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
|
||||
```
|
||||
|
||||
export const Highlight = ({children, color}) => (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '20px',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
alert(`You clicked the color ${color} with label ${children}`);
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
|
||||
|
||||
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Tutorial - Extras",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,55 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Manage Docs Versions
|
||||
|
||||
Docusaurus can manage multiple versions of your docs.
|
||||
|
||||
## Create a docs version
|
||||
|
||||
Release a version 1.0 of your project:
|
||||
|
||||
```bash
|
||||
npm run docusaurus docs:version 1.0
|
||||
```
|
||||
|
||||
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
|
||||
|
||||
Your docs now have 2 versions:
|
||||
|
||||
- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs
|
||||
- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs**
|
||||
|
||||
## Add a Version Dropdown
|
||||
|
||||
To navigate seamlessly across versions, add a version dropdown.
|
||||
|
||||
Modify the `docusaurus.config.js` file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
// highlight-start
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The docs version dropdown appears in your navbar:
|
||||
|
||||

|
||||
|
||||
## Update an existing version
|
||||
|
||||
It is possible to edit versioned docs in their respective folder:
|
||||
|
||||
- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello`
|
||||
- `docs/hello.md` updates `http://localhost:3000/docs/next/hello`
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Translate your site
|
||||
|
||||
Let's translate `docs/intro.md` to French.
|
||||
|
||||
## Configure i18n
|
||||
|
||||
Modify `docusaurus.config.js` to add support for the `fr` locale:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Translate a doc
|
||||
|
||||
Copy the `docs/intro.md` file to the `i18n/fr` folder:
|
||||
|
||||
```bash
|
||||
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
|
||||
|
||||
cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md
|
||||
```
|
||||
|
||||
Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French.
|
||||
|
||||
## Start your localized site
|
||||
|
||||
Start your site on the French locale:
|
||||
|
||||
```bash
|
||||
npm run start -- --locale fr
|
||||
```
|
||||
|
||||
Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated.
|
||||
|
||||
:::caution
|
||||
|
||||
In development, you can only use one locale at a same time.
|
||||
|
||||
:::
|
||||
|
||||
## Add a Locale Dropdown
|
||||
|
||||
To navigate seamlessly across languages, add a locale dropdown.
|
||||
|
||||
Modify the `docusaurus.config.js` file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
// highlight-start
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The locale dropdown now appears in your navbar:
|
||||
|
||||

|
||||
|
||||
## Build your localized site
|
||||
|
||||
Build your site for a specific locale:
|
||||
|
||||
```bash
|
||||
npm run build -- --locale fr
|
||||
```
|
||||
|
||||
Or build your site to include all the locales at once:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
@@ -1,103 +1,132 @@
|
||||
module.exports = {
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require('prism-react-renderer/themes/github');
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'My Site',
|
||||
tagline: 'The tagline of my site',
|
||||
tagline: 'Dinosaurs are cool',
|
||||
url: 'https://your-docusaurus-test-site.com',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'facebook', // Usually your GitHub org/user name.
|
||||
projectName: 'docusaurus', // Usually your repo name.
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
title: 'My Site',
|
||||
logo: {
|
||||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
links: [
|
||||
{
|
||||
to: 'docs/doc1',
|
||||
activeBasePath: 'docs',
|
||||
label: 'Docs',
|
||||
position: 'left',
|
||||
},
|
||||
{to: 'blog', label: 'Blog', position: 'left'},
|
||||
{
|
||||
href: 'https://github.com/facebook/docusaurus',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{
|
||||
label: 'Style Guide',
|
||||
to: 'docs/doc1',
|
||||
},
|
||||
{
|
||||
label: 'Second Doc',
|
||||
to: 'docs/doc2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discordapp.com/invite/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/docusaurus',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'Blog',
|
||||
to: 'blog',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/facebook/docusaurus',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
|
||||
},
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'@docusaurus/preset-classic',
|
||||
{
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website/',
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website/blog/',
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
title: 'My Site',
|
||||
logo: {
|
||||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
docId: 'intro',
|
||||
position: 'left',
|
||||
label: 'Tutorial',
|
||||
},
|
||||
{to: '/blog', label: 'Blog', position: 'left'},
|
||||
{
|
||||
href: 'https://github.com/facebook/docusaurus',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{
|
||||
label: 'Tutorial',
|
||||
to: '/docs/intro',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discordapp.com/invite/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/docusaurus',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'Blog',
|
||||
to: '/blog',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/facebook/docusaurus',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
{
|
||||
"name": "docusaurus-2",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy"
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-alpha.54",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.54",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4"
|
||||
"@docusaurus/core": "2.0.1",
|
||||
"@docusaurus/preset-classic": "2.0.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
@@ -26,5 +34,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
module.exports = {
|
||||
someSidebar: {
|
||||
Docusaurus: ['doc1', 'doc2', 'doc3'],
|
||||
Features: ['mdx'],
|
||||
},
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const FeatureList = [
|
||||
{
|
||||
title: 'Easy to Use',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Focus on What Matters',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Powered by React',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({Svg, title, description}) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* stylelint-disable docusaurus/copyright-header */
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
@@ -7,19 +6,25 @@
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: rgb(33, 175, 144);
|
||||
--ifm-color-primary-darker: rgb(31, 165, 136);
|
||||
--ifm-color-primary-darkest: rgb(26, 136, 112);
|
||||
--ifm-color-primary-light: rgb(70, 203, 174);
|
||||
--ifm-color-primary-lighter: rgb(102, 212, 189);
|
||||
--ifm-color-primary-lightest: rgb(146, 224, 208);
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgb(72, 77, 91);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@@ -1,97 +1,41 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import styles from './styles.module.css';
|
||||
import Layout from '@theme/Layout';
|
||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: <>Easy to Use</>,
|
||||
imageUrl: 'img/undraw_docusaurus_mountain.svg',
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <>Focus on What Matters</>,
|
||||
imageUrl: 'img/undraw_docusaurus_tree.svg',
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <>Powered by React</>,
|
||||
imageUrl: 'img/undraw_docusaurus_react.svg',
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
import styles from './index.module.css';
|
||||
|
||||
function Feature({imageUrl, title, description}) {
|
||||
const imgUrl = useBaseUrl(imageUrl);
|
||||
function HomepageHeader() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<div className={classnames('col col--4', styles.feature)}>
|
||||
{imgUrl && (
|
||||
<div className="text--center">
|
||||
<img className={styles.featureImage} src={imgUrl} alt={title} />
|
||||
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro">
|
||||
Docusaurus Tutorial - 5min ⏱️
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const context = useDocusaurusContext();
|
||||
const {siteConfig = {}} = context;
|
||||
export default function Home() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />">
|
||||
<header className={classnames('hero hero--primary', styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className={classnames(
|
||||
'button button--outline button--secondary button--lg',
|
||||
styles.getStarted,
|
||||
)}
|
||||
to={useBaseUrl('docs/doc1')}>
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
{features && features.length && (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{features.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* stylelint-disable docusaurus/copyright-header */
|
||||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
@@ -11,7 +10,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 966px) {
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
}
|
||||
@@ -22,15 +21,3 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureImage {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
7
examples/docusaurus-2/src/pages/markdown-page.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
||||
0
examples/docusaurus-2/static/.nojekyll
Normal file
BIN
examples/docusaurus-2/static/img/docusaurus.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 3.5 KiB |
@@ -1,4 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
|
||||
<title>Easy to Use</title>
|
||||
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
|
||||
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
|
||||
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -1,4 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
|
||||
<title>Powered by React</title>
|
||||
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
|
||||
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
|
||||
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -26,12 +26,12 @@
|
||||
"jest": "28.0.2",
|
||||
"json5": "2.1.1",
|
||||
"lint-staged": "9.2.5",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"npm-package-arg": "6.1.0",
|
||||
"prettier": "2.6.2",
|
||||
"ts-eager": "2.0.2",
|
||||
"ts-jest": "28.0.5",
|
||||
"turbo": "1.3.1"
|
||||
"turbo": "1.3.2-canary.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lerna": "lerna",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -14,8 +14,7 @@
|
||||
"build": "node build",
|
||||
"test": "jest --env node --verbose --runInBand --bail",
|
||||
"test-unit": "yarn test test/unit.*test.*",
|
||||
"test-integration-once": "yarn test test/integration.test.ts",
|
||||
"prepublishOnly": "node build"
|
||||
"test-integration-once": "yarn test test/integration.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iarna/toml": "2.2.3",
|
||||
@@ -35,7 +34,6 @@
|
||||
"aggregate-error": "3.0.1",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "2.1.4",
|
||||
"boxen": "4.2.0",
|
||||
"cross-spawn": "6.0.5",
|
||||
"end-of-stream": "1.4.1",
|
||||
"fs-extra": "10.0.0",
|
||||
@@ -44,7 +42,7 @@
|
||||
"js-yaml": "3.13.1",
|
||||
"minimatch": "3.0.4",
|
||||
"multistream": "2.1.1",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"semver": "6.1.1",
|
||||
"typescript": "4.3.4",
|
||||
"yazl": "2.5.1"
|
||||
|
||||
@@ -33,6 +33,11 @@ export class EdgeFunction {
|
||||
*/
|
||||
envVarsInUse?: string[];
|
||||
|
||||
/**
|
||||
* Extra binary files to be included in the edge function
|
||||
*/
|
||||
assets?: { name: string; path: string }[];
|
||||
|
||||
constructor(params: Omit<EdgeFunction, 'type'>) {
|
||||
this.type = 'EdgeFunction';
|
||||
this.name = params.name;
|
||||
@@ -40,5 +45,6 @@ export class EdgeFunction {
|
||||
this.entrypoint = params.entrypoint;
|
||||
this.files = params.files;
|
||||
this.envVarsInUse = params.envVarsInUse;
|
||||
this.assets = params.assets;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,7 @@ async function prepareSymlinkTarget(
|
||||
}
|
||||
|
||||
if (file.type === 'FileRef' || file.type === 'FileBlob') {
|
||||
const targetPathBufferPromise = await streamToBuffer(
|
||||
await file.toStreamAsync()
|
||||
);
|
||||
const targetPathBufferPromise = streamToBuffer(await file.toStreamAsync());
|
||||
const [targetPathBuffer] = await Promise.all([
|
||||
targetPathBufferPromise,
|
||||
mkdirPromise,
|
||||
@@ -42,9 +40,15 @@ async function prepareSymlinkTarget(
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadFile(file: File, fsPath: string): Promise<FileFsRef> {
|
||||
export async function downloadFile(
|
||||
file: File,
|
||||
fsPath: string
|
||||
): Promise<FileFsRef> {
|
||||
const { mode } = file;
|
||||
|
||||
// If the source is a symlink, try to create it instead of copying the file.
|
||||
// Note: creating symlinks on Windows requires admin priviliges or symlinks
|
||||
// enabled in the group policy. We may want to improve the error message.
|
||||
if (isSymbolicLink(mode)) {
|
||||
const target = await prepareSymlinkTarget(file, fsPath);
|
||||
|
||||
@@ -92,12 +96,28 @@ export default async function download(
|
||||
await removeFile(basePath, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// If a file didn't change, do not re-download it.
|
||||
if (Array.isArray(filesChanged) && !filesChanged.includes(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Some builders resolve symlinks and return both
|
||||
// a file, node_modules/<symlink>/package.json, and
|
||||
// node_modules/<symlink>, a symlink.
|
||||
// Removing the file matches how the yazl lambda zip
|
||||
// behaves so we can use download() with `vercel build`.
|
||||
const parts = name.split('/');
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const dir = parts.slice(0, i).join('/');
|
||||
const parent = files[dir];
|
||||
if (parent && isSymbolicLink(parent.mode)) {
|
||||
console.warn(
|
||||
`Warning: file "${name}" is within a symlinked directory "${dir}" and will be ignored`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const file = files[name];
|
||||
const fsPath = path.join(basePath, name);
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ function getHint(isAuto = false) {
|
||||
: `Please set "engines": { "node": "${range}" } in your \`package.json\` file to use Node.js ${major}.`;
|
||||
}
|
||||
|
||||
const upstreamProvider =
|
||||
'This change is the result of a decision made by an upstream infrastructure provider (AWS).';
|
||||
|
||||
export function getLatestNodeVersion() {
|
||||
return allOptions[0];
|
||||
}
|
||||
@@ -75,7 +72,7 @@ export async function getSupportedNodeVersion(
|
||||
throw new NowBuildError({
|
||||
code: 'BUILD_UTILS_NODE_VERSION_DISCONTINUED',
|
||||
link: 'http://vercel.link/node-version',
|
||||
message: `${intro} ${getHint(isAuto)} ${upstreamProvider}`,
|
||||
message: `${intro} ${getHint(isAuto)}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,9 +83,9 @@ export async function getSupportedNodeVersion(
|
||||
console.warn(
|
||||
`Error: Node.js version ${
|
||||
selection.range
|
||||
} is deprecated. Deployments created on or after ${d} will fail to build. ${getHint(
|
||||
} has reached End-of-Life. Deployments created on or after ${d} will fail to build. ${getHint(
|
||||
isAuto
|
||||
)} ${upstreamProvider}`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -305,67 +305,46 @@ export async function scanParentDirs(
|
||||
): Promise<ScanParentDirsResult> {
|
||||
assert(path.isAbsolute(destPath));
|
||||
|
||||
let cliType: CliType = 'yarn';
|
||||
let packageJson: PackageJson | undefined;
|
||||
let packageJsonPath: string | undefined;
|
||||
let currentDestPath = destPath;
|
||||
const pkgJsonPath = await walkParentDirs({
|
||||
base: '/',
|
||||
start: destPath,
|
||||
filename: 'package.json',
|
||||
});
|
||||
const packageJson: PackageJson | undefined =
|
||||
readPackageJson && pkgJsonPath
|
||||
? JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'))
|
||||
: undefined;
|
||||
const [yarnLockPath, npmLockPath, pnpmLockPath] = await walkParentDirsMulti({
|
||||
base: '/',
|
||||
start: destPath,
|
||||
filenames: ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'],
|
||||
});
|
||||
let lockfileVersion: number | undefined;
|
||||
let cliType: CliType = 'yarn';
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
packageJsonPath = path.join(currentDestPath, 'package.json');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
// Only read the contents of the *first* `package.json` file found,
|
||||
// since that's the one related to this installation.
|
||||
if (readPackageJson && !packageJson) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
}
|
||||
const [hasYarnLock, packageLockJson, pnpmLockYaml] = await Promise.all([
|
||||
Boolean(yarnLockPath),
|
||||
npmLockPath
|
||||
? readConfigFile<{ lockfileVersion: number }>(npmLockPath)
|
||||
: null,
|
||||
pnpmLockPath
|
||||
? readConfigFile<{ lockfileVersion: number }>(pnpmLockPath)
|
||||
: null,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [packageLockJson, hasYarnLock, pnpmLockYaml] = await Promise.all([
|
||||
fs
|
||||
.readJson(path.join(currentDestPath, 'package-lock.json'))
|
||||
.catch(error => {
|
||||
// If the file doesn't exist, fail gracefully otherwise error
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
fs.pathExists(path.join(currentDestPath, 'yarn.lock')),
|
||||
readConfigFile<{ lockfileVersion: number }>(
|
||||
path.join(currentDestPath, 'pnpm-lock.yaml')
|
||||
),
|
||||
]);
|
||||
|
||||
// Priority order is Yarn > pnpm > npm
|
||||
// - find highest priority lock file and use that
|
||||
if (hasYarnLock) {
|
||||
cliType = 'yarn';
|
||||
} else if (pnpmLockYaml) {
|
||||
cliType = 'pnpm';
|
||||
// just ensure that it is read as a number and not a string
|
||||
lockfileVersion = Number(pnpmLockYaml.lockfileVersion);
|
||||
} else if (packageLockJson) {
|
||||
cliType = 'npm';
|
||||
lockfileVersion = packageLockJson.lockfileVersion;
|
||||
}
|
||||
|
||||
// Only stop iterating if a lockfile was found, because it's possible
|
||||
// that the lockfile is in a higher path than where the `package.json`
|
||||
// file was found.
|
||||
if (packageLockJson || hasYarnLock || pnpmLockYaml) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newDestPath = path.dirname(currentDestPath);
|
||||
if (currentDestPath === newDestPath) break;
|
||||
currentDestPath = newDestPath;
|
||||
// Priority order is Yarn > pnpm > npm
|
||||
if (hasYarnLock) {
|
||||
cliType = 'yarn';
|
||||
} else if (pnpmLockYaml) {
|
||||
cliType = 'pnpm';
|
||||
// just ensure that it is read as a number and not a string
|
||||
lockfileVersion = Number(pnpmLockYaml.lockfileVersion);
|
||||
} else if (packageLockJson) {
|
||||
cliType = 'npm';
|
||||
lockfileVersion = packageLockJson.lockfileVersion;
|
||||
}
|
||||
|
||||
const packageJsonPath = pkgJsonPath || undefined;
|
||||
return { cliType, packageJson, lockfileVersion, packageJsonPath };
|
||||
}
|
||||
|
||||
@@ -387,11 +366,48 @@ export async function walkParentDirs({
|
||||
}
|
||||
|
||||
parent = path.dirname(current);
|
||||
|
||||
if (parent === current) {
|
||||
// Reached root directory of the filesystem
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function walkParentDirsMulti({
|
||||
base,
|
||||
start,
|
||||
filenames,
|
||||
}: {
|
||||
base: string;
|
||||
start: string;
|
||||
filenames: string[];
|
||||
}): Promise<(string | undefined)[]> {
|
||||
let parent = '';
|
||||
for (let current = start; base.length <= current.length; current = parent) {
|
||||
const fullPaths = filenames.map(f => path.join(current, f));
|
||||
const existResults = await Promise.all(
|
||||
fullPaths.map(f => fs.pathExists(f))
|
||||
);
|
||||
const foundOneOrMore = existResults.some(b => b);
|
||||
|
||||
if (foundOneOrMore) {
|
||||
return fullPaths.map((f, i) => (existResults[i] ? f : undefined));
|
||||
}
|
||||
|
||||
parent = path.dirname(current);
|
||||
|
||||
if (parent === current) {
|
||||
// Reached root directory of the filesystem
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function isSet<T>(v: any): v is Set<T> {
|
||||
return v?.constructor?.name === 'Set';
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import FileRef from './file-ref';
|
||||
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
|
||||
import { NodejsLambda } from './nodejs-lambda';
|
||||
import { Prerender } from './prerender';
|
||||
import download, { DownloadedFiles, isSymbolicLink } from './fs/download';
|
||||
import download, {
|
||||
downloadFile,
|
||||
DownloadedFiles,
|
||||
isSymbolicLink,
|
||||
} from './fs/download';
|
||||
import getWriteableDirectory from './fs/get-writable-directory';
|
||||
import glob, { GlobOptions } from './fs/glob';
|
||||
import rename from './fs/rename';
|
||||
@@ -46,6 +50,7 @@ export {
|
||||
createLambda,
|
||||
Prerender,
|
||||
download,
|
||||
downloadFile,
|
||||
DownloadedFiles,
|
||||
getWriteableDirectory,
|
||||
glob,
|
||||
|
||||
20
packages/build-utils/test/integration.test.ts
vendored
@@ -1,22 +1,12 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
packAndDeploy,
|
||||
testDeployment,
|
||||
// @ts-ignore
|
||||
} from '../../../test/lib/deployment/test-deployment';
|
||||
|
||||
jest.setTimeout(4 * 60 * 1000);
|
||||
|
||||
const builderUrl = '@canary';
|
||||
let buildUtilsUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildUtilsPath = path.resolve(__dirname, '..');
|
||||
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
|
||||
console.log('buildUtilsUrl', buildUtilsUrl);
|
||||
});
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
// Fixtures that have separate tests and should be skipped in the loop
|
||||
@@ -42,10 +32,7 @@ for (const fixture of fs.readdirSync(fixturesPath)) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`Should build "${fixture}"`, async () => {
|
||||
await expect(
|
||||
testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
path.join(fixturesPath, fixture)
|
||||
)
|
||||
testDeployment(path.join(fixturesPath, fixture))
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
}
|
||||
@@ -68,10 +55,7 @@ for (const builder of buildersToTestWith) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`Should build "${builder}/${fixture}"`, async () => {
|
||||
await expect(
|
||||
testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
path.join(fixturesPath2, fixture)
|
||||
)
|
||||
testDeployment(path.join(fixturesPath2, fixture))
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
}
|
||||
|
||||
139
packages/build-utils/test/unit.test.ts
vendored
@@ -1,6 +1,7 @@
|
||||
import ms from 'ms';
|
||||
import path from 'path';
|
||||
import fs, { readlink } from 'fs-extra';
|
||||
import retry from 'async-retry';
|
||||
import { strict as assert, strictEqual } from 'assert';
|
||||
import { createZip } from '../src/lambda';
|
||||
import { getSupportedNodeVersion } from '../src/fs/node-version';
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
Meta,
|
||||
} from '../src';
|
||||
|
||||
jest.setTimeout(7 * 1000);
|
||||
jest.setTimeout(10 * 1000);
|
||||
|
||||
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
||||
let result;
|
||||
@@ -169,6 +170,53 @@ it('should create zip files with symlinks properly', async () => {
|
||||
assert(aStat.isFile());
|
||||
});
|
||||
|
||||
it('should download symlinks even with incorrect file', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log('Skipping test on windows');
|
||||
return;
|
||||
}
|
||||
const files = {
|
||||
'dir/file.txt': new FileBlob({
|
||||
mode: 33188,
|
||||
contentType: undefined,
|
||||
data: 'file text',
|
||||
}),
|
||||
linkdir: new FileBlob({
|
||||
mode: 41453,
|
||||
contentType: undefined,
|
||||
data: 'dir',
|
||||
}),
|
||||
'linkdir/file.txt': new FileBlob({
|
||||
mode: 33188,
|
||||
contentType: undefined,
|
||||
data: 'this file should be discarded',
|
||||
}),
|
||||
};
|
||||
|
||||
const outDir = path.join(__dirname, 'symlinks-out');
|
||||
await fs.remove(outDir);
|
||||
await fs.mkdirp(outDir);
|
||||
|
||||
await download(files, outDir);
|
||||
|
||||
const [dir, file, linkdir] = await Promise.all([
|
||||
fs.lstat(path.join(outDir, 'dir')),
|
||||
fs.lstat(path.join(outDir, 'dir/file.txt')),
|
||||
fs.lstat(path.join(outDir, 'linkdir')),
|
||||
]);
|
||||
expect(dir.isFile()).toBe(false);
|
||||
expect(dir.isSymbolicLink()).toBe(false);
|
||||
|
||||
expect(file.isFile()).toBe(true);
|
||||
expect(file.isSymbolicLink()).toBe(false);
|
||||
|
||||
expect(linkdir.isSymbolicLink()).toBe(true);
|
||||
|
||||
expect(warningMessages).toEqual([
|
||||
'Warning: file "linkdir/file.txt" is within a symlinked directory "linkdir" and will be ignored',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should only match supported node versions, otherwise throw an error', async () => {
|
||||
expect(await getSupportedNodeVersion('12.x', false)).toHaveProperty(
|
||||
'major',
|
||||
@@ -386,10 +434,10 @@ it('should warn for deprecated versions, soon to be discontinued', async () => {
|
||||
12
|
||||
);
|
||||
expect(warningMessages).toStrictEqual([
|
||||
'Error: Node.js version 10.x is deprecated. Deployments created on or after 2021-04-20 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).',
|
||||
'Error: Node.js version 10.x is deprecated. Deployments created on or after 2021-04-20 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).',
|
||||
'Error: Node.js version 12.x is deprecated. Deployments created on or after 2022-08-09 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).',
|
||||
'Error: Node.js version 12.x is deprecated. Deployments created on or after 2022-08-09 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16. This change is the result of a decision made by an upstream infrastructure provider (AWS).',
|
||||
'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
|
||||
'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
|
||||
'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-08-09 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
|
||||
'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-08-09 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
|
||||
]);
|
||||
|
||||
global.Date.now = realDateNow;
|
||||
@@ -453,6 +501,7 @@ it('should return lockfileVersion 2 with npm7', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(2);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should not return lockfileVersion with yarn', async () => {
|
||||
@@ -460,6 +509,7 @@ it('should not return lockfileVersion with yarn', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should return lockfileVersion 1 with older versions of npm', async () => {
|
||||
@@ -467,6 +517,7 @@ it('should return lockfileVersion 1 with older versions of npm', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(1);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect npm Workspaces', async () => {
|
||||
@@ -474,48 +525,88 @@ it('should detect npm Workspaces', async () => {
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(2);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect pnpm', async () => {
|
||||
it('should detect pnpm without workspace', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '22-pnpm');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('pnpm');
|
||||
expect(result.lockfileVersion).toEqual(5.3);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect pnpm Workspaces', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '23-pnpm-workspaces/a');
|
||||
it('should detect pnpm with workspaces', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '23-pnpm-workspaces/c');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('pnpm');
|
||||
expect(result.lockfileVersion).toEqual(5.3);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect package.json in nested backend', async () => {
|
||||
const fixture = path.join(
|
||||
__dirname,
|
||||
'../../node/test/fixtures/18.1-nested-packagejson/backend'
|
||||
);
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should detect package.json in nested frontend', async () => {
|
||||
const fixture = path.join(
|
||||
__dirname,
|
||||
'../../node/test/fixtures/18.1-nested-packagejson/frontend'
|
||||
);
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
|
||||
});
|
||||
|
||||
it('should only invoke `runNpmInstall()` once per `package.json` file (serial)', async () => {
|
||||
const meta: Meta = {};
|
||||
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
|
||||
const apiDir = path.join(fixture, 'api');
|
||||
const run1 = await runNpmInstall(apiDir, [], undefined, meta);
|
||||
expect(run1).toEqual(true);
|
||||
expect(
|
||||
(meta.runNpmInstallSet as Set<string>).has(
|
||||
path.join(fixture, 'package.json')
|
||||
)
|
||||
).toEqual(true);
|
||||
const run2 = await runNpmInstall(apiDir, [], undefined, meta);
|
||||
expect(run2).toEqual(false);
|
||||
const run3 = await runNpmInstall(fixture, [], undefined, meta);
|
||||
expect(run3).toEqual(false);
|
||||
const retryOpts = { maxRetryTime: 1000 };
|
||||
let run1, run2, run3;
|
||||
await retry(async () => {
|
||||
run1 = await runNpmInstall(apiDir, [], undefined, meta);
|
||||
expect(run1).toEqual(true);
|
||||
expect(
|
||||
(meta.runNpmInstallSet as Set<string>).has(
|
||||
path.join(fixture, 'package.json')
|
||||
)
|
||||
).toEqual(true);
|
||||
}, retryOpts);
|
||||
await retry(async () => {
|
||||
run2 = await runNpmInstall(apiDir, [], undefined, meta);
|
||||
expect(run2).toEqual(false);
|
||||
}, retryOpts);
|
||||
await retry(async () => {
|
||||
run3 = await runNpmInstall(fixture, [], undefined, meta);
|
||||
expect(run3).toEqual(false);
|
||||
}, retryOpts);
|
||||
});
|
||||
|
||||
it('should only invoke `runNpmInstall()` once per `package.json` file (parallel)', async () => {
|
||||
const meta: Meta = {};
|
||||
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
|
||||
const apiDir = path.join(fixture, 'api');
|
||||
const [run1, run2, run3] = await Promise.all([
|
||||
runNpmInstall(apiDir, [], undefined, meta),
|
||||
runNpmInstall(apiDir, [], undefined, meta),
|
||||
runNpmInstall(fixture, [], undefined, meta),
|
||||
]);
|
||||
let results: [boolean, boolean, boolean] | undefined;
|
||||
await retry(
|
||||
async () => {
|
||||
results = await Promise.all([
|
||||
runNpmInstall(apiDir, [], undefined, meta),
|
||||
runNpmInstall(apiDir, [], undefined, meta),
|
||||
runNpmInstall(fixture, [], undefined, meta),
|
||||
]);
|
||||
},
|
||||
{ maxRetryTime: 3000 }
|
||||
);
|
||||
const [run1, run2, run3] = results || [];
|
||||
expect(run1).toEqual(true);
|
||||
expect(run2).toEqual(false);
|
||||
expect(run3).toEqual(false);
|
||||
|
||||
@@ -53,13 +53,13 @@ At this point you can make modifications to the CLI source code and test them ou
|
||||
cd packages/cli
|
||||
```
|
||||
|
||||
From within the `packages/cli` directory, you can use the `ts-eager` command line tool to quickly excute Vercel CLI from its TypeScript source code directly (without having to manually compile first). For example:
|
||||
From within the `packages/cli` directory, you can use the "dev" script to quickly execute Vercel CLI from its TypeScript source code directly (without having to manually compile first). For example:
|
||||
|
||||
```bash
|
||||
npx ts-eager src
|
||||
npx ts-eager src login
|
||||
npx ts-eager src switch --debug
|
||||
npx ts-eager src dev
|
||||
yarn dev deploy
|
||||
yarn dev whoami
|
||||
yarn dev login
|
||||
yarn dev switch --debug
|
||||
```
|
||||
|
||||
When you are satisfied with your changes, make a commit and create a pull request!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "27.0.0",
|
||||
"version": "27.3.6",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -16,10 +16,9 @@
|
||||
"test-unit": "yarn test test/unit/",
|
||||
"test-integration-cli": "rimraf test/fixtures/integration && ava test/integration.js --serial --fail-fast --verbose",
|
||||
"test-integration-dev": "yarn test test/dev/",
|
||||
"prepublishOnly": "yarn build",
|
||||
"coverage": "codecov",
|
||||
"build": "node -r ts-eager/register ./scripts/build.ts",
|
||||
"build-dev": "node -r ts-eager/register ./scripts/build.ts --dev"
|
||||
"build": "ts-node ./scripts/build.ts",
|
||||
"dev": "ts-node ./src/index.ts"
|
||||
},
|
||||
"bin": {
|
||||
"vc": "./dist/index.js",
|
||||
@@ -42,16 +41,16 @@
|
||||
"node": ">= 14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "5.0.1",
|
||||
"@vercel/go": "2.0.5",
|
||||
"@vercel/hydrogen": "0.0.2",
|
||||
"@vercel/next": "3.1.4",
|
||||
"@vercel/node": "2.4.1",
|
||||
"@vercel/python": "3.0.5",
|
||||
"@vercel/redwood": "1.0.6",
|
||||
"@vercel/remix": "1.0.6",
|
||||
"@vercel/ruby": "1.3.13",
|
||||
"@vercel/static-build": "1.0.5",
|
||||
"@vercel/build-utils": "5.1.1",
|
||||
"@vercel/go": "2.0.14",
|
||||
"@vercel/hydrogen": "0.0.11",
|
||||
"@vercel/next": "3.1.14",
|
||||
"@vercel/node": "2.5.5",
|
||||
"@vercel/python": "3.1.6",
|
||||
"@vercel/redwood": "1.0.15",
|
||||
"@vercel/remix": "1.0.16",
|
||||
"@vercel/ruby": "1.3.22",
|
||||
"@vercel/static-build": "1.0.15",
|
||||
"update-notifier": "5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -59,6 +58,7 @@
|
||||
"@next/env": "11.1.2",
|
||||
"@sentry/node": "5.5.0",
|
||||
"@sindresorhus/slugify": "0.11.0",
|
||||
"@swc/core": "1.2.218",
|
||||
"@tootallnate/once": "1.1.2",
|
||||
"@types/ansi-escapes": "3.0.0",
|
||||
"@types/ansi-regex": "4.0.0",
|
||||
@@ -96,11 +96,11 @@
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel/client": "12.1.0",
|
||||
"@vercel/frameworks": "1.1.0",
|
||||
"@vercel/fs-detectors": "1.0.1",
|
||||
"@vercel/client": "12.1.9",
|
||||
"@vercel/frameworks": "1.1.2",
|
||||
"@vercel/fs-detectors": "2.0.4",
|
||||
"@vercel/fun": "1.0.4",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@zeit/fun": "0.11.2",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
"ajv": "6.12.2",
|
||||
"alpha-sort": "2.0.1",
|
||||
@@ -111,6 +111,7 @@
|
||||
"async-retry": "1.1.3",
|
||||
"async-sema": "2.1.4",
|
||||
"ava": "2.2.0",
|
||||
"boxen": "4.2.0",
|
||||
"bytes": "3.0.0",
|
||||
"chalk": "4.1.0",
|
||||
"chance": "1.1.7",
|
||||
@@ -147,7 +148,7 @@
|
||||
"minimatch": "3.0.4",
|
||||
"mri": "1.1.5",
|
||||
"ms": "2.1.2",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"npm-package-arg": "6.1.0",
|
||||
"open": "8.4.0",
|
||||
"ora": "3.4.0",
|
||||
@@ -169,8 +170,8 @@
|
||||
"title": "3.4.1",
|
||||
"tmp-promise": "1.0.3",
|
||||
"tree-kill": "1.2.2",
|
||||
"ts-node": "8.3.0",
|
||||
"typescript": "4.3.4",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.7.4",
|
||||
"universal-analytics": "0.4.20",
|
||||
"utility-types": "2.1.0",
|
||||
"which": "2.0.2",
|
||||
|
||||
@@ -27,40 +27,38 @@ function envToString(key: string) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isDev = process.argv[2] === '--dev';
|
||||
// Read the secrets from GitHub Actions and generate a file.
|
||||
// During local development, these secrets will be empty.
|
||||
await createConstants();
|
||||
|
||||
if (!isDev) {
|
||||
// Read the secrets from GitHub Actions and generate a file.
|
||||
// During local development, these secrets will be empty.
|
||||
await createConstants();
|
||||
// `vercel dev` uses chokidar to watch the filesystem, but opts-out of the
|
||||
// `fsevents` feature using `useFsEvents: false`, so delete the module here so
|
||||
// that it is not compiled by ncc, which makes the npm package size larger
|
||||
// than necessary.
|
||||
await remove(join(dirRoot, '../../node_modules/fsevents'));
|
||||
|
||||
// `vercel dev` uses chokidar to watch the filesystem, but opts-out of the
|
||||
// `fsevents` feature using `useFsEvents: false`, so delete the module here so
|
||||
// that it is not compiled by ncc, which makes the npm package size larger
|
||||
// than necessary.
|
||||
await remove(join(dirRoot, '../../node_modules/fsevents'));
|
||||
|
||||
// Compile the `doT.js` template files for `vercel dev`
|
||||
console.log();
|
||||
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
// Compile the `doT.js` template files for `vercel dev`
|
||||
console.log();
|
||||
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
// Do the initial `ncc` build
|
||||
console.log();
|
||||
const args = ['ncc', 'build', '--external', 'update-notifier'];
|
||||
if (isDev) {
|
||||
args.push('--source-map');
|
||||
}
|
||||
args.push('src/index.ts');
|
||||
const args = [
|
||||
'ncc',
|
||||
'build',
|
||||
'--external',
|
||||
'update-notifier',
|
||||
'src/index.ts',
|
||||
];
|
||||
await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot });
|
||||
|
||||
// `ncc` has some issues with `@zeit/fun`'s runtime files:
|
||||
// `ncc` has some issues with `@vercel/fun`'s runtime files:
|
||||
// - Executable bits on the `bootstrap` files appear to be lost:
|
||||
// https://github.com/zeit/ncc/pull/182
|
||||
// https://github.com/vercel/ncc/pull/182
|
||||
// - The `bootstrap.js` asset does not get copied into the output dir:
|
||||
// https://github.com/zeit/ncc/issues/278
|
||||
// https://github.com/vercel/ncc/issues/278
|
||||
//
|
||||
// Aside from those issues, all the same files from the `runtimes` directory
|
||||
// should be copied into the output runtimes dir, specifically the `index.js`
|
||||
@@ -70,7 +68,7 @@ async function main() {
|
||||
// with `fun`'s cache invalidation mechanism and they need to be shasum'd.
|
||||
const runtimes = join(
|
||||
dirRoot,
|
||||
'../../node_modules/@zeit/fun/dist/src/runtimes'
|
||||
'../../node_modules/@vercel/fun/dist/src/runtimes'
|
||||
);
|
||||
await cpy('**/*', join(distRoot, 'runtimes'), {
|
||||
parents: true,
|
||||
@@ -79,6 +77,7 @@ async function main() {
|
||||
|
||||
// Band-aid to bundle stuff that `ncc` neglects to bundle
|
||||
await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot);
|
||||
await cpy(join(dirRoot, 'src/util/dev/builder-worker.js'), distRoot);
|
||||
|
||||
console.log('Finished building Vercel CLI');
|
||||
}
|
||||
|
||||
@@ -22,19 +22,7 @@ export default async function ls(
|
||||
) {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
|
||||
output.error('Please provide a number for flag --next');
|
||||
|
||||
@@ -23,19 +23,7 @@ export default async function rm(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [aliasOrId] = args;
|
||||
|
||||
|
||||
@@ -15,10 +15,9 @@ import { isValidName } from '../../util/is-valid-name';
|
||||
import handleCertError from '../../util/certs/handle-cert-error';
|
||||
import isWildcardAlias from '../../util/alias/is-wildcard-alias';
|
||||
import link from '../../util/output/link';
|
||||
import { User } from '../../types';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import toHost from '../../util/to-host';
|
||||
import { VercelConfig } from '../../util/dev/types';
|
||||
import type { VercelConfig } from '@vercel/client';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -30,23 +29,9 @@ export default async function set(
|
||||
opts: Partial<Options>,
|
||||
args: string[]
|
||||
) {
|
||||
const { output, localConfig } = client;
|
||||
|
||||
const setStamp = stamp();
|
||||
|
||||
let user: User;
|
||||
let contextName: string | null = null;
|
||||
|
||||
try {
|
||||
({ contextName, user } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { output, localConfig } = client;
|
||||
const { contextName, user } = await getScope(client);
|
||||
|
||||
// If there are more than two args we have to error
|
||||
if (args.length > 2) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
MergeRoutesProps,
|
||||
Route,
|
||||
} from '@vercel/routing-utils';
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
import type { VercelConfig } from '@vercel/client';
|
||||
|
||||
import pull from './pull';
|
||||
import { staticFiles as getFiles } from '../util/get-files';
|
||||
@@ -36,7 +36,10 @@ import * as cli from '../util/pkg-name';
|
||||
import cliPkg from '../util/pkg';
|
||||
import readJSONFile from '../util/read-json-file';
|
||||
import { CantParseJSONFile } from '../util/errors-ts';
|
||||
import { readProjectSettings } from '../util/projects/project-settings';
|
||||
import {
|
||||
ProjectLinkAndSettings,
|
||||
readProjectSettings,
|
||||
} from '../util/projects/project-settings';
|
||||
import { VERCEL_DIR } from '../util/projects/link';
|
||||
import confirm from '../util/input/confirm';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
@@ -46,11 +49,31 @@ import {
|
||||
PathOverride,
|
||||
writeBuildResult,
|
||||
} from '../util/build/write-build-result';
|
||||
import { importBuilders, BuilderWithPkg } from '../util/build/import-builders';
|
||||
import { importBuilders } from '../util/build/import-builders';
|
||||
import { initCorepack, cleanupCorepack } from '../util/build/corepack';
|
||||
import { sortBuilders } from '../util/build/sort-builders';
|
||||
import { toEnumerableError } from '../util/error';
|
||||
|
||||
type BuildResult = BuildResultV2 | BuildResultV3;
|
||||
|
||||
interface SerializedBuilder extends Builder {
|
||||
error?: any;
|
||||
require?: string;
|
||||
requirePath?: string;
|
||||
apiVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contents of the `builds.json` file.
|
||||
*/
|
||||
export interface BuildsManifest {
|
||||
'//': string;
|
||||
target: string;
|
||||
argv: string[];
|
||||
error?: any;
|
||||
builds?: SerializedBuilder[];
|
||||
}
|
||||
|
||||
const help = () => {
|
||||
return console.log(`
|
||||
${chalk.bold(`${cli.logo} ${cli.name} build`)}
|
||||
@@ -166,21 +189,83 @@ export default async function main(client: Client): Promise<number> {
|
||||
project = await readProjectSettings(join(cwd, VERCEL_DIR));
|
||||
}
|
||||
|
||||
// TODO: load env vars from the API, fall back to local files if that fails
|
||||
// Delete output directory from potential previous build
|
||||
const outputDir = argv['--output']
|
||||
? resolve(argv['--output'])
|
||||
: join(cwd, OUTPUT_DIR);
|
||||
await fs.remove(outputDir);
|
||||
|
||||
const envPath = await checkExists([
|
||||
join(cwd, VERCEL_DIR, `.env.${target}.local`),
|
||||
join(cwd, `.env`),
|
||||
]);
|
||||
if (envPath) {
|
||||
dotenv.config({ path: envPath, debug: client.output.isDebugEnabled() });
|
||||
output.log(`Loaded env from "${relative(cwd, envPath)}"`);
|
||||
const buildsJson: BuildsManifest = {
|
||||
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
|
||||
target,
|
||||
argv: process.argv,
|
||||
};
|
||||
|
||||
const envToUnset = new Set<string>(['VERCEL', 'NOW_BUILDER']);
|
||||
|
||||
try {
|
||||
const envPath = join(cwd, VERCEL_DIR, `.env.${target}.local`);
|
||||
// TODO (maybe?): load env vars from the API, fall back to the local file if that fails
|
||||
const dotenvResult = dotenv.config({
|
||||
path: envPath,
|
||||
debug: client.output.isDebugEnabled(),
|
||||
});
|
||||
if (dotenvResult.error) {
|
||||
output.debug(
|
||||
`Failed loading environment variables: ${dotenvResult.error}`
|
||||
);
|
||||
} else if (dotenvResult.parsed) {
|
||||
for (const key of Object.keys(dotenvResult.parsed)) {
|
||||
envToUnset.add(key);
|
||||
}
|
||||
output.debug(`Loaded environment variables from "${envPath}"`);
|
||||
}
|
||||
|
||||
// For Vercel Analytics support
|
||||
if (project.settings.analyticsId) {
|
||||
envToUnset.add('VERCEL_ANALYTICS_ID');
|
||||
process.env.VERCEL_ANALYTICS_ID = project.settings.analyticsId;
|
||||
}
|
||||
|
||||
// Some build processes use these env vars to platform detect Vercel
|
||||
process.env.VERCEL = '1';
|
||||
process.env.NOW_BUILDER = '1';
|
||||
|
||||
return await doBuild(client, project, buildsJson, cwd, outputDir);
|
||||
} catch (err: any) {
|
||||
output.prettyError(err);
|
||||
|
||||
// Write error to `builds.json` file
|
||||
buildsJson.error = toEnumerableError(err);
|
||||
const buildsJsonPath = join(outputDir, 'builds.json');
|
||||
const configJsonPath = join(outputDir, 'config.json');
|
||||
await fs.outputJSON(buildsJsonPath, buildsJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
await fs.writeJSON(configJsonPath, { version: 3 }, { spaces: 2 });
|
||||
|
||||
return 1;
|
||||
} finally {
|
||||
// Unset environment variables that were added by dotenv
|
||||
// (this is mostly for the unit tests)
|
||||
for (const key of envToUnset) {
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some build processes use these env vars to platform detect Vercel
|
||||
process.env.VERCEL = '1';
|
||||
process.env.NOW_BUILDER = '1';
|
||||
|
||||
/**
|
||||
* Execute the Project's builders. If this function throws an error,
|
||||
* then it will be serialized into the `builds.json` manifest file.
|
||||
*/
|
||||
async function doBuild(
|
||||
client: Client,
|
||||
project: ProjectLinkAndSettings,
|
||||
buildsJson: BuildsManifest,
|
||||
cwd: string,
|
||||
outputDir: string
|
||||
): Promise<number> {
|
||||
const { output } = client;
|
||||
const workPath = join(cwd, project.settings.rootDirectory || '.');
|
||||
|
||||
// Load `package.json` and `vercel.json` files
|
||||
@@ -198,23 +283,23 @@ export default async function main(client: Client): Promise<number> {
|
||||
normalizePath(relative(workPath, f))
|
||||
);
|
||||
|
||||
const routesResult = getTransformedRoutes({ nowConfig: vercelConfig || {} });
|
||||
const routesResult = getTransformedRoutes(vercelConfig || {});
|
||||
if (routesResult.error) {
|
||||
output.prettyError(routesResult.error);
|
||||
return 1;
|
||||
throw routesResult.error;
|
||||
}
|
||||
|
||||
if (vercelConfig?.builds && vercelConfig.functions) {
|
||||
output.prettyError({
|
||||
throw new NowBuildError({
|
||||
code: 'bad_request',
|
||||
message:
|
||||
'The `functions` property cannot be used in conjunction with the `builds` property. Please remove one of them.',
|
||||
link: 'https://vercel.link/functions-and-builds',
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
let builds = vercelConfig?.builds || [];
|
||||
let zeroConfigRoutes: Route[] = [];
|
||||
let isZeroConfig = false;
|
||||
|
||||
if (builds.length > 0) {
|
||||
output.warn(
|
||||
@@ -223,17 +308,18 @@ export default async function main(client: Client): Promise<number> {
|
||||
builds = builds.map(b => expandBuild(files, b)).flat();
|
||||
} else {
|
||||
// Zero config
|
||||
isZeroConfig = true;
|
||||
|
||||
// Detect the Vercel Builders that will need to be invoked
|
||||
const detectedBuilders = await detectBuilders(files, pkg, {
|
||||
...vercelConfig,
|
||||
projectSettings: project.settings,
|
||||
ignoreBuildScript: true,
|
||||
featHandleMiss: true,
|
||||
});
|
||||
|
||||
if (detectedBuilders.errors && detectedBuilders.errors.length > 0) {
|
||||
output.prettyError(detectedBuilders.errors[0]);
|
||||
return 1;
|
||||
throw detectedBuilders.errors[0];
|
||||
}
|
||||
|
||||
for (const w of detectedBuilders.warnings) {
|
||||
@@ -266,13 +352,7 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
const builderSpecs = new Set(builds.map(b => b.use));
|
||||
|
||||
let buildersWithPkgs: Map<string, BuilderWithPkg>;
|
||||
try {
|
||||
buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
|
||||
} catch (err: any) {
|
||||
output.prettyError(err);
|
||||
return 1;
|
||||
}
|
||||
const buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
|
||||
|
||||
// Populate Files -> FileFsRef mapping
|
||||
const filesMap: Files = {};
|
||||
@@ -282,12 +362,6 @@ export default async function main(client: Client): Promise<number> {
|
||||
filesMap[path] = new FileFsRef({ mode, fsPath });
|
||||
}
|
||||
|
||||
// Delete output directory from potential previous build
|
||||
const outputDir = argv['--output']
|
||||
? resolve(argv['--output'])
|
||||
: join(cwd, OUTPUT_DIR);
|
||||
await fs.remove(outputDir);
|
||||
|
||||
const buildStamp = stamp();
|
||||
|
||||
// Create fresh new output directory
|
||||
@@ -296,32 +370,31 @@ export default async function main(client: Client): Promise<number> {
|
||||
const ops: Promise<Error | void>[] = [];
|
||||
|
||||
// Write the `detectedBuilders` result to output dir
|
||||
ops.push(
|
||||
fs.writeJSON(
|
||||
join(outputDir, 'builds.json'),
|
||||
{
|
||||
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
|
||||
target,
|
||||
argv: process.argv,
|
||||
builds: builds.map(build => {
|
||||
const builderWithPkg = buildersWithPkgs.get(build.use);
|
||||
if (!builderWithPkg) {
|
||||
throw new Error(`Failed to load Builder "${build.use}"`);
|
||||
}
|
||||
const { builder, pkg: builderPkg } = builderWithPkg;
|
||||
return {
|
||||
require: builderPkg.name,
|
||||
requirePath: builderWithPkg.path,
|
||||
apiVersion: builder.version,
|
||||
...build,
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
spaces: 2,
|
||||
const buildsJsonBuilds = new Map<Builder, SerializedBuilder>(
|
||||
builds.map(build => {
|
||||
const builderWithPkg = buildersWithPkgs.get(build.use);
|
||||
if (!builderWithPkg) {
|
||||
throw new Error(`Failed to load Builder "${build.use}"`);
|
||||
}
|
||||
)
|
||||
const { builder, pkg: builderPkg } = builderWithPkg;
|
||||
return [
|
||||
build,
|
||||
{
|
||||
require: builderPkg.name,
|
||||
requirePath: builderWithPkg.path,
|
||||
apiVersion: builder.version,
|
||||
...build,
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
buildsJson.builds = Array.from(buildsJsonBuilds.values());
|
||||
const buildsJsonPath = join(outputDir, 'builds.json');
|
||||
const writeBuildsJsonPromise = fs.writeJSON(buildsJsonPath, buildsJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
ops.push(writeBuildsJsonPromise);
|
||||
|
||||
// The `meta` config property is re-used for each Builder
|
||||
// invocation so that Builders can share state between
|
||||
@@ -332,65 +405,91 @@ export default async function main(client: Client): Promise<number> {
|
||||
};
|
||||
|
||||
// Execute Builders for detected entrypoints
|
||||
// TODO: parallelize builds
|
||||
// TODO: parallelize builds (except for frontend)
|
||||
const sortedBuilders = sortBuilders(builds);
|
||||
const buildResults: Map<Builder, BuildResult> = new Map();
|
||||
const overrides: PathOverride[] = [];
|
||||
const repoRootPath = cwd;
|
||||
const rootPackageJsonPath = repoRootPath || workPath;
|
||||
const corepackShimDir = await initCorepack({ cwd, rootPackageJsonPath });
|
||||
const corepackShimDir = await initCorepack({ repoRootPath });
|
||||
|
||||
for (const build of builds) {
|
||||
for (const build of sortedBuilders) {
|
||||
if (typeof build.src !== 'string') continue;
|
||||
|
||||
const builderWithPkg = buildersWithPkgs.get(build.use);
|
||||
if (!builderWithPkg) {
|
||||
throw new Error(`Failed to load Builder "${build.use}"`);
|
||||
}
|
||||
const { builder, pkg: builderPkg } = builderWithPkg;
|
||||
|
||||
const buildConfig: Config = {
|
||||
outputDirectory: project.settings.outputDirectory ?? undefined,
|
||||
...build.config,
|
||||
projectSettings: project.settings,
|
||||
installCommand: project.settings.installCommand ?? undefined,
|
||||
devCommand: project.settings.devCommand ?? undefined,
|
||||
buildCommand: project.settings.buildCommand ?? undefined,
|
||||
framework: project.settings.framework,
|
||||
nodeVersion: project.settings.nodeVersion,
|
||||
};
|
||||
const buildOptions: BuildOptions = {
|
||||
files: filesMap,
|
||||
entrypoint: build.src,
|
||||
workPath,
|
||||
repoRootPath,
|
||||
config: buildConfig,
|
||||
meta,
|
||||
};
|
||||
output.debug(
|
||||
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
|
||||
);
|
||||
const buildResult = await builder.build(buildOptions);
|
||||
try {
|
||||
const { builder, pkg: builderPkg } = builderWithPkg;
|
||||
|
||||
// Store the build result to generate the final `config.json` after
|
||||
// all builds have completed
|
||||
buildResults.set(build, buildResult);
|
||||
const buildConfig: Config = isZeroConfig
|
||||
? {
|
||||
outputDirectory: project.settings.outputDirectory ?? undefined,
|
||||
...build.config,
|
||||
projectSettings: project.settings,
|
||||
installCommand: project.settings.installCommand ?? undefined,
|
||||
devCommand: project.settings.devCommand ?? undefined,
|
||||
buildCommand: project.settings.buildCommand ?? undefined,
|
||||
framework: project.settings.framework,
|
||||
nodeVersion: project.settings.nodeVersion,
|
||||
}
|
||||
: build.config || {};
|
||||
const buildOptions: BuildOptions = {
|
||||
files: filesMap,
|
||||
entrypoint: build.src,
|
||||
workPath,
|
||||
repoRootPath,
|
||||
config: buildConfig,
|
||||
meta,
|
||||
};
|
||||
output.debug(
|
||||
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
|
||||
);
|
||||
const buildResult = await builder.build(buildOptions);
|
||||
|
||||
// Start flushing the file outputs to the filesystem asynchronously
|
||||
ops.push(
|
||||
writeBuildResult(
|
||||
outputDir,
|
||||
buildResult,
|
||||
build,
|
||||
builder,
|
||||
builderPkg,
|
||||
vercelConfig?.cleanUrls
|
||||
).then(
|
||||
override => {
|
||||
if (override) overrides.push(override);
|
||||
},
|
||||
err => err
|
||||
)
|
||||
);
|
||||
// Store the build result to generate the final `config.json` after
|
||||
// all builds have completed
|
||||
buildResults.set(build, buildResult);
|
||||
|
||||
// Start flushing the file outputs to the filesystem asynchronously
|
||||
ops.push(
|
||||
writeBuildResult(
|
||||
outputDir,
|
||||
buildResult,
|
||||
build,
|
||||
builder,
|
||||
builderPkg,
|
||||
vercelConfig
|
||||
).then(
|
||||
override => {
|
||||
if (override) overrides.push(override);
|
||||
},
|
||||
err => err
|
||||
)
|
||||
);
|
||||
} catch (err: any) {
|
||||
output.prettyError(err);
|
||||
|
||||
const writeConfigJsonPromise = fs.writeJSON(
|
||||
join(outputDir, 'config.json'),
|
||||
{ version: 3 },
|
||||
{ spaces: 2 }
|
||||
);
|
||||
|
||||
await Promise.all([writeBuildsJsonPromise, writeConfigJsonPromise]);
|
||||
|
||||
const buildJsonBuild = buildsJsonBuilds.get(build);
|
||||
if (buildJsonBuild) {
|
||||
buildJsonBuild.error = toEnumerableError(err);
|
||||
|
||||
await fs.writeJSON(buildsJsonPath, buildsJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (corepackShimDir) {
|
||||
@@ -399,15 +498,12 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
// Wait for filesystem operations to complete
|
||||
// TODO render progress bar?
|
||||
let hadError = false;
|
||||
const errors = await Promise.all(ops);
|
||||
for (const error of errors) {
|
||||
if (error) {
|
||||
hadError = true;
|
||||
output.prettyError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (hadError) return 1;
|
||||
|
||||
// Merge existing `config.json` file into the one that will be produced
|
||||
const configPath = join(outputDir, 'config.json');
|
||||
@@ -527,7 +623,7 @@ function mergeImages(
|
||||
let images: BuildResultV2Typical['images'] = undefined;
|
||||
for (const result of buildResults) {
|
||||
if ('images' in result && result.images) {
|
||||
images = Object.assign({} || images, result.images);
|
||||
images = Object.assign({}, images, result.images);
|
||||
}
|
||||
}
|
||||
return images;
|
||||
@@ -545,14 +641,3 @@ function mergeWildcard(
|
||||
}
|
||||
return wildcard;
|
||||
}
|
||||
|
||||
async function checkExists(paths: Iterable<string>) {
|
||||
for (const path of paths) {
|
||||
try {
|
||||
await fs.stat(path);
|
||||
return path;
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import stamp from '../../util/output/stamp';
|
||||
import createCertFromFile from '../../util/certs/create-cert-from-file';
|
||||
import createCertForCns from '../../util/certs/create-cert-for-cns';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { Cert } from '../../types';
|
||||
|
||||
interface Options {
|
||||
'--overwrite'?: boolean;
|
||||
@@ -21,7 +22,7 @@ async function add(
|
||||
const { output } = client;
|
||||
const addStamp = stamp();
|
||||
|
||||
let cert;
|
||||
let cert: Cert | Error;
|
||||
|
||||
const {
|
||||
'--overwrite': overwite,
|
||||
@@ -30,18 +31,7 @@ async function add(
|
||||
'--ca': caPath,
|
||||
} = opts;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (overwite) {
|
||||
output.error('Overwrite option is deprecated');
|
||||
|
||||
@@ -39,18 +39,7 @@ export default async function issue(
|
||||
'--ca': caPath,
|
||||
} = opts;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (overwite) {
|
||||
output.error('Overwrite option is deprecated');
|
||||
|
||||
@@ -21,18 +21,8 @@ async function ls(
|
||||
): Promise<number> {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
let contextName = null;
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
if (typeof nextTimestamp !== 'undefined' && Number.isNaN(nextTimestamp)) {
|
||||
output.error('Please provide a number for flag --next');
|
||||
return 1;
|
||||
|
||||
@@ -17,21 +17,9 @@ import { getCommandName } from '../../util/pkg-name';
|
||||
type Options = {};
|
||||
|
||||
async function rm(client: Client, opts: Options, args: string[]) {
|
||||
const { output } = client;
|
||||
const rmStamp = stamp();
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { output } = client;
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (args.length !== 1) {
|
||||
output.error(
|
||||
|
||||
@@ -15,26 +15,28 @@ export const help = () => `
|
||||
)}
|
||||
dev Start a local development server
|
||||
env Manages the Environment Variables for your current Project
|
||||
git Manage Git provider repository for your current Project
|
||||
help [cmd] Displays complete help for [cmd]
|
||||
init [example] Initialize an example project
|
||||
ls | list [app] Lists deployments
|
||||
inspect [id] Displays information related to a deployment
|
||||
link [path] Link local directory to a Vercel Project
|
||||
ls | list [app] Lists deployments
|
||||
login [email] Logs into your account or creates a new one
|
||||
logout Logs out of your account
|
||||
pull [path] Pull your Project Settings from the cloud
|
||||
switch [scope] Switches between teams and your personal account
|
||||
help [cmd] Displays complete help for [cmd]
|
||||
|
||||
${chalk.dim('Advanced')}
|
||||
|
||||
rm | remove [id] Removes a deployment
|
||||
alias [cmd] Manages your domain aliases
|
||||
bisect Use binary search to find the deployment that introduced a bug
|
||||
domains [name] Manages your domain names
|
||||
projects Manages your Projects
|
||||
dns [name] Manages your DNS records
|
||||
certs [cmd] Manages your SSL certificates
|
||||
secrets [name] Manages your global Secrets, for use in Environment Variables
|
||||
dns [name] Manages your DNS records
|
||||
domains [name] Manages your domain names
|
||||
logs [url] Displays the logs for a deployment
|
||||
projects Manages your Projects
|
||||
rm | remove [id] Removes a deployment
|
||||
secrets [name] Manages your global Secrets, for use in Environment Variables
|
||||
teams Manages your teams
|
||||
whoami Shows the username of the currently logged in user
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
||||
import bytes from 'bytes';
|
||||
import chalk from 'chalk';
|
||||
import { join, resolve, basename } from 'path';
|
||||
import { Dictionary, fileNameSymbol, VercelConfig } from '@vercel/client';
|
||||
import { fileNameSymbol, VercelConfig } from '@vercel/client';
|
||||
import code from '../../util/output/code';
|
||||
import highlight from '../../util/output/highlight';
|
||||
import { readLocalConfig } from '../../util/config/files';
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
ConflictingPathSegment,
|
||||
BuildError,
|
||||
NotDomainOwner,
|
||||
isAPIError,
|
||||
} from '../../util/errors-ts';
|
||||
import { SchemaValidationFailed } from '../../util/errors';
|
||||
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
|
||||
@@ -64,7 +65,9 @@ import { help } from './args';
|
||||
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
|
||||
import parseTarget from '../../util/deploy/parse-target';
|
||||
import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
|
||||
import { createGitMeta } from '../../util/deploy/create-git-meta';
|
||||
import { createGitMeta } from '../../util/create-git-meta';
|
||||
import { parseEnv } from '../../util/parse-env';
|
||||
import { errorToString, isErrnoException, isError } from '../../util/is-error';
|
||||
|
||||
export default async (client: Client) => {
|
||||
const { output } = client;
|
||||
@@ -95,6 +98,7 @@ export default async (client: Client) => {
|
||||
// deprecated
|
||||
'--name': String,
|
||||
'-n': '--name',
|
||||
'--no-clipboard': Boolean,
|
||||
'--target': String,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -183,6 +187,17 @@ export default async (client: Client) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (argv['--no-clipboard']) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${param(
|
||||
'--no-clipboard'
|
||||
)} option was ignored because it is the default behavior. Please remove it.`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// build `target`
|
||||
const target = parseTarget(output, argv['--target'], argv['--prod']);
|
||||
if (typeof target === 'number') {
|
||||
@@ -204,6 +219,22 @@ export default async (client: Client) => {
|
||||
}
|
||||
|
||||
const prebuiltBuild = await getPrebuiltJson(path);
|
||||
|
||||
// Ensure that there was not a build error
|
||||
const prebuiltError =
|
||||
prebuiltBuild?.error ||
|
||||
prebuiltBuild?.builds?.find(build => 'error' in build)?.error;
|
||||
if (prebuiltError) {
|
||||
output.log(
|
||||
`Prebuilt deployment cannot be created because ${getCommandName(
|
||||
'build'
|
||||
)} failed with error:\n`
|
||||
);
|
||||
prettyError(prebuiltError);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Ensure that the deploy target matches the build target
|
||||
const assumedTarget = target || 'preview';
|
||||
if (prebuiltBuild?.target && prebuiltBuild.target !== assumedTarget) {
|
||||
let specifyTarget = '';
|
||||
@@ -256,8 +287,11 @@ export default async (client: Client) => {
|
||||
'Which scope do you want to deploy to?',
|
||||
autoConfirm
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
) {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
@@ -416,7 +450,7 @@ export default async (client: Client) => {
|
||||
parseMeta(argv['--meta'])
|
||||
);
|
||||
|
||||
const gitMetadata = await createGitMeta(path, output);
|
||||
const gitMetadata = await createGitMeta(path, output, project);
|
||||
|
||||
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
|
||||
const deploymentEnv = Object.assign(
|
||||
@@ -436,8 +470,8 @@ export default async (client: Client) => {
|
||||
try {
|
||||
await addProcessEnv(log, deploymentEnv);
|
||||
await addProcessEnv(log, deploymentBuildEnv);
|
||||
} catch (err) {
|
||||
error(err.message);
|
||||
} catch (err: unknown) {
|
||||
error(errorToString(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -598,8 +632,10 @@ export default async (client: Client) => {
|
||||
error('Uploading failed. Please try again.');
|
||||
return 1;
|
||||
}
|
||||
} catch (err) {
|
||||
debug(`Error: ${err}\n${err.stack}`);
|
||||
} catch (err: unknown) {
|
||||
if (isError(err)) {
|
||||
debug(`Error: ${err}\n${err.stack}`);
|
||||
}
|
||||
|
||||
if (err instanceof NotDomainOwner) {
|
||||
output.error(err.message);
|
||||
@@ -666,13 +702,7 @@ export default async (client: Client) => {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') {
|
||||
const { additionalProperty = '' } = err.params || {};
|
||||
const message = `Invalid DC name for the scale option: ${additionalProperty}`;
|
||||
error(message);
|
||||
}
|
||||
|
||||
if (err.code === 'size_limit_exceeded') {
|
||||
if (isAPIError(err) && err.code === 'size_limit_exceeded') {
|
||||
const { sizeLimit = 0 } = err;
|
||||
const message = `File size limit exceeded (${bytes(sizeLimit)})`;
|
||||
error(message);
|
||||
@@ -882,36 +912,3 @@ const printDeploymentStatus = async (
|
||||
output.print(message + link);
|
||||
}
|
||||
};
|
||||
|
||||
// Converts `env` Arrays, Strings and Objects into env Objects.
|
||||
const parseEnv = (env?: string[] | Dictionary<string>) => {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof env === 'string') {
|
||||
// a single `--env` arg comes in as a String
|
||||
env = [env];
|
||||
}
|
||||
|
||||
if (Array.isArray(env)) {
|
||||
return env.reduce((o, e) => {
|
||||
let key;
|
||||
let value;
|
||||
const equalsSign = e.indexOf('=');
|
||||
|
||||
if (equalsSign === -1) {
|
||||
key = e;
|
||||
} else {
|
||||
key = e.slice(0, equalsSign);
|
||||
value = e.slice(equalsSign + 1);
|
||||
}
|
||||
|
||||
o[key] = value;
|
||||
return o;
|
||||
}, {} as Dictionary<string | undefined>);
|
||||
}
|
||||
|
||||
// assume it's already an Object
|
||||
return env;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import readConfig from '../../util/config/read-config';
|
||||
import readJSONFile from '../../util/read-json-file';
|
||||
import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import { CantParseJSONFile } from '../../util/errors-ts';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
dev: ['dev'],
|
||||
@@ -136,7 +137,7 @@ export default async function main(client: Client) {
|
||||
try {
|
||||
return await dev(client, argv, args);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
if (isErrnoException(err) && err.code === 'ENOTFOUND') {
|
||||
// Error message will look like the following:
|
||||
// "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
|
||||
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
|
||||
@@ -148,7 +149,9 @@ export default async function main(client: Client) {
|
||||
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
output.debug(err.stack);
|
||||
if (typeof err.stack === 'string') {
|
||||
output.debug(err.stack);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
output.prettyError(err);
|
||||
|
||||
@@ -21,18 +21,7 @@ export default async function add(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const parsedParams = parseAddDNSRecordArgs(args);
|
||||
if (!parsedParams) {
|
||||
|
||||
@@ -14,18 +14,7 @@ export default async function add(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (args.length !== 2) {
|
||||
output.error(
|
||||
|
||||
@@ -24,18 +24,7 @@ export default async function ls(
|
||||
) {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
const lsStamp = stamp();
|
||||
|
||||
@@ -14,21 +14,11 @@ type Options = {};
|
||||
|
||||
export default async function rm(
|
||||
client: Client,
|
||||
opts: Options,
|
||||
_opts: Options,
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
try {
|
||||
await getScope(client);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
await getScope(client);
|
||||
|
||||
const [recordId] = args;
|
||||
if (args.length !== 1) {
|
||||
|
||||
@@ -26,18 +26,7 @@ export default async function add(
|
||||
) {
|
||||
const { output } = client;
|
||||
const force = opts['--force'];
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const project = await getLinkedProject(client).then(result => {
|
||||
if (result.status === 'linked') {
|
||||
|
||||
@@ -11,6 +11,7 @@ import promptBool from '../../util/input/prompt-bool';
|
||||
import purchaseDomain from '../../util/domains/purchase-domain';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { errorToString } from '../../util/is-error';
|
||||
|
||||
type Options = {};
|
||||
|
||||
@@ -20,18 +21,7 @@ export default async function buy(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
if (!domainName) {
|
||||
@@ -68,6 +58,11 @@ export default async function buy(
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (renewalPrice instanceof Error) {
|
||||
output.prettyError(renewalPrice);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(await getDomainStatus(client, domainName)).available) {
|
||||
output.error(
|
||||
`The domain ${param(domainName)} is ${chalk.underline(
|
||||
@@ -109,11 +104,11 @@ export default async function buy(
|
||||
|
||||
try {
|
||||
buyResult = await purchaseDomain(client, domainName, price, autoRenew);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.error(
|
||||
'An unexpected error occurred while purchasing your domain. Please try again later.'
|
||||
);
|
||||
output.debug(`Server response: ${err.message}`);
|
||||
output.debug(`Server response: ${errorToString(err)}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,19 +23,7 @@ export default async function inspect(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
const inspectStamp = stamp();
|
||||
|
||||
@@ -25,23 +25,13 @@ export default async function ls(
|
||||
) {
|
||||
const { output } = client;
|
||||
const { '--next': nextTimestamp } = opts;
|
||||
let contextName = null;
|
||||
|
||||
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
|
||||
output.error('Please provide a number for flag --next');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const lsStamp = stamp();
|
||||
|
||||
|
||||
@@ -25,20 +25,7 @@ export default async function move(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
let user = null;
|
||||
|
||||
try {
|
||||
({ contextName, user } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { contextName, user } = await getScope(client);
|
||||
const { domainName, destination } = await getArgs(args);
|
||||
if (!isRootDomain(domainName)) {
|
||||
output.error(
|
||||
|
||||
@@ -29,18 +29,7 @@ export default async function rm(
|
||||
) {
|
||||
const { output } = client;
|
||||
const [domainName] = args;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
if (!domainName) {
|
||||
output.error(
|
||||
@@ -122,10 +111,10 @@ async function removeDomain(
|
||||
output.debug(`Removing alias ${id}`);
|
||||
try {
|
||||
await removeAliasById(client, id);
|
||||
} catch (error) {
|
||||
} catch (err: unknown) {
|
||||
// Ignore if the alias does not exist anymore
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
if (!ERRORS.isAPIError(err) || err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,10 +123,10 @@ async function removeDomain(
|
||||
output.debug(`Removing cert ${id}`);
|
||||
try {
|
||||
await deleteCertById(output, client, id);
|
||||
} catch (error) {
|
||||
} catch (err: unknown) {
|
||||
// Ignore if the cert does not exist anymore
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
if (!ERRORS.isAPIError(err) || err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,7 @@ export default async function transferIn(
|
||||
args: string[]
|
||||
) {
|
||||
const { output } = client;
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
const [domainName] = args;
|
||||
if (!domainName) {
|
||||
|
||||
18
packages/cli/src/commands/env/add.ts
vendored
@@ -1,5 +1,4 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { ProjectEnvTarget, Project, ProjectEnvType } from '../../types';
|
||||
import { Output } from '../../util/output';
|
||||
import Client from '../../util/client';
|
||||
@@ -16,6 +15,7 @@ import param from '../../util/output/param';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isKnownError } from '../../util/env/known-error';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -66,7 +66,7 @@ export default async function add(
|
||||
}
|
||||
|
||||
while (!envName) {
|
||||
const { inputName } = await inquirer.prompt({
|
||||
const { inputName } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputName',
|
||||
message: `What’s the name of the variable?`,
|
||||
@@ -106,7 +106,7 @@ export default async function add(
|
||||
if (stdInput) {
|
||||
envValue = stdInput;
|
||||
} else {
|
||||
const { inputValue } = await inquirer.prompt({
|
||||
const { inputValue } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputValue',
|
||||
message: `What’s the value of ${envName}?`,
|
||||
@@ -116,7 +116,7 @@ export default async function add(
|
||||
}
|
||||
|
||||
while (envTargets.length === 0) {
|
||||
const { inputTargets } = await inquirer.prompt({
|
||||
const { inputTargets } = await client.prompt({
|
||||
name: 'inputTargets',
|
||||
type: 'checkbox',
|
||||
message: `Add ${envName} to which Environments (select multiple)?`,
|
||||
@@ -136,7 +136,7 @@ export default async function add(
|
||||
envTargets.length === 1 &&
|
||||
envTargets[0] === ProjectEnvTarget.Preview
|
||||
) {
|
||||
const { inputValue } = await inquirer.prompt({
|
||||
const { inputValue } = await client.prompt({
|
||||
type: 'input',
|
||||
name: 'inputValue',
|
||||
message: `Add ${envName} to which Git branch? (leave empty for all Preview branches)?`,
|
||||
@@ -157,12 +157,12 @@ export default async function add(
|
||||
envTargets,
|
||||
envGitBranch
|
||||
);
|
||||
} catch (error) {
|
||||
if (isKnownError(error) && error.serverMessage) {
|
||||
output.error(error.serverMessage);
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && isKnownError(err)) {
|
||||
output.error(err.serverMessage);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
|
||||
output.print(
|
||||
|
||||
21
packages/cli/src/commands/env/index.ts
vendored
@@ -1,7 +1,9 @@
|
||||
import chalk from 'chalk';
|
||||
import { ProjectEnvTarget } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { getEnvTargetPlaceholder } from '../../util/env/env-target';
|
||||
import {
|
||||
getEnvTargetPlaceholder,
|
||||
isValidEnvTarget,
|
||||
} from '../../util/env/env-target';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import getSubcommand from '../../util/get-subcommand';
|
||||
@@ -29,6 +31,7 @@ const help = () => {
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
--environment Set the Environment (development, preview, production) when pulling Environment Variables
|
||||
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
|
||||
'FILE'
|
||||
)} Path to the local ${'`vercel.json`'} file
|
||||
@@ -111,6 +114,7 @@ export default async function main(client: Client) {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--yes': Boolean,
|
||||
'-y': '--yes',
|
||||
'--environment': String,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
@@ -126,6 +130,17 @@ export default async function main(client: Client) {
|
||||
const subArgs = argv._.slice(1);
|
||||
const { subcommand, args } = getSubcommand(subArgs, COMMAND_CONFIG);
|
||||
const { output, config } = client;
|
||||
|
||||
const target = argv['--environment']?.toLowerCase() || 'development';
|
||||
if (!isValidEnvTarget(target)) {
|
||||
output.error(
|
||||
`Invalid environment \`${chalk.cyan(
|
||||
target
|
||||
)}\`. Valid options: ${getEnvTargetPlaceholder()}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'error') {
|
||||
return link.exitCode;
|
||||
@@ -150,7 +165,7 @@ export default async function main(client: Client) {
|
||||
return pull(
|
||||
client,
|
||||
project,
|
||||
ProjectEnvTarget.Development,
|
||||
target,
|
||||
argv,
|
||||
args,
|
||||
output,
|
||||
|
||||
34
packages/cli/src/commands/env/pull.ts
vendored
@@ -14,6 +14,11 @@ import param from '../../util/output/param';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { EnvRecordsSource } from '../../util/env/get-env-records';
|
||||
import {
|
||||
buildDeltaString,
|
||||
createEnvObject,
|
||||
} from '../../util/env/diff-env-files';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
@@ -36,8 +41,8 @@ function readHeadSync(path: string, length: number) {
|
||||
function tryReadHeadSync(path: string, length: number) {
|
||||
try {
|
||||
return readHeadSync(path, length);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
} catch (err: unknown) {
|
||||
if (!isErrnoException(err) || err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +74,7 @@ export default async function pull(
|
||||
const exists = typeof head !== 'undefined';
|
||||
|
||||
if (head === CONTENTS_PREFIX) {
|
||||
output.print(`Overwriting existing ${chalk.bold(filename)} file\n`);
|
||||
output.log(`Overwriting existing ${chalk.bold(filename)} file`);
|
||||
} else if (
|
||||
exists &&
|
||||
!skipConfirmation &&
|
||||
@@ -83,10 +88,10 @@ export default async function pull(
|
||||
return 0;
|
||||
}
|
||||
|
||||
output.print(
|
||||
`Downloading "${environment}" Environment Variables for Project ${chalk.bold(
|
||||
project.name
|
||||
)}\n`
|
||||
output.log(
|
||||
`Downloading \`${chalk.cyan(
|
||||
environment
|
||||
)}\` Environment Variables for Project ${chalk.bold(project.name)}`
|
||||
);
|
||||
|
||||
const pullStamp = stamp();
|
||||
@@ -107,6 +112,15 @@ export default async function pull(
|
||||
environment
|
||||
);
|
||||
|
||||
let deltaString = '';
|
||||
let oldEnv;
|
||||
if (exists) {
|
||||
oldEnv = await createEnvObject(fullPath, output);
|
||||
if (oldEnv) {
|
||||
deltaString = buildDeltaString(oldEnv, records);
|
||||
}
|
||||
}
|
||||
|
||||
const contents =
|
||||
CONTENTS_PREFIX +
|
||||
Object.entries(records)
|
||||
@@ -116,6 +130,12 @@ export default async function pull(
|
||||
|
||||
await outputFile(fullPath, contents, 'utf8');
|
||||
|
||||
if (deltaString) {
|
||||
output.print('\n' + deltaString);
|
||||
} else if (oldEnv && exists) {
|
||||
output.log('No changes found.');
|
||||
}
|
||||
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`${exists ? 'Updated' : 'Created'} ${chalk.bold(
|
||||
|
||||
9
packages/cli/src/commands/env/rm.ts
vendored
@@ -16,6 +16,7 @@ import param from '../../util/output/param';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isKnownError } from '../../util/env/known-error';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -120,12 +121,12 @@ export default async function rm(
|
||||
try {
|
||||
output.spinner('Removing');
|
||||
await removeEnvRecord(output, client, project.id, env);
|
||||
} catch (error) {
|
||||
if (isKnownError(error) && error.serverMessage) {
|
||||
output.error(error.serverMessage);
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && isKnownError(err)) {
|
||||
output.error(err.serverMessage);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
|
||||
output.print(
|
||||
|
||||
205
packages/cli/src/commands/git/connect.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import chalk from 'chalk';
|
||||
import { join } from 'path';
|
||||
import { Org, Project } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { parseGitConfig, pluckRemoteUrls } from '../../util/create-git-meta';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import list, { ListChoice } from '../../util/input/list';
|
||||
import { Output } from '../../util/output';
|
||||
import link from '../../util/output/link';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import {
|
||||
connectGitProvider,
|
||||
disconnectGitProvider,
|
||||
formatProvider,
|
||||
parseRepoUrl,
|
||||
} from '../../util/projects/connect-git-provider';
|
||||
import validatePaths from '../../util/validate-paths';
|
||||
|
||||
export default async function connect(
|
||||
client: Client,
|
||||
argv: any,
|
||||
args: string[],
|
||||
project: Project | undefined,
|
||||
org: Org | undefined
|
||||
) {
|
||||
const { output } = client;
|
||||
const confirm = Boolean(argv['--confirm']);
|
||||
|
||||
if (args.length !== 0) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project connect')}`
|
||||
)}`
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
if (!project || !org) {
|
||||
output.error(
|
||||
`Can't find \`org\` or \`project\`. Make sure your current directory is linked to a Vercel projet by running ${getCommandName(
|
||||
'link'
|
||||
)}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let paths = [process.cwd()];
|
||||
|
||||
const validate = await validatePaths(client, paths);
|
||||
if (!validate.valid) {
|
||||
return validate.exitCode;
|
||||
}
|
||||
const { path } = validate;
|
||||
|
||||
const gitProviderLink = project.link;
|
||||
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
|
||||
// get project from .git
|
||||
const gitConfigPath = join(path, '.git/config');
|
||||
const gitConfig = await parseGitConfig(gitConfigPath, output);
|
||||
if (!gitConfig) {
|
||||
output.error(
|
||||
`No local git repo found. Run ${chalk.cyan(
|
||||
'`git clone <url>`'
|
||||
)} to clone a remote Git repository first.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const remoteUrls = pluckRemoteUrls(gitConfig);
|
||||
if (!remoteUrls) {
|
||||
output.error(
|
||||
`No remote URLs found in your Git config. Make sure you've configured a remote repo in your local Git config. Run ${chalk.cyan(
|
||||
'`git remote --help`'
|
||||
)} for more details.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let remoteUrl: string;
|
||||
|
||||
if (Object.keys(remoteUrls).length > 1) {
|
||||
output.log(`Found multiple remote URLs.`);
|
||||
remoteUrl = await selectRemoteUrl(client, remoteUrls);
|
||||
} else {
|
||||
// If only one is found, get it — usually "origin"
|
||||
remoteUrl = Object.values(remoteUrls)[0];
|
||||
}
|
||||
|
||||
if (remoteUrl === '') {
|
||||
output.log('Aborted.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
output.log(`Connecting Git remote: ${link(remoteUrl)}`);
|
||||
|
||||
const parsedUrl = parseRepoUrl(remoteUrl);
|
||||
if (!parsedUrl) {
|
||||
output.error(
|
||||
`Failed to parse Git repo data from the following remote URL: ${link(
|
||||
remoteUrl
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const { provider, org: gitOrg, repo } = parsedUrl;
|
||||
const repoPath = `${gitOrg}/${repo}`;
|
||||
let connectedRepoPath;
|
||||
|
||||
if (!gitProviderLink) {
|
||||
const connect = await connectGitProvider(
|
||||
client,
|
||||
org,
|
||||
project.id,
|
||||
provider,
|
||||
repoPath
|
||||
);
|
||||
if (typeof connect === 'number') {
|
||||
return connect;
|
||||
}
|
||||
} else {
|
||||
const connectedProvider = gitProviderLink.type;
|
||||
const connectedOrg = gitProviderLink.org;
|
||||
const connectedRepo = gitProviderLink.repo;
|
||||
connectedRepoPath = `${connectedOrg}/${connectedRepo}`;
|
||||
|
||||
const isSameRepo =
|
||||
connectedProvider === provider &&
|
||||
connectedOrg === gitOrg &&
|
||||
connectedRepo === repo;
|
||||
if (isSameRepo) {
|
||||
output.log(
|
||||
`${chalk.cyan(connectedRepoPath)} is already connected to your project.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const shouldReplaceRepo = await confirmRepoConnect(
|
||||
client,
|
||||
output,
|
||||
confirm,
|
||||
connectedRepoPath
|
||||
);
|
||||
if (!shouldReplaceRepo) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
await disconnectGitProvider(client, org, project.id);
|
||||
const connect = await connectGitProvider(
|
||||
client,
|
||||
org,
|
||||
project.id,
|
||||
provider,
|
||||
repoPath
|
||||
);
|
||||
if (typeof connect === 'number') {
|
||||
return connect;
|
||||
}
|
||||
}
|
||||
|
||||
output.log(
|
||||
`Connected ${formatProvider(provider)} repository ${chalk.cyan(repoPath)}!`
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function confirmRepoConnect(
|
||||
client: Client,
|
||||
output: Output,
|
||||
yes: boolean,
|
||||
connectedRepoPath: string
|
||||
) {
|
||||
let shouldReplaceProject = yes;
|
||||
if (!shouldReplaceProject) {
|
||||
shouldReplaceProject = await confirm(
|
||||
client,
|
||||
`Looks like you already have a repository connected: ${chalk.cyan(
|
||||
connectedRepoPath
|
||||
)}. Do you want to replace it?`,
|
||||
true
|
||||
);
|
||||
if (!shouldReplaceProject) {
|
||||
output.log(`Aborted. Repo not connected.`);
|
||||
}
|
||||
}
|
||||
return shouldReplaceProject;
|
||||
}
|
||||
|
||||
async function selectRemoteUrl(
|
||||
client: Client,
|
||||
remoteUrls: { [key: string]: string }
|
||||
): Promise<string> {
|
||||
let choices: ListChoice[] = [];
|
||||
for (const [urlKey, urlValue] of Object.entries(remoteUrls)) {
|
||||
choices.push({
|
||||
name: `${urlValue} ${chalk.gray(`(${urlKey})`)}`,
|
||||
value: urlValue,
|
||||
short: urlKey,
|
||||
});
|
||||
}
|
||||
|
||||
return await list(client, {
|
||||
message: 'Which remote do you want to connect?',
|
||||
choices,
|
||||
});
|
||||
}
|
||||
58
packages/cli/src/commands/git/disconnect.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import chalk from 'chalk';
|
||||
import { Org, Project } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { disconnectGitProvider } from '../../util/projects/connect-git-provider';
|
||||
|
||||
export default async function disconnect(
|
||||
client: Client,
|
||||
args: string[],
|
||||
project: Project | undefined,
|
||||
org: Org | undefined
|
||||
) {
|
||||
const { output } = client;
|
||||
|
||||
if (args.length !== 0) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project disconnect')}`
|
||||
)}`
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
if (!project || !org) {
|
||||
output.error('An unexpected error occurred.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (project.link) {
|
||||
const { org: linkOrg, repo } = project.link;
|
||||
output.print(
|
||||
`Your Vercel project will no longer create deployments when you push to this repository.\n`
|
||||
);
|
||||
const confirmDisconnect = await confirm(
|
||||
client,
|
||||
`Are you sure you want to disconnect ${chalk.cyan(
|
||||
`${linkOrg}/${repo}`
|
||||
)} from your project?`,
|
||||
false
|
||||
);
|
||||
|
||||
if (confirmDisconnect) {
|
||||
await disconnectGitProvider(client, org, project.id);
|
||||
output.log(`Disconnected ${chalk.cyan(`${linkOrg}/${repo}`)}.`);
|
||||
} else {
|
||||
output.log('Aborted.');
|
||||
}
|
||||
} else {
|
||||
output.error(
|
||||
`No Git repository connected. Run ${getCommandName(
|
||||
'project connect'
|
||||
)} to connect one.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
94
packages/cli/src/commands/git/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../../util/client';
|
||||
import { ensureLink } from '../../util/ensure-link';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import validatePaths from '../../util/validate-paths';
|
||||
import connect from './connect';
|
||||
import disconnect from './disconnect';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} git`)} <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
connect Connect your Git config "origin" remote as a Git provider to your project
|
||||
disconnect Disconnect the Git provider repository from your project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Connect a Git provider repository
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} git connect`)}
|
||||
|
||||
${chalk.gray('–')} Disconnect the Git provider repository
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} git disconnect`)}
|
||||
`);
|
||||
};
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
connect: ['connect'],
|
||||
disconnect: ['disconnect'],
|
||||
};
|
||||
|
||||
export default async function main(client: Client) {
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--confirm': Boolean,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
subcommand = argv._[0];
|
||||
const args = argv._.slice(1);
|
||||
const confirm = Boolean(argv['--confirm']);
|
||||
const { output } = client;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
const pathValidation = await validatePaths(client, paths);
|
||||
if (!pathValidation.valid) {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
const { path } = pathValidation;
|
||||
|
||||
const linkedProject = await ensureLink('git', client, path, confirm);
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
|
||||
const { org, project } = linkedProject;
|
||||
|
||||
switch (subcommand) {
|
||||
case 'connect':
|
||||
return await connect(client, argv, args, project, org);
|
||||
case 'disconnect':
|
||||
return await disconnect(client, args, project, org);
|
||||
default:
|
||||
output.error(getInvalidSubcommand(COMMAND_CONFIG));
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default new Map([
|
||||
['domain', 'domains'],
|
||||
['domains', 'domains'],
|
||||
['env', 'env'],
|
||||
['git', 'git'],
|
||||
['help', 'help'],
|
||||
['init', 'init'],
|
||||
['inspect', 'inspect'],
|
||||
@@ -25,8 +26,8 @@ export default new Map([
|
||||
['logout', 'logout'],
|
||||
['logs', 'logs'],
|
||||
['ls', 'list'],
|
||||
['project', 'projects'],
|
||||
['projects', 'projects'],
|
||||
['project', 'project'],
|
||||
['projects', 'project'],
|
||||
['pull', 'pull'],
|
||||
['remove', 'remove'],
|
||||
['rm', 'remove'],
|
||||
|
||||
@@ -7,6 +7,7 @@ import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import init from './init';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import { isError } from '../../util/is-error';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
init: ['init'],
|
||||
@@ -70,9 +71,11 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
return await init(client, argv, args);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.prettyError(err);
|
||||
output.debug(err.stack);
|
||||
if (isError(err) && typeof err.stack === 'string') {
|
||||
output.debug(err.stack);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import Client from '../util/client';
|
||||
import { getDeployment } from '../util/get-deployment';
|
||||
import { Deployment } from '@vercel/client';
|
||||
import { Build } from '../types';
|
||||
import title from 'title';
|
||||
import { isErrnoException } from '../util/is-error';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { URL } from 'url';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -63,7 +67,7 @@ export default async function main(client: Client) {
|
||||
const { print, log, error } = client.output;
|
||||
|
||||
// extract the first parameter
|
||||
const [, deploymentIdOrHost] = argv._;
|
||||
let [, deploymentIdOrHost] = argv._;
|
||||
|
||||
if (argv._.length !== 2) {
|
||||
error(`${getCommandName('inspect <url>')} expects exactly one argument`);
|
||||
@@ -75,8 +79,11 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
) {
|
||||
error(err.message);
|
||||
return 1;
|
||||
}
|
||||
@@ -84,36 +91,50 @@ export default async function main(client: Client) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// resolve the deployment, since we might have been given an alias
|
||||
const depFetchStart = Date.now();
|
||||
|
||||
try {
|
||||
deploymentIdOrHost = new URL(deploymentIdOrHost).hostname;
|
||||
} catch {}
|
||||
client.output.spinner(
|
||||
`Fetching deployment "${deploymentIdOrHost}" in ${chalk.bold(contextName)}`
|
||||
);
|
||||
|
||||
// resolve the deployment, since we might have been given an alias
|
||||
try {
|
||||
deployment = await getDeployment(client, deploymentIdOrHost);
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
error(
|
||||
`Failed to find deployment "${deploymentIdOrHost}" in ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (err.status === 403) {
|
||||
error(
|
||||
`No permission to access deployment "${deploymentIdOrHost}" in ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 404) {
|
||||
error(
|
||||
`Failed to find deployment "${deploymentIdOrHost}" in ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (err.status === 403) {
|
||||
error(
|
||||
`No permission to access deployment "${deploymentIdOrHost}" in ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// unexpected
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { id, name, url, createdAt, routes, readyState } = deployment;
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
createdAt,
|
||||
routes,
|
||||
readyState,
|
||||
alias: aliases,
|
||||
} = deployment;
|
||||
|
||||
const { builds } =
|
||||
deployment.version === 2
|
||||
@@ -121,20 +142,20 @@ export default async function main(client: Client) {
|
||||
: { builds: [] };
|
||||
|
||||
log(
|
||||
`Fetched deployment "${url}" in ${chalk.bold(contextName)} ${elapsed(
|
||||
Date.now() - depFetchStart
|
||||
)}`
|
||||
`Fetched deployment ${chalk.bold(url)} in ${chalk.bold(
|
||||
contextName
|
||||
)} ${elapsed(Date.now() - depFetchStart)}`
|
||||
);
|
||||
|
||||
print('\n');
|
||||
print(chalk.bold(' General\n\n'));
|
||||
print(` ${chalk.cyan('id')}\t\t${id}\n`);
|
||||
print(` ${chalk.cyan('name')}\t${name}\n`);
|
||||
print(` ${chalk.cyan('readyState')}\t${stateString(readyState)}\n`);
|
||||
print(` ${chalk.cyan('url')}\t\t${url}\n`);
|
||||
print(` ${chalk.cyan('status')}\t${stateString(readyState)}\n`);
|
||||
print(` ${chalk.cyan('url')}\t\thttps://${url}\n`);
|
||||
if (createdAt) {
|
||||
print(
|
||||
` ${chalk.cyan('createdAt')}\t${new Date(createdAt)} ${elapsed(
|
||||
` ${chalk.cyan('created')}\t${new Date(createdAt)} ${elapsed(
|
||||
Date.now() - createdAt,
|
||||
true
|
||||
)}\n`
|
||||
@@ -142,6 +163,16 @@ export default async function main(client: Client) {
|
||||
}
|
||||
print('\n\n');
|
||||
|
||||
if (aliases.length > 0) {
|
||||
print(chalk.bold(' Aliases\n\n'));
|
||||
let aliasList = '';
|
||||
for (const alias of aliases) {
|
||||
aliasList += `${chalk.gray('╶')} https://${alias}\n`;
|
||||
}
|
||||
print(indent(aliasList, 4));
|
||||
print('\n\n');
|
||||
}
|
||||
|
||||
if (builds.length > 0) {
|
||||
const times: { [id: string]: string | null } = {};
|
||||
|
||||
@@ -165,19 +196,24 @@ export default async function main(client: Client) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// renders the state string
|
||||
function stateString(s: Deployment['readyState']) {
|
||||
const CIRCLE = '● ';
|
||||
const sTitle = s && title(s);
|
||||
switch (s) {
|
||||
case 'INITIALIZING':
|
||||
return chalk.yellow(s);
|
||||
|
||||
case 'BUILDING':
|
||||
case 'DEPLOYING':
|
||||
case 'ANALYZING':
|
||||
return chalk.yellow(CIRCLE) + sTitle;
|
||||
case 'ERROR':
|
||||
return chalk.red(s);
|
||||
|
||||
return chalk.red(CIRCLE) + sTitle;
|
||||
case 'READY':
|
||||
return s;
|
||||
|
||||
return chalk.green(CIRCLE) + sTitle;
|
||||
case 'QUEUED':
|
||||
return chalk.gray(CIRCLE) + sTitle;
|
||||
case 'CANCELED':
|
||||
return chalk.gray(CIRCLE) + sTitle;
|
||||
default:
|
||||
return chalk.gray(s || 'UNKNOWN');
|
||||
return chalk.gray('UNKNOWN');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import validatePaths from '../util/validate-paths';
|
||||
import { getLinkedProject } from '../util/projects/link';
|
||||
import { ensureLink } from '../util/ensure-link';
|
||||
import getScope from '../util/get-scope';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -152,16 +153,7 @@ export default async function main(client: Client) {
|
||||
|
||||
const { currentTeam } = config;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
({ contextName } = await getScope(client));
|
||||
|
||||
const nextTimestamp = argv['--next'];
|
||||
|
||||
@@ -228,8 +220,8 @@ export default async function main(client: Client) {
|
||||
|
||||
try {
|
||||
await now.findDeployment(app);
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 404) {
|
||||
debug('Ignore findDeployment 404');
|
||||
} else {
|
||||
throw err;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { writeToConfigFile, writeToAuthConfigFile } from '../util/config/files';
|
||||
import getArgs from '../util/get-args';
|
||||
import Client from '../util/client';
|
||||
import { getCommandName, getPkgName } from '../util/pkg-name';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { errorToString } from '../util/is-error';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -63,12 +65,14 @@ export default async function main(client: Client): Promise<number> {
|
||||
method: 'DELETE',
|
||||
useCurrentTeam: false,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 403) {
|
||||
output.debug('Token is invalid so it cannot be revoked');
|
||||
} else if (err.status !== 200) {
|
||||
output.debug(err?.message ?? '');
|
||||
exitCode = 1;
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 403) {
|
||||
output.debug('Token is invalid so it cannot be revoked');
|
||||
} else if (err.status !== 200) {
|
||||
output.debug(err?.message ?? '');
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +90,8 @@ export default async function main(client: Client): Promise<number> {
|
||||
writeToConfigFile(config);
|
||||
writeToAuthConfigFile(authConfig);
|
||||
output.debug('Configuration has been deleted');
|
||||
} catch (err) {
|
||||
output.debug(err?.message ?? '');
|
||||
} catch (err: unknown) {
|
||||
output.debug(errorToString(err));
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getPkgName } from '../util/pkg-name';
|
||||
import getArgs from '../util/get-args';
|
||||
import Client from '../util/client';
|
||||
import { getDeployment } from '../util/get-deployment';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -125,22 +126,24 @@ export default async function main(client: Client) {
|
||||
let deployment;
|
||||
try {
|
||||
deployment = await getDeployment(client, id);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
output.stopSpinner();
|
||||
|
||||
if (err.status === 404) {
|
||||
output.error(
|
||||
`Failed to find deployment "${id}" in ${chalk.bold(contextName)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (err.status === 403) {
|
||||
output.error(
|
||||
`No permission to access deployment "${id}" in ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
if (isAPIError(err)) {
|
||||
if (err.status === 404) {
|
||||
output.error(
|
||||
`Failed to find deployment "${id}" in ${chalk.bold(contextName)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (err.status === 403) {
|
||||
output.error(
|
||||
`No permission to access deployment "${id}" in ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// unexpected
|
||||
throw err;
|
||||
|
||||
56
packages/cli/src/commands/project/add.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
export default async function add(
|
||||
client: Client,
|
||||
args: string[],
|
||||
contextName: string
|
||||
) {
|
||||
const { output } = client;
|
||||
if (args.length !== 1) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project add <name>')}`
|
||||
)}`
|
||||
);
|
||||
|
||||
if (args.length > 1) {
|
||||
const example = chalk.cyan(
|
||||
`${getCommandName(`project add "${args.join(' ')}"`)}`
|
||||
);
|
||||
output.log(
|
||||
`If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const [name] = args;
|
||||
try {
|
||||
await client.fetch('/projects', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
output.log(
|
||||
`${chalk.cyan('Success!')} Project ${chalk.bold(
|
||||
name.toLowerCase()
|
||||
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
91
packages/cli/src/commands/project/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../../util/client';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import getScope from '../../util/get-scope';
|
||||
import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import add from './add';
|
||||
import list from './list';
|
||||
import rm from './rm';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} project`)} [options] <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
-S, --scope Set a custom scope
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Add a new project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} project add my-project`)}
|
||||
|
||||
${chalk.gray('–')} Paginate projects, where ${chalk.dim(
|
||||
'`1584722256178`'
|
||||
)} is the time in milliseconds since the UNIX epoch.
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} project ls --next 1584722256178`)}
|
||||
`);
|
||||
};
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
ls: ['ls', 'list'],
|
||||
add: ['add'],
|
||||
rm: ['rm', 'remove'],
|
||||
};
|
||||
|
||||
export default async function main(client: Client) {
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
subcommand = argv._[0] || 'list';
|
||||
const args = argv._.slice(1);
|
||||
const { output } = client;
|
||||
const { contextName } = await getScope(client);
|
||||
|
||||
switch (subcommand) {
|
||||
case 'ls':
|
||||
case 'list':
|
||||
return await list(client, argv, args, contextName);
|
||||
case 'add':
|
||||
return await add(client, args, contextName);
|
||||
case 'rm':
|
||||
case 'remove':
|
||||
return await rm(client, args);
|
||||
default:
|
||||
output.error(getInvalidSubcommand(COMMAND_CONFIG));
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
86
packages/cli/src/commands/project/list.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import Client from '../../util/client';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import strlen from '../../util/strlen';
|
||||
|
||||
export default async function list(
|
||||
client: Client,
|
||||
argv: any,
|
||||
args: string[],
|
||||
contextName: string
|
||||
) {
|
||||
const { output } = client;
|
||||
if (args.length !== 0) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project ls')}`
|
||||
)}`
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
|
||||
|
||||
let projectsUrl = '/v4/projects/?limit=20';
|
||||
|
||||
const next = argv['--next'] || false;
|
||||
if (next) {
|
||||
projectsUrl += `&until=${next}`;
|
||||
}
|
||||
|
||||
const {
|
||||
projects: list,
|
||||
pagination,
|
||||
}: {
|
||||
projects: [{ name: string; updatedAt: number }];
|
||||
pagination: { count: number; next: number };
|
||||
} = await client.fetch(projectsUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
output.log(
|
||||
`${list.length > 0 ? 'Projects' : 'No projects'} found under ${chalk.bold(
|
||||
contextName
|
||||
)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
const cur = Date.now();
|
||||
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
|
||||
|
||||
const out = table(
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
output.print(`\n${out}\n\n`);
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
|
||||
const nextCmd = `project ls${flags} --next ${pagination.next}`;
|
||||
output.log(`To display the next page run ${getCommandName(nextCmd)}`);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
64
packages/cli/src/commands/project/rm.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
export default async function rm(client: Client, args: string[]) {
|
||||
if (args.length !== 1) {
|
||||
client.output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project rm <name>')}`
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const yes = await readConfirmation(client, name);
|
||||
|
||||
if (!yes) {
|
||||
client.output.log('User abort');
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 404) {
|
||||
client.output.error('No such project exists');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
client.output.log(
|
||||
`${chalk.cyan('Success!')} Project ${chalk.bold(name)} removed ${chalk.gray(
|
||||
`[${elapsed}]`
|
||||
)}`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function readConfirmation(
|
||||
client: Client,
|
||||
projectName: string
|
||||
): Promise<boolean> {
|
||||
client.output.print(
|
||||
prependEmoji(
|
||||
`The project ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`,
|
||||
emoji('warning')
|
||||
)
|
||||
);
|
||||
|
||||
return await confirm(client, `${chalk.bold.red('Are you sure?')}`, false);
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import strlen from '../util/strlen';
|
||||
import getArgs from '../util/get-args';
|
||||
import { handleError, error } from '../util/error';
|
||||
import exit from '../util/exit';
|
||||
import logo from '../util/output/logo';
|
||||
import getScope from '../util/get-scope';
|
||||
import getCommandFlags from '../util/get-command-flags';
|
||||
import { getPkgName, getCommandName } from '../util/pkg-name';
|
||||
import Client from '../util/client';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} projects`)} [options] <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
-S, --scope Set a custom scope
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Add a new project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} projects add my-project`)}
|
||||
|
||||
${chalk.gray('–')} Paginate projects, where ${chalk.dim(
|
||||
'`1584722256178`'
|
||||
)} is the time in milliseconds since the UNIX epoch.
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} projects ls --next 1584722256178`)}
|
||||
`);
|
||||
};
|
||||
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
const main = async (client: Client) => {
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
|
||||
subcommand = argv._[0] || 'list';
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await run({ client, contextName });
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export default async (client: Client) => {
|
||||
try {
|
||||
await main(client);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
async function run({
|
||||
client,
|
||||
contextName,
|
||||
}: {
|
||||
client: Client;
|
||||
contextName: string;
|
||||
}) {
|
||||
const { output } = client;
|
||||
const args = argv._.slice(1);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
if (subcommand === 'ls' || subcommand === 'list') {
|
||||
if (args.length !== 0) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('projects ls')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
|
||||
|
||||
let projectsUrl = '/v4/projects/?limit=20';
|
||||
|
||||
const next = argv['--next'];
|
||||
if (next) {
|
||||
projectsUrl += `&until=${next}`;
|
||||
}
|
||||
|
||||
const {
|
||||
projects: list,
|
||||
pagination,
|
||||
}: {
|
||||
projects: [{ name: string; updatedAt: number }];
|
||||
pagination: { count: number; next: number };
|
||||
} = await client.fetch(projectsUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
console.log(
|
||||
`> ${
|
||||
list.length > 0 ? 'Projects' : 'No projects'
|
||||
} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
const cur = Date.now();
|
||||
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
|
||||
|
||||
const out = table(
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
console.log(`\n${out}\n`);
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
|
||||
const nextCmd = `projects ls${flags} --next ${pagination.next}`;
|
||||
console.log(`To display the next page run ${getCommandName(nextCmd)}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'rm' || subcommand === 'remove') {
|
||||
if (args.length !== 1) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project rm <name>')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const yes = await readConfirmation(name);
|
||||
if (!yes) {
|
||||
console.error(error('User abort'));
|
||||
return exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
console.error(error('No such project exists'));
|
||||
return exit(1);
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
name
|
||||
)} removed ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
if (args.length !== 1) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('projects add <name>')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
||||
if (args.length > 1) {
|
||||
const example = chalk.cyan(
|
||||
`${getCommandName(`projects add "${args.join(' ')}"`)}`
|
||||
);
|
||||
console.log(
|
||||
`> If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
|
||||
);
|
||||
}
|
||||
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const [name] = args;
|
||||
try {
|
||||
await client.fetch('/projects', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
name.toLowerCase()
|
||||
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error('Please specify a valid subcommand: ls | add | rm'));
|
||||
help();
|
||||
exit(2);
|
||||
}
|
||||
|
||||
process.on('uncaughtException', err => {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
function readConfirmation(projectName: string) {
|
||||
return new Promise(resolve => {
|
||||
process.stdout.write(
|
||||
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`
|
||||
);
|
||||
|
||||
process.stdout.write(
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
|
||||
);
|
||||
|
||||
process.stdin
|
||||
.on('data', d => {
|
||||
process.stdin.pause();
|
||||
resolve(d.toString().trim().toLowerCase() === 'y');
|
||||
})
|
||||
.resume();
|
||||
});
|
||||
}
|
||||