Compare commits

..

5 Commits

Author SHA1 Message Date
IgorKlopov
dca8b07952 Revert "break intentionally"
This reverts commit 7c9db4da39.
2020-07-24 18:16:03 +03:00
IgorKlopov
7c9db4da39 break intentionally 2020-07-24 16:17:19 +03:00
IgorKlopov
24a229384e remove tests to make it pass 2020-07-24 15:18:05 +03:00
IgorKlopov
ec0f97d093 regress to make it pass 2020-07-24 14:56:11 +03:00
IgorKlopov
4ff3697d81 print the url of static-single-file/first.png 2020-07-24 14:18:47 +03:00
409 changed files with 10760 additions and 65129 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: styfle/cancel-workflow-action@0.4.1
- uses: styfle/cancel-workflow-action@0.3.2
with:
workflow_id: 849295, 849296, 849297, 849298
access_token: ${{ github.token }}

View File

@@ -20,12 +20,8 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin master --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/master...HEAD --name-only
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- run: yarn install
- run: yarn run build
- uses: actions/setup-node@v1

View File

@@ -20,12 +20,8 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin master --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/master...HEAD --name-only
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- name: Install Hugo
if: matrix.os == 'macos-latest'
run: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/

View File

@@ -15,12 +15,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin master --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/master...HEAD --name-only
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- run: yarn install
- run: yarn run build
- run: yarn test-integration-once --clean false

View File

@@ -20,15 +20,9 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin master --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/master...HEAD --name-only
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: yarn install
- run: yarn run build
- run: yarn run lint

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules
package-lock.json
dist
.vscode
npm-debug.log

View File

@@ -15,7 +15,7 @@ _Live Example: https://ionic-angular.now-examples.now.sh_
To get started with Ionic Angular deployed with Vercel, you can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize the project:
```shell
$ npx @ionic/cli start [project-name] conference --type angular && cd [project-name]
$ npx ionic start [project-name] conference --type angular && cd [project-name]
```
### Deploying From Your Terminal

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
"@ionic-native/in-app-browser": "5.0.0-beta.15",
"@ionic-native/splash-screen": "5.0.0-beta.15",
"@ionic-native/status-bar": "5.0.0-beta.15",
"@ionic/angular": "^5.1.1",
"@ionic/angular": "^5.0.6",
"@ionic/storage": "^2.1.3",
"cordova-android": "^8.1.0",
"cordova-ios": "^5.1.1",

View File

@@ -11,24 +11,20 @@
# next.js
/.next/
/out/
!public/
# production
/build
# misc
.DS_Store
*.pem
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# Environment Variables
.env
.env.build

View File

@@ -1,4 +1,4 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app).
## Getting Started
@@ -21,7 +21,7 @@ To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
You can check out [the Next.js GitHub repository](https://github.com/zeit/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel

View File

@@ -8,8 +8,8 @@
"start": "next start"
},
"dependencies": {
"next": "9.5.1",
"react": "16.13.1",
"react-dom": "16.13.1"
"next": "^9.3.3",
"react": "^16.13.0",
"react-dom": "^16.13.0"
}
}

View File

@@ -1,7 +0,0 @@
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp

View File

@@ -1,6 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default (req, res) => {
res.statusCode = 200
res.json({ name: 'John Doe' })
}

View File

@@ -1,65 +1,203 @@
import Head from 'next/head'
import styles from '../styles/Home.module.css'
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
const Home = () => (
<div className="container">
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<main>
<h1 className="title">
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.js</code>
</p>
<p className="description">
Get started by editing <code>pages/index.js</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h3>Documentation &rarr;</h3>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h3>Learn &rarr;</h3>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/master/examples"
className={styles.card}
>
<h3>Examples &rarr;</h3>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/import?filter=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h3>Deploy &rarr;</h3>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
<div className="grid">
<a href="https://nextjs.org/docs" className="card">
<h3>Documentation &rarr;</h3>
<p>Find in-depth information about Next.js features and API.</p>
</a>
</footer>
</div>
)
}
<a href="https://nextjs.org/learn" className="card">
<h3>Learn &rarr;</h3>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/zeit/next.js/tree/master/examples"
className="card"
>
<h3>Examples &rarr;</h3>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?filter=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className="card"
>
<h3>Deploy &rarr;</h3>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by <img src="/vercel.svg" alt="Vercel Logo" />
</a>
</footer>
<style jsx>{`
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
footer img {
margin-left: 0.5rem;
}
footer a {
display: flex;
justify-content: center;
align-items: center;
}
a {
color: inherit;
text-decoration: none;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
`}</style>
<style jsx global>{`
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
* {
box-sizing: border-box;
}
`}</style>
</div>
)
export default Home

View File

@@ -1,4 +1,3 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>
<svg width="70" height="16" viewBox="0 0 70 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.255.05l9.108 15.753H.148L9.255.05zM39.434 8.418c0-2.535-1.87-4.307-4.554-4.307-2.683 0-4.554 1.772-4.554 4.307 0 2.487 2.019 4.308 4.8 4.308 1.526 0 2.905-.566 3.79-1.6l-1.673-.96c-.517.517-1.28.837-2.117.837-1.23 0-2.29-.665-2.658-1.674l-.074-.172h6.966a3.76 3.76 0 00.074-.739zm-7.065-.738l.05-.148c.32-1.058 1.255-1.698 2.436-1.698 1.207 0 2.117.64 2.437 1.698l.05.148h-4.973zM65.945 8.418c0-2.535-1.871-4.307-4.554-4.307-2.683 0-4.554 1.772-4.554 4.307 0 2.487 2.018 4.308 4.8 4.308 1.526 0 2.904-.566 3.79-1.6l-1.673-.96c-.517.517-1.28.837-2.117.837-1.23 0-2.29-.665-2.659-1.674l-.073-.172h6.966a3.76 3.76 0 00.074-.739zM58.88 7.68l.05-.148c.32-1.058 1.255-1.698 2.436-1.698 1.206 0 2.117.64 2.437 1.698l.05.148H58.88zM54.13 7.015l1.673-.96c-.788-1.23-2.19-1.92-3.89-1.92-2.682 0-4.553 1.773-4.553 4.308 0 2.536 1.87 4.308 4.554 4.308 1.698 0 3.101-.69 3.89-1.92l-1.675-.96c-.443.738-1.23 1.157-2.215 1.157-1.55 0-2.585-1.034-2.585-2.585 0-1.55 1.034-2.585 2.585-2.585.96 0 1.772.419 2.215 1.157zM69.637 1.428h-1.97v11.077h1.97V1.428zM31.779 1.428h-2.265L25.182 8.91l-4.333-7.483H18.56l6.622 11.421 6.597-11.421zM45.71 6.4c.222 0 .444.025.665.074V4.382c-1.673.049-3.249.984-3.249 2.141V4.382h-1.97v8.123h1.97v-3.52c0-1.527 1.059-2.585 2.585-2.585z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,123 +0,0 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@@ -1,16 +0,0 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

View File

@@ -1,4 +1,4 @@
![RedwoodJS Logo](https://github.com/vercel/vercel/blob/master/packages/frameworks/logos/redwoodjs.svg)
![RedwoodJS Logo](https://github.com/vercel/vercel/blob/master/packages/frameworks/logos/redwood.svg)
# RedwoodJS Example

View File

@@ -3,6 +3,6 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "0.15.0"
"@redwoodjs/api": "0.14.0"
}
}

View File

@@ -7,7 +7,7 @@
]
},
"devDependencies": {
"@redwoodjs/core": "0.15.0"
"@redwoodjs/core": "0.14.0"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config"

View File

@@ -6,8 +6,8 @@
"defaults"
],
"dependencies": {
"@redwoodjs/router": "0.15.0",
"@redwoodjs/web": "0.15.0",
"@redwoodjs/router": "0.14.0",
"@redwoodjs/web": "0.14.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1"

File diff suppressed because it is too large Load Diff

View File

@@ -2,17 +2,17 @@
"name": "svelte-app",
"version": "1.0.0",
"devDependencies": {
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-node-resolve": "^8.1.0",
"npm-run-all": "^4.1.5",
"rollup": "^2.18.0",
"rollup": "^1.10.1",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^6.1.0",
"rollup-plugin-terser": "^4.0.4",
"svelte": "^3.0.0"
},
"dependencies": {
"sirv-cli": "^1.0.1"
"sirv-cli": "^0.4.4"
},
"scripts": {
"build": "rollup -c",

View File

@@ -1,6 +1,6 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';

View File

@@ -7,7 +7,6 @@
"tagline": "Blitz.js: The Fullstack React Framework",
"description": "A brand new Blitz.js app - the result of running `npx blitz new`.",
"website": "https://blitzjs.com",
"useRuntime": { "src": "package.json", "use": "@vercel/next" },
"detectors": {
"every": [
{
@@ -37,7 +36,6 @@
"description": "A Next.js app and a Serverless Function API.",
"website": "https://nextjs.org",
"sort": 1,
"useRuntime": { "src": "package.json", "use": "@vercel/next" },
"detectors": {
"every": [
{
@@ -687,13 +685,10 @@
{
"name": "RedwoodJS",
"slug": "redwoodjs",
"demo": "https://redwoodjs.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/redwoodjs.svg",
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/redwood.svg",
"tagline": "RedwoodJS is a full-stack framework for the Jamstack.",
"description": "A RedwoodJS app, bootstraped with create-redwood-app.",
"website": "https://redwoodjs.com",
"useRuntime": { "src": "package.json", "use": "@vercel/redwood" },
"ignoreRuntimes": ["@vercel/node"],
"detectors": {
"every": [
{
@@ -704,13 +699,13 @@
},
"settings": {
"buildCommand": {
"value": "yarn rw build && yarn rw db up --no-db-client --auto-approve && yarn rw dataMigrate up"
"value": "yarn rw db up --no-db-client --auto-approve && yarn rw build"
},
"devCommand": {
"value": "yarn rw dev --fwd=\"--port=$PORT --open=false\""
"value": "yarn rw dev"
},
"outputDirectory": {
"placeholder": "RedwoodJS default"
"value": "RedwoodJS default"
}
}
},
@@ -836,7 +831,7 @@
"description": "No framework or a unoptimized framework.",
"settings": {
"buildCommand": {
"placeholder": "`npm run vercel-build` or `npm run build`"
"placeholder": "`npm run now-build` or `npm run build`"
},
"devCommand": {
"placeholder": "None"

View File

@@ -20,8 +20,6 @@ export interface Framework {
website?: string;
description: string;
sort?: number;
useRuntime?: { src: string; use: string };
ignoreRuntimes?: string[];
detectors?: {
every?: FrameworkDetectionItem[];
some?: FrameworkDetectionItem[];

View File

@@ -0,0 +1 @@
<svg fill="none" height="1000" viewBox="0 0 917 1000" width="917" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m249.557 144.582 194.171 132.54c4.383 2.918 9.502 4.516 14.755 4.606 5.261-.038 10.394-1.641 14.755-4.606l194.319-132.986c7.55-5.406 11.714-14.418 10.957-23.717-.757-9.298-6.322-17.507-14.646-21.6024l-194.171-96.13614c-7.366-3.573948-15.947-3.573948-23.313 0l-193.581 96.13614c-8.474 4.1174-14.113 12.4854-14.783 21.9354-.67 9.451 3.73 18.541 11.537 23.83zm274.879 174.144c.016 8.789 4.318 17.01 11.509 21.991l155.662 106.389c9.965 6.87 23.298 6.012 32.313-2.081l130.579-116.789c5.819-5.199 9.051-12.729 8.823-20.56s-3.892-15.158-10.004-20.005l-124.677-99.702c-9.062-7.199-21.704-7.68-31.28-1.189l-161.416 110.401c-7.064 4.89-11.35 12.914-11.509 21.545zm-387.163 144.724c6.292 5.652 9.526 13.988 8.706 22.437-.817 8.499-5.726 16.052-13.132 20.208l-92.9545 55.72c-9.4227 5.633-21.32 4.82-29.90183-2.041-8.5818-6.861-12.06543-18.346-8.75546-28.865l34.37839-108.172c2.6969-8.57 9.5328-15.175 18.1483-17.533 8.609-2.505 17.8924-.309 24.4928 5.795zm504.168 11.293-168.056-115.007c-8.931-6.01-20.578-6.01-29.509 0l-168.056 115.007c-6.684 4.626-10.919 12.061-11.509 20.208-.435 8.203 2.816 16.169 8.853 21.693l167.909 150.222c4.842 4.319 11.089 6.698 17.558 6.687 6.465-.002 12.708-2.38 17.558-6.687l167.908-150.222c6.056-5.501 9.265-13.5 8.705-21.693-.469-8.146-4.666-15.612-11.361-20.208zm-448.247-29.718-130.4316-116.79c-5.8687-5.331-9.1073-12.995-8.8528-20.95.1419-7.841 3.7705-15.204 9.8856-20.06l124.6768-100.296c9.126-7.179 21.793-7.658 31.428-1.189l161.269 110.401c7.484 4.908 11.998 13.293 11.998 22.288 0 8.994-4.514 17.38-11.998 22.288l-155.515 106.388c-10.025 6.841-23.376 5.985-32.46-2.08zm669.715 167.756-132.792-79.495c-9.862-5.943-22.415-4.739-30.985 2.972l-162.301 144.873c-6.846 6.114-10.062 15.362-8.499 24.441 1.563 9.08 7.681 16.698 16.171 20.135l225.157 91.233c3.088 1.283 6.397 1.939 9.738 1.932 10.449.033 19.936-6.142 24.197-15.751l69.79-156.314c5.68-12.37 1.157-27.062-10.476-34.026zm18.443-190.043 34.379 108.171h-.295c2.542 8.091 1.097 16.919-3.889 23.761-4.986 6.841-12.915 10.876-21.342 10.86-4.728.016-9.37-1.269-13.427-3.715l-93.102-55.72c-7.254-4.243-11.992-11.789-12.689-20.208-.87-8.456 2.373-16.814 8.705-22.436l59.019-52.6c6.668-5.976 15.881-8.156 24.493-5.795 8.609 2.459 15.423 9.098 18.148 17.682zm-492.511 282.761c1.587-9.042-1.597-18.266-8.41-24.368l-162.302-144.873c-8.57-7.711-21.123-8.915-30.985-2.972l-132.7921 79.495c-11.4977 6.995-16.0467 21.502-10.6233 33.878l69.9374 156.314c5.794 13.034 20.774 19.134 33.936 13.818l225.009-91.232c8.492-3.407 14.632-10.995 16.23-20.06zm79.675 44.577 180.598 73.105c8.83 3.779 14.93 12.084 15.935 21.694 1.143 9.729-3.178 19.291-11.214 24.814l-180.745 125.556c-4.331 3.043-9.473 4.7-14.754 4.755-5.277-.082-10.411-1.737-14.755-4.755l-180.597-125.556c-8.066-5.508-12.439-15.061-11.362-24.814 1.206-9.71 7.526-18.006 16.526-21.694l180.597-73.105c6.351-2.532 13.421-2.532 19.771 0z" fill="#bf4722" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" width="48" height="48" viewBox="0 0 917 1000" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m249.557 144.582 194.171 132.54c4.383 2.918 9.502 4.516 14.755 4.606 5.261-.038 10.394-1.641 14.755-4.606l194.319-132.986c7.55-5.406 11.714-14.418 10.957-23.717-.757-9.298-6.322-17.507-14.646-21.6024l-194.171-96.13614c-7.366-3.573948-15.947-3.573948-23.313 0l-193.581 96.13614c-8.474 4.1174-14.113 12.4854-14.783 21.9354-.67 9.451 3.73 18.541 11.537 23.83zm274.879 174.144c.016 8.789 4.318 17.01 11.509 21.991l155.662 106.389c9.965 6.87 23.298 6.012 32.313-2.081l130.579-116.789c5.819-5.199 9.051-12.729 8.823-20.56s-3.892-15.158-10.004-20.005l-124.677-99.702c-9.062-7.199-21.704-7.68-31.28-1.189l-161.416 110.401c-7.064 4.89-11.35 12.914-11.509 21.545zm-387.163 144.724c6.292 5.652 9.526 13.988 8.706 22.437-.817 8.499-5.726 16.052-13.132 20.208l-92.9545 55.72c-9.4227 5.633-21.32 4.82-29.90183-2.041-8.5818-6.861-12.06543-18.346-8.75546-28.865l34.37839-108.172c2.6969-8.57 9.5328-15.175 18.1483-17.533 8.609-2.505 17.8924-.309 24.4928 5.795zm504.168 11.293-168.056-115.007c-8.931-6.01-20.578-6.01-29.509 0l-168.056 115.007c-6.684 4.626-10.919 12.061-11.509 20.208-.435 8.203 2.816 16.169 8.853 21.693l167.909 150.222c4.842 4.319 11.089 6.698 17.558 6.687 6.465-.002 12.708-2.38 17.558-6.687l167.908-150.222c6.056-5.501 9.265-13.5 8.705-21.693-.469-8.146-4.666-15.612-11.361-20.208zm-448.247-29.718-130.4316-116.79c-5.8687-5.331-9.1073-12.995-8.8528-20.95.1419-7.841 3.7705-15.204 9.8856-20.06l124.6768-100.296c9.126-7.179 21.793-7.658 31.428-1.189l161.269 110.401c7.484 4.908 11.998 13.293 11.998 22.288 0 8.994-4.514 17.38-11.998 22.288l-155.515 106.388c-10.025 6.841-23.376 5.985-32.46-2.08zm669.715 167.756-132.792-79.495c-9.862-5.943-22.415-4.739-30.985 2.972l-162.301 144.873c-6.846 6.114-10.062 15.362-8.499 24.441 1.563 9.08 7.681 16.698 16.171 20.135l225.157 91.233c3.088 1.283 6.397 1.939 9.738 1.932 10.449.033 19.936-6.142 24.197-15.751l69.79-156.314c5.68-12.37 1.157-27.062-10.476-34.026zm18.443-190.043 34.379 108.171h-.295c2.542 8.091 1.097 16.919-3.889 23.761-4.986 6.841-12.915 10.876-21.342 10.86-4.728.016-9.37-1.269-13.427-3.715l-93.102-55.72c-7.254-4.243-11.992-11.789-12.689-20.208-.87-8.456 2.373-16.814 8.705-22.436l59.019-52.6c6.668-5.976 15.881-8.156 24.493-5.795 8.609 2.459 15.423 9.098 18.148 17.682zm-492.511 282.761c1.587-9.042-1.597-18.266-8.41-24.368l-162.302-144.873c-8.57-7.711-21.123-8.915-30.985-2.972l-132.7921 79.495c-11.4977 6.995-16.0467 21.502-10.6233 33.878l69.9374 156.314c5.794 13.034 20.774 19.134 33.936 13.818l225.009-91.232c8.492-3.407 14.632-10.995 16.23-20.06zm79.675 44.577 180.598 73.105c8.83 3.779 14.93 12.084 15.935 21.694 1.143 9.729-3.178 19.291-11.214 24.814l-180.745 125.556c-4.331 3.043-9.473 4.7-14.754 4.755-5.277-.082-10.411-1.737-14.755-4.755l-180.597-125.556c-8.066-5.508-12.439-15.061-11.362-24.814 1.206-9.71 7.526-18.006 16.526-21.694l180.597-73.105c6.351-2.532 13.421-2.532 19.771 0z" fill="#bf4722" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.0.18-canary.5",
"version": "0.0.18-canary.0",
"main": "frameworks.json",
"license": "UNLICENSED",
"scripts": {

View File

@@ -64,21 +64,6 @@ const Schema = {
tagline: { type: 'string' },
website: { type: 'string' },
description: { type: 'string' },
useRuntime: {
type: 'object',
required: ['src', 'use'],
additionalProperties: false,
properties: {
src: { type: 'string' },
use: { type: 'string' },
},
},
ignoreRuntimes: {
type: 'array',
items: {
type: 'string',
},
},
detectors: {
type: 'object',
additionalProperties: false,

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.4.3-canary.4",
"version": "2.4.3-canary.2",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -29,7 +29,6 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.0.18-canary.5",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",
"async-sema": "2.1.4",

View File

@@ -2,13 +2,8 @@ import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
import { parse as parsePath, extname } from 'path';
import { Route, Source } from '@vercel/routing-utils';
import _frameworks, { Framework } from '@vercel/frameworks';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
import { isOfficialRuntime } from './';
const frameworkList = _frameworks as Framework[];
const slugToFramework = new Map<string | null, Framework>(
frameworkList.map(f => [f.slug, f])
);
interface ErrorResponse {
code: string;
@@ -111,6 +106,7 @@ export async function detectBuilders(
};
}
const apiMatches = getApiMatches(options);
const sortedFiles = files.sort(sortFiles);
const apiSortedFiles = files.sort(sortFilesBySegmentCount);
@@ -126,16 +122,6 @@ export async function detectBuilders(
const { projectSettings = {} } = options;
const { buildCommand, outputDirectory, framework } = projectSettings;
const ignoreRuntimes = new Set(
slugToFramework.get(framework || '')?.ignoreRuntimes
);
const withTag = options.tag ? `@${options.tag}` : '';
const apiMatches = getApiMatches()
.filter(b => !ignoreRuntimes.has(b.use))
.map(b => {
b.use = `${b.use}${withTag}`;
return b;
});
// If either is missing we'll make the frontend static
const makeFrontendStatic = buildCommand === '' || outputDirectory === '';
@@ -323,6 +309,13 @@ export async function detectBuilders(
options
);
if (frontendBuilder && framework === 'redwoodjs') {
// RedwoodJS uses the /api directory differently so we must
// clear any existing builders and only use `@vercel/redwood`.
builders.length = 0;
builders.push(frontendBuilder);
}
return {
warnings,
builders: builders.length ? builders : null,
@@ -408,15 +401,16 @@ function getFunction(fileName: string, { functions = {} }: Options) {
: { fnPattern: null, func: null };
}
function getApiMatches() {
function getApiMatches({ tag }: Options = {}) {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return [
{ src: 'api/**/*.js', use: `@vercel/node`, config },
{ src: 'api/**/*.ts', use: `@vercel/node`, config },
{ src: 'api/**/!(*_test).go', use: `@vercel/go`, config },
{ src: 'api/**/*.py', use: `@vercel/python`, config },
{ src: 'api/**/*.rb', use: `@vercel/ruby`, config },
{ src: 'api/**/*.js', use: `@vercel/node${withTag}`, config },
{ src: 'api/**/*.ts', use: `@vercel/node${withTag}`, config },
{ src: 'api/**/!(*_test).go', use: `@vercel/go${withTag}`, config },
{ src: 'api/**/*.py', use: `@vercel/python${withTag}`, config },
{ src: 'api/**/*.rb', use: `@vercel/ruby${withTag}`, config },
];
}
@@ -456,7 +450,7 @@ function detectFrontBuilder(
config.outputDirectory = projectSettings.outputDirectory;
}
if (pkg && framework !== null) {
if (pkg) {
const deps: PackageJson['dependencies'] = {
...pkg.dependencies,
...pkg.devDependencies,
@@ -477,10 +471,12 @@ function detectFrontBuilder(
});
}
const f = slugToFramework.get(framework || '');
if (f && f.useRuntime) {
const { src, use } = f.useRuntime;
return { src, use: `${use}${withTag}`, config };
if (framework === 'nextjs' || framework === 'blitzjs') {
return { src: 'package.json', use: `@vercel/next${withTag}`, config };
}
if (framework === 'redwoodjs') {
return { src: 'package.json', use: `@vercel/redwood${withTag}`, config };
}
// Entrypoints for other frameworks
@@ -1045,5 +1041,5 @@ function sortFilesBySegmentCount(fileA: string, fileB: string): number {
return -1;
}
return fileA.localeCompare(fileB);
return 0;
}

View File

@@ -1080,51 +1080,6 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(errorRoutes).toStrictEqual([]);
});
it('Using "Other" framework with Storybook should NOT autodetect Next.js', async () => {
const pkg = {
scripts: {
dev: 'next dev',
build: 'next build',
storybook: 'start-storybook -p 6006',
'build-storybook': 'build-storybook',
},
dependencies: {
next: '9.3.5',
react: '16.13.1',
'react-dom': '16.13.1',
},
devDependencies: {
'@babel/core': '7.9.0',
'@storybook/addon-links': '5.3.18',
'@storybook/addons': '5.3.18',
'@storybook/react': '5.3.18',
},
};
const files = ['package.json', 'pages/api/foo.js', 'index.html'];
const projectSettings = {
framework: null, // Selected "Other" framework
buildCommand: 'yarn build-storybook',
};
const { builders, errorRoutes } = await detectBuilders(files, pkg, {
projectSettings,
featHandleMiss,
});
expect(builders).toEqual([
{
use: '@vercel/static-build',
src: 'package.json',
config: {
zeroConfig: true,
buildCommand: projectSettings.buildCommand,
},
},
]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
});
it('api + raw static', async () => {
const files = ['api/endpoint.js', 'index.html', 'favicon.ico'];
@@ -1853,38 +1808,23 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect((errorRoutes![0] as Source).status).toBe(404);
});
const redwoodFiles = [
'package.json',
'web/package.json',
'web/public/robots.txt',
'web/src/index.html',
'web/src/index.css',
'web/src/index.js',
'api/package.json',
'api/prisma/seeds.js',
'api/src/functions/graphql.js',
'api/src/graphql/.keep',
'api/src/services/.keep',
'api/src/lib/db.js',
];
it('RedwoodJS should only use Redwood builder and not Node builder', async () => {
const files = [...redwoodFiles].sort();
it('RedwoodJS should only use redwood builder', async () => {
const files = [
'package.json',
'web/index.html',
'api/one.js',
'api/two.js',
];
const projectSettings = {
framework: 'redwoodjs',
};
const {
builders,
defaultRoutes,
rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, {
const { builders, errorRoutes } = await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
expect(builders).toStrictEqual([
expect(builders).toEqual([
{
use: '@vercel/redwood',
src: 'package.json',
@@ -1894,79 +1834,8 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
},
},
]);
expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([]);
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
dest: '/404.html',
},
]);
});
it('RedwoodJS should allow usage of non-js API', async () => {
const files = [...redwoodFiles, 'api/golang.go', 'api/python.py'].sort();
const projectSettings = {
framework: 'redwoodjs',
};
const {
builders,
defaultRoutes,
rewriteRoutes,
errorRoutes,
} = await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
expect(builders).toStrictEqual([
{
use: '@vercel/go',
src: 'api/golang.go',
config: {
zeroConfig: true,
},
},
{
use: '@vercel/python',
src: 'api/python.py',
config: {
zeroConfig: true,
},
},
{
use: '@vercel/redwood',
src: 'package.json',
config: {
zeroConfig: true,
framework: 'redwoodjs',
},
},
]);
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
src: '^/api/(.+)(?:\\.(?:go|py))$',
dest: '/api/$1',
check: true,
},
]);
expect(rewriteRoutes).toStrictEqual([
{
status: 404,
src: '^/api(/.*)?$',
continue: true,
},
]);
expect(errorRoutes).toStrictEqual([
{
status: 404,
src: '^/(?!.*api).*$',
dest: '/404.html',
},
]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
});
it('No framework, only package.json', async () => {

View File

@@ -13,7 +13,7 @@
"outDir": "./dist",
"types": ["node", "jest"],
"strict": true,
"target": "es2019"
"target": "esnext"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "20.0.0-canary.21",
"version": "20.0.0-canary.4",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -14,6 +14,7 @@
"preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js test/dev-validate.unit.js --serial --fail-fast --verbose",
"test-integration-cli": "ava test/integration.js --serial --fail-fast --verbose",
"test-integration-v1": "ava test/integration-v1.js --serial --fail-fast",
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"prepublishOnly": "yarn build",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
@@ -61,14 +62,14 @@
"node": ">= 10"
},
"dependencies": {
"@vercel/build-utils": "2.4.3-canary.4",
"@vercel/build-utils": "2.4.3-canary.2",
"@vercel/go": "1.1.5-canary.0",
"@vercel/next": "2.6.20",
"@vercel/node": "1.7.5-canary.1",
"@vercel/next": "2.6.14-canary.1",
"@vercel/node": "1.7.4-canary.0",
"@vercel/python": "1.2.2",
"@vercel/redwood": "0.0.2-canary.7",
"@vercel/redwood": "0.0.2-canary.1",
"@vercel/ruby": "1.2.3",
"@vercel/static-build": "0.17.7-canary.3",
"@vercel/static-build": "0.17.7-canary.1",
"update-notifier": "4.1.0"
},
"devDependencies": {
@@ -103,7 +104,7 @@
"@types/universal-analytics": "0.4.2",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.0.18-canary.5",
"@zeit/dockerignore": "0.0.5",
"@zeit/fun": "0.11.2",
"@zeit/ncc": "0.18.5",
"@zeit/source-map-support": "0.6.2",
@@ -143,6 +144,7 @@
"glob": "7.1.2",
"http-proxy": "1.17.0",
"ignore": "4.0.6",
"ini": "1.3.4",
"inquirer": "7.0.4",
"is-port-reachable": "3.0.0",
"is-url": "1.2.2",

View File

@@ -1,3 +1,4 @@
//
import chalk from 'chalk';
import { handleError } from '../../util/error';
@@ -31,11 +32,15 @@ const help = () => {
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
-r ${chalk.bold.underline('RULES_FILE')}, --rules=${chalk.bold.underline(
'RULES_FILE'
)} Rules file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-S, --scope Set a custom scope
-n, --no-verify Don't wait until instance count meets the previous alias constraints
-N, --next Show next page of results
${chalk.dim('Examples:')}
@@ -61,6 +66,30 @@ const help = () => {
${chalk.dim('')} ${chalk.dim(
'Protocols'
)} in the URLs are unneeded and ignored.
${chalk.gray('')} Add and modify path based aliases for ${chalk.underline(
'example.com'
)}
${chalk.cyan(
`$ ${getPkgName()} alias ${chalk.underline(
'example.com'
)} -r ${chalk.underline('rules.json')}`
)}
Export effective routing rules
${chalk.cyan(
`$ ${getPkgName()} alias ls aliasId --json > ${chalk.underline(
'rules.json'
)}`
)}
${chalk.gray('')} Paginate results, where ${chalk.dim(
'`1584722256178`'
)} is the time in milliseconds since the UNIX epoch.
${chalk.cyan(`$ ${getPkgName()} alias ls --next 1584722256178`)}
`);
};
@@ -77,8 +106,12 @@ export default async function main(ctx) {
try {
argv = getArgs(ctx.argv.slice(2), {
'--json': Boolean,
'--no-verify': Boolean,
'--rules': String,
'--yes': Boolean,
'--next': Number,
'-n': '--no-verify',
'-r': '--rules',
'-y': '--yes',
'-N': '--next',
});

View File

@@ -1,3 +1,4 @@
import ms from 'ms';
import chalk from 'chalk';
import { SetDifference } from 'utility-types';
import { AliasRecord } from '../../util/alias/create-alias';
@@ -6,24 +7,28 @@ import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import assignAlias from '../../util/alias/assign-alias';
import Client from '../../util/client';
import formatDnsTable from '../../util/format-dns-table';
import formatNSTable from '../../util/format-ns-table';
import getDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host';
import { getDeploymentForAlias } from '../../util/alias/get-deployment-by-alias';
import getDeploymentForAlias from '../../util/alias/get-deployment-for-alias';
import getRulesFromFile from '../../util/alias/get-rules-from-file';
import getScope from '../../util/get-scope';
import { getTargetsForAlias } from '../../util/alias/get-targets-for-alias';
import humanizePath from '../../util/humanize-path';
import setupDomain from '../../util/domains/setup-domain';
import stamp from '../../util/output/stamp';
import { isValidName } from '../../util/is-valid-name';
import upsertPathAlias from '../../util/alias/upsert-path-alias';
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 { NowConfig } from '../../util/dev/types';
type Options = {
'--debug': boolean;
'--local-config': string;
'--no-verify': boolean;
'--rules': string;
};
export default async function set(
@@ -42,7 +47,11 @@ export default async function set(
const { apiUrl } = ctx;
const setStamp = stamp();
const { '--debug': debugEnabled } = opts;
const {
'--debug': debugEnabled,
'--no-verify': noVerify,
'--rules': rulesPath,
} = opts;
const client = new Client({
apiUrl,
@@ -87,7 +96,35 @@ export default async function set(
return 1;
}
if (args.length === 0) {
// Read the path alias rules in case there is is given
const rules = await getRulesFromFile(rulesPath);
if (rules instanceof ERRORS.FileNotFound) {
output.error(`Can't find the provided rules file at location:`);
output.print(` ${chalk.gray('-')} ${rules.meta.file}\n`);
return 1;
}
if (rules instanceof ERRORS.CantParseJSONFile) {
output.error(`Error parsing provided rules.json file at location:`);
output.print(` ${chalk.gray('-')} ${rules.meta.file}\n`);
return 1;
}
if (rules instanceof ERRORS.RulesFileValidationError) {
output.error(`Path Alias validation error: ${rules.meta.message}`);
output.print(` ${chalk.gray('-')} ${rules.meta.location}\n`);
return 1;
}
// If the user provided rules and also a deployment target, we should fail
if (args.length === 2 && rules) {
output.error(
`You can't supply a deployment target and target rules simultaneously.`
);
return 1;
}
if (args.length === 0 && !rules) {
output.error(
`To ship to production, optionally configure your domains (${link(
'https://vercel.com/docs/v2/custom-domains'
@@ -96,79 +133,62 @@ export default async function set(
return 1;
}
// For `now alias set <argument>`
if (args.length === 1) {
const deployment = handleCertError(
output,
await getDeploymentForAlias(
client,
output,
args,
opts['--local-config'],
user,
contextName,
localConfig
)
// Find the targets to perform the alias
const targets = getTargetsForAlias(args, localConfig);
if (targets instanceof ERRORS.NoAliasInConfig) {
output.error(`Couldn't find an alias in config`);
return 1;
}
if (targets instanceof ERRORS.InvalidAliasInConfig) {
output.error(
`Wrong value for alias found in config. It must be a string or array of string.`
);
return 1;
}
if (deployment === 1) {
return deployment;
}
if (deployment instanceof Error) {
output.error(deployment.message);
return 1;
}
if (!deployment) {
output.error(
`Couldn't find a deployment to alias. Please provide one as an argument.`
);
return 1;
}
// Find the targets to perform the alias
const targets = getTargetsForAlias(args, localConfig);
if (targets instanceof Error) {
output.prettyError(targets);
return 1;
}
if (rules) {
// If we have rules for path alias we assign them to the domain
for (const target of targets) {
output.log(`Assigning alias ${target} to deployment ${deployment.url}`);
const record = await assignAlias(
output.log(
`Assigning path alias rules from ${humanizePath(
rulesPath
)} to ${target}`
);
const pathAlias = await upsertPathAlias(
output,
client,
deployment,
rules,
target,
contextName
);
const handleResult = handleSetupDomainError(
output,
handleCreateAliasError(output, record)
);
if (handleResult === 1) {
return 1;
const remaining = handleCreateAliasError(output, pathAlias);
if (handleSetupDomainError(output, remaining) !== 1) {
console.log(
`${chalk.cyan('> Success!')} ${
rules.length
} rules configured for ${chalk.underline(target)} ${setStamp()}`
);
}
console.log(
`${chalk.cyan('> Success!')} ${chalk.bold(
`${isWildcardAlias(target) ? '' : 'https://'}${handleResult.alias}`
)} now points to https://${deployment.url} ${setStamp()}`
);
}
return 0;
}
const [deploymentIdOrHost, aliasTarget] = args;
// If there are no rules for path alias we should find out a deployment and perform the alias
const deployment = handleCertError(
output,
await getDeploymentByIdOrHost(client, contextName, deploymentIdOrHost)
await getDeploymentForAlias(
client,
output,
args,
opts['--local-config'],
user,
contextName,
localConfig
)
);
if (deployment === 1) {
@@ -205,32 +225,37 @@ export default async function set(
return 1;
}
output.log(`Assigning alias ${aliasTarget} to deployment ${deployment.url}`);
// Assign the alias for each of the targets in the array
for (const target of targets) {
output.log(`Assigning alias ${target} to deployment ${deployment.url}`);
const isWildcard = isWildcardAlias(aliasTarget);
const record = await assignAlias(
output,
client,
deployment,
aliasTarget,
contextName
);
const handleResult = handleSetupDomainError(
output,
handleCreateAliasError(output, record)
);
if (handleResult === 1) {
return 1;
const isWildcard = isWildcardAlias(target);
const record = await assignAlias(
output,
client,
deployment,
target,
contextName,
noVerify
);
const handleResult = handleSetupDomainError(
output,
handleCreateAliasError(output, record),
isWildcard
);
if (handleResult === 1) {
return 1;
}
const prefix = isWildcard ? '' : 'https://';
console.log(
`${chalk.cyan('> Success!')} ${chalk.bold(
`${prefix}${handleResult.alias}`
)} now points to https://${deployment.url} ${setStamp()}`
);
}
const prefix = isWildcard ? '' : 'https://';
console.log(
`${chalk.cyan('> Success!')} ${chalk.bold(
`${prefix}${handleResult.alias}`
)} now points to https://${deployment.url} ${setStamp()}`
);
return 0;
}
@@ -240,7 +265,8 @@ type SetupDomainError = Exclude<SetupDomainResolve, Domain>;
function handleSetupDomainError<T>(
output: Output,
error: SetupDomainError | T
error: SetupDomainError | T,
isWildcard: boolean = false
): T | 1 {
if (
error instanceof ERRORS.DomainVerificationFailed ||
@@ -252,7 +278,9 @@ function handleSetupDomainError<T>(
`We could not alias since the domain ${domain} could not be verified due to the following reasons:\n`
);
output.print(
`Nameservers verification failed since we see a different set than the intended set:`
` ${chalk.gray(
'a)'
)} Nameservers verification failed since we see a different set than the intended set:`
);
output.print(
`\n${formatNSTable(
@@ -261,6 +289,34 @@ function handleSetupDomainError<T>(
{ extraSpace: ' ' }
)}\n\n`
);
if (error instanceof ERRORS.DomainVerificationFailed && !isWildcard) {
const { txtVerification } = error.meta;
output.print(
` ${chalk.gray(
'b)'
)} DNS TXT verification failed since found no matching records.`
);
output.print(
`\n${formatDnsTable(
[['_now', 'TXT', txtVerification.verificationRecord]],
{ extraSpace: ' ' }
)}\n\n`
);
output.print(
` Once your domain uses either the nameservers or the TXT DNS record from above, run again ${getCommandName(
'domains verify <domain>'
)}.\n`
);
output.print(
` We will also periodically run a verification check for you and you will receive an email once your domain is verified.\n`
);
} else {
output.print(
` Once your domain uses the nameservers from above, run again ${getCommandName(
'domains verify <domain>'
)}.\n`
);
}
output.print(' Read more: https://err.sh/now/domain-verification\n');
return 1;
}
@@ -401,6 +457,66 @@ function handleCreateAliasError<T>(
return 1;
}
if (error instanceof ERRORS.RuleValidationFailed) {
output.error(`Rule validation error: ${error.meta.message}.`);
output.print(` Make sure your rules file is written correctly.\n`);
return 1;
}
if (error instanceof ERRORS.VerifyScaleTimeout) {
output.error(`Instance verification timed out (${ms(error.meta.timeout)})`);
output.log('Read more: https://err.sh/now/verification-timeout');
return 1;
}
if (error instanceof ERRORS.NotSupportedMinScaleSlots) {
output.error(
`Scale rules from previous aliased deployment ${chalk.dim(
error.meta.url
)} could not be copied since Cloud v2 deployments cannot have a non-zero min`
);
output.log(
`Update the scale settings on ${chalk.dim(
error.meta.url
)} with ${getCommandName('scale')} and try again`
);
output.log('Read more: https://err.sh/now/v2-no-min');
return 1;
}
if (error instanceof ERRORS.ForbiddenScaleMaxInstances) {
output.error(
`Scale rules from previous aliased deployment ${chalk.dim(
error.meta.url
)} could not be copied since the given number of max instances (${
error.meta.max
}) is not allowed.`
);
output.log(
`Update the scale settings on ${chalk.dim(
error.meta.url
)} with ${getCommandName('scale')} and try again`
);
return 1;
}
if (error instanceof ERRORS.ForbiddenScaleMinInstances) {
output.error(
`You can't scale to more than ${error.meta.max} min instances with your current plan.`
);
return 1;
}
if (error instanceof ERRORS.InvalidScaleMinMaxRelation) {
output.error(
`Scale rules from previous aliased deployment ${chalk.dim(
error.meta.url
)} could not be copied becuase the relation between min and max instances is wrong.`
);
output.log(
`Update the scale settings on ${chalk.dim(
error.meta.url
)} with ${getCommandName('scale')} and try again`
);
return 1;
}
if (error instanceof ERRORS.CertMissing) {
output.error(
`There is no certificate for the domain ${error.meta.domain} and it could not be created.`
@@ -436,22 +552,3 @@ function handleCreateAliasError<T>(
return error;
}
function getTargetsForAlias(args: string[], { alias }: NowConfig) {
if (args.length) {
return [args[args.length - 1]]
.map(target => (target.indexOf('.') !== -1 ? toHost(target) : target))
.filter((x): x is string => !!x && typeof x === 'string');
}
if (!alias) {
return new ERRORS.NoAliasInConfig();
}
// Check the type for the option aliases
if (typeof alias !== 'string' && !Array.isArray(alias)) {
return new ERRORS.InvalidAliasInConfig(alias);
}
return typeof alias === 'string' ? [alias] : alias;
}

View File

@@ -7,7 +7,7 @@ import * as ERRORS from '../../util/errors-ts';
import { Output } from '../../util/output';
import deleteCertById from '../../util/certs/delete-cert-by-id';
import getCertById from '../../util/certs/get-cert-by-id';
import { getCustomCertsForDomain } from '../../util/certs/get-custom-certs-for-domain';
import getCertsForDomain from '../../util/certs/get-certs-for-domain';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
@@ -66,17 +66,9 @@ async function rm(
}
if (certs.length === 0) {
if (id.includes('.')) {
output.error(
`No custom certificates found for "${id}" under ${chalk.bold(
contextName
)}`
);
} else {
output.error(
`No certificates found by id "${id}" under ${chalk.bold(contextName)}`
);
}
output.error(
`No certificates found by id "${id}" under ${chalk.bold(contextName)}`
);
return 1;
}
@@ -109,7 +101,7 @@ async function getCertsToDelete(
) {
const cert = await getCertById(client, id);
if (cert instanceof ERRORS.CertNotFound) {
const certs = await getCustomCertsForDomain(client, contextName, id);
const certs = await getCertsForDomain(output, client, contextName, id);
if (certs instanceof ERRORS.CertsPermissionDenied) {
return certs;
}
@@ -133,7 +125,12 @@ function readConfirmation(output: Output, msg: string, certs: Cert[]) {
process.stdin
.on('data', d => {
process.stdin.pause();
resolve(d.toString().trim().toLowerCase() === 'y');
resolve(
d
.toString()
.trim()
.toLowerCase() === 'y'
);
})
.resume();
});

View File

@@ -4,7 +4,7 @@ import code from '../../util/output/code';
import note from '../../util/output/note';
import { getPkgName } from '../../util/pkg-name.ts';
export const help = () => `
export const latestHelp = () => `
${chalk.bold(`${logo} ${getPkgName()}`)} [options] <command | path>
${chalk.dim('Commands:')}
@@ -100,7 +100,7 @@ export const help = () => `
`;
export const args = {
export const latestArgs = {
'--force': Boolean,
'--with-cache': Boolean,
'--public': Boolean,
@@ -108,6 +108,7 @@ export const args = {
'--env': [String],
'--build-env': [String],
'--meta': [String],
'--no-scale': Boolean,
// This is not an array in favor of matching
// the config property name.
'--regions': String,
@@ -126,3 +127,180 @@ export const args = {
'-n': '--name',
'--target': String,
};
export const legacyArgsMri = {
string: [
'name',
'build-env',
'alias',
'meta',
'session-affinity',
'regions',
'dotenv',
'target',
],
boolean: [
'help',
'version',
'debug',
'force',
'links',
'C',
'clipboard',
'forward-npm',
'docker',
'npm',
'static',
'public',
'no-scale',
'no-verify',
'dotenv',
'prod',
],
default: {
C: false,
clipboard: true,
},
alias: {
env: 'e',
meta: 'm',
'build-env': 'b',
dotenv: 'E',
help: 'h',
debug: 'd',
version: 'v',
force: 'f',
links: 'l',
public: 'p',
'forward-npm': 'N',
'session-affinity': 'S',
name: 'n',
project: 'P',
alias: 'a',
},
};
// The following arg parsing is simply to make it compatible
// with the index. Let's not migrate it to the new args parsing, as
// we are gonna delete this file soon anyways.
const argList = {};
for (const item of legacyArgsMri.string) {
argList[`--${item}`] = String;
}
for (const item of legacyArgsMri.boolean) {
argList[`--${item}`] = Boolean;
}
for (const item of Object.keys(legacyArgsMri.alias)) {
argList[`-${legacyArgsMri.alias[item]}`] = `--${item}`;
}
export const legacyArgs = argList;
export const legacyHelp = () => `
${chalk.bold(`${logo} now`)} [options] <command | path>
${chalk.dim('Commands:')}
${chalk.dim('Cloud')}
deploy [path] Performs a deployment ${chalk.bold(
'(default)'
)}
ls | list [app] Lists deployments
rm | remove [id] Removes a deployment
ln | alias [id] [url] Configures aliases for deployments
inspect [id] Displays information related to a deployment
domains [name] Manages your domain names
certs [cmd] Manages your SSL certificates
secrets [name] Manages your secret environment variables
dns [name] Manages your DNS records
logs [url] Displays the logs for a deployment
scale [args] Scales the instance count of a deployment
init [example] Initialize an example project
help [cmd] Displays complete help for [cmd]
${chalk.dim('Administrative')}
billing | cc [cmd] Manages your credit cards and billing methods
upgrade | downgrade [plan] Upgrades or downgrades your plan
teams Manages your teams
switch [scope] Switches between teams and your account
login [email] Logs into your account or creates a new one
logout Logs out of your account
whoami Shows the username of the currently logged in user
${chalk.dim('Options:')}
-h, --help Output usage information
-v, --version Output the version number
-V, --platform-version Set the platform version to deploy to
-n, --name Set the project name of the deployment
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
-f, --force Force a new deployment even if nothing has changed
-t ${chalk.underline('TOKEN')}, --token=${chalk.underline(
'TOKEN'
)} Login token
-l, --links Copy symlinks without resolving their target
-p, --public Deployment is public (${chalk.dim(
'`/_src`'
)} is exposed) [on for oss, off for premium]
-e, --env Include an env var during run time (e.g.: ${chalk.dim(
'`-e KEY=value`'
)}). Can appear many times.
-b, --build-env Similar to ${chalk.dim(
'`--env`'
)} but for build time only.
-m, --meta Add metadata for the deployment (e.g.: ${chalk.dim(
'`-m KEY=value`'
)}). Can appear many times.
-E ${chalk.underline('FILE')}, --dotenv=${chalk.underline(
'FILE'
)} Include env vars from .env file. Defaults to '.env'
-C, --no-clipboard Do not attempt to copy URL to clipboard
-N, --forward-npm Forward login information to install private npm modules
--session-affinity Session affinity, \`ip\` or \`random\` (default) to control session affinity
-S, --scope Set a custom scope
--regions Set default regions or DCs to enable the deployment on
--no-scale Skip scaling rules deploying with the default presets
--no-verify Skip step of waiting until instance count meets given constraints
${chalk.dim(`Enforceable Types (by default, it's detected automatically):`)}
--npm Node.js application
--docker Docker container
--static Static file hosting
${chalk.dim('Examples:')}
${chalk.gray('')} Deploy the current directory
${chalk.cyan('$ now')}
${chalk.gray('')} Deploy a custom path
${chalk.cyan('$ now /usr/src/project')}
${chalk.gray('')} Deploy a GitHub repository
${chalk.cyan('$ now user/repo#ref')}
${chalk.gray('')} Deploy with environment variables
${chalk.cyan('$ now -e NODE_ENV=production -e SECRET=@mysql-secret')}
${chalk.gray('')} Show the usage information for the sub command ${chalk.dim(
'`list`'
)}
${chalk.cyan('$ now help list')}
`;

View File

@@ -1,16 +1,22 @@
import fs from 'fs-extra';
import { resolve, basename } from 'path';
import { resolve, basename, parse, join } from 'path';
import { fileNameSymbol } from '@vercel/client';
import Client from '../../util/client.ts';
import getScope from '../../util/get-scope.ts';
import createOutput from '../../util/output';
import code from '../../util/output/code';
import highlight from '../../util/output/highlight';
import param from '../../util/output/param.ts';
import { readLocalConfig } from '../../util/config/files';
import getArgs from '../../util/get-args';
import * as parts from './args';
import { handleError } from '../../util/error';
import { help, args } from './args';
import deploy from './latest';
import readPackage from '../../util/read-package';
import preferV2Deployment, {
hasDockerfile,
hasServerfile,
} from '../../util/prefer-v2-deployment';
import getProjectName from '../../util/get-project-name';
export default async ctx => {
const {
@@ -18,12 +24,14 @@ export default async ctx => {
config: { currentTeam },
apiUrl,
} = ctx;
const combinedArgs = Object.assign({}, parts.legacyArgs, parts.latestArgs);
let platformVersion = null;
let contextName = currentTeam || 'current user';
let argv = null;
try {
argv = getArgs(ctx.argv.slice(2), args);
argv = getArgs(ctx.argv.slice(2), combinedArgs);
} catch (error) {
handleError(error);
return 1;
@@ -50,8 +58,12 @@ export default async ctx => {
const debugEnabled = argv['--debug'];
const output = createOutput({ debug: debugEnabled });
const stats = {};
const versionFlag = argv['--platform-version'];
if (argv['--help']) {
const lastArg = argv._[argv._.length - 1];
const help = lastArg === 'deploy-v1' ? parts.legacyHelp : parts.latestHelp;
output.print(help());
return 2;
}
@@ -60,15 +72,28 @@ export default async ctx => {
try {
stats[path] = await fs.lstat(path);
} catch (err) {
output.error(
`The specified file or directory "${basename(path)}" does not exist.`
);
return 1;
const { ext } = parse(path);
if (versionFlag === 1 && !ext) {
// This will ensure `-V 1 zeit/serve` (GitHub deployments) work. Since
// GitHub repositories are never just one file, we need to set
// the `isFile` property accordingly.
stats[path] = {
isFile: () => false,
};
} else {
output.error(
`The specified file or directory "${basename(path)}" does not exist.`
);
return 1;
}
}
}
let client = null;
const isFile = Object.keys(stats).length === 1 && stats[paths[0]].isFile();
if (authConfig && authConfig.token) {
client = new Client({
apiUrl,
@@ -77,7 +102,7 @@ export default async ctx => {
debug: debugEnabled,
});
try {
({ contextName } = await getScope(client));
({ contextName, platformVersion } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
@@ -95,14 +120,17 @@ export default async ctx => {
if (version) {
if (typeof version === 'number') {
if (version !== 2) {
const two = code(2);
if (version !== 1 && version !== 2) {
const first = code(1);
const second = code(2);
output.error(
`The value of the ${prop} property within ${file} can only be ${two}.`
`The value of the ${prop} property within ${file} can only be ${first} or ${second}.`
);
return 1;
}
platformVersion = version;
} else {
output.error(
`The ${prop} property inside your ${file} file must be a number.`
@@ -112,5 +140,61 @@ export default async ctx => {
}
}
return deploy(ctx, contextName, output, stats, localConfig, args);
if (versionFlag) {
if (versionFlag !== 1 && versionFlag !== 2) {
output.error(
`The ${param('--platform-version')} option must be either ${code(
'1'
)} or ${code('2')}.`
);
return 1;
}
platformVersion = versionFlag;
}
if (
platformVersion === 1 &&
versionFlag !== 1 &&
!argv['--docker'] &&
!argv['--npm']
) {
// Only check when it was not set via CLI flag
const reason = await preferV2Deployment({
client,
localConfig,
projectName: getProjectName({
argv,
nowConfig: localConfig || {},
isFile,
paths,
}),
hasServerfile: await hasServerfile(paths[0]),
hasDockerfile: await hasDockerfile(paths[0]),
pkg: await readPackage(join(paths[0], 'package.json')),
});
if (reason) {
output.note(reason);
platformVersion = 2;
}
}
if (platformVersion === null || platformVersion > 1) {
return require('./latest').default(
ctx,
contextName,
output,
stats,
localConfig,
parts.latestArgs
);
}
return require('./legacy').default(
ctx,
contextName,
output,
parts.legacyArgsMri
);
};

View File

@@ -669,7 +669,7 @@ export default async function main(
}
if (err instanceof BuildError) {
output.error(err.message || 'Build failed');
output.error('Build failed');
output.error(
`Check your logs at https://${now.url}/_logs or run ${getCommandName(
`logs ${now.url}`,

File diff suppressed because it is too large Load Diff

View File

@@ -128,7 +128,7 @@ export default async function add(
return 1;
}
const domainConfig = await getDomainConfig(client, domainName);
const domainConfig = await getDomainConfig(client, contextName, domainName);
if (domainConfig.misconfigured) {
output.warn(
@@ -142,7 +142,7 @@ export default async function add(
);
output.print(
` ${chalk.grey('b)')} ` +
`Change your Domains's nameservers to the intended set`
`Change your domain nameservers to the intended set`
);
output.print(
`\n${formatNSTable(

View File

@@ -71,9 +71,8 @@ export default async function buy(
const availableStamp = stamp();
const domainPrice = await getDomainPrice(client, domainName);
if (domainPrice instanceof Error) {
output.prettyError(domainPrice);
if (domainPrice instanceof ERRORS.UnsupportedTLD) {
output.error(`The TLD for ${param(domainName)} is not supported.`);
return 1;
}

View File

@@ -14,7 +14,6 @@ import inspect from './inspect';
import ls from './ls';
import rm from './rm';
import move from './move';
import verify from './verify';
import { getPkgName } from '../../util/pkg-name';
const help = () => {
@@ -82,7 +81,6 @@ const COMMAND_CONFIG = {
move: ['move'],
rm: ['rm', 'remove'],
transferIn: ['transfer-in'],
verify: ['verify'],
};
export default async function main(ctx: NowContext) {
@@ -121,8 +119,6 @@ export default async function main(ctx: NowContext) {
return rm(ctx, argv, args, output);
case 'transferIn':
return transferIn(ctx, argv, args, output);
case 'verify':
return verify(ctx, argv, args, output);
default:
return ls(ctx, argv, args, output);
}

View File

@@ -14,8 +14,6 @@ import getDomainPrice from '../../util/domains/get-domain-price';
import { getCommandName } from '../../util/pkg-name';
import { getDomainConfig } from '../../util/domains/get-domain-config';
import code from '../../util/output/code';
import wait from '../../util/output/wait';
import { getDomainRegistrar } from '../../util/domains/get-domain-registrar';
type Options = {
'--debug': boolean;
@@ -69,26 +67,38 @@ export default async function inspect(
}
output.debug(`Fetching domain info`);
const cancelWait = wait(
`Fetching Domain ${domainName} under ${chalk.bold(contextName)}`
);
const information = await fetchInformation({
output,
client,
contextName,
domainName,
cancelWait,
}).finally(() => {
cancelWait();
});
if (typeof information === 'number') {
return information;
const [domain, renewalPrice] = await Promise.all([
getDomainByName(client, contextName, domainName),
getDomainPrice(client, domainName, 'renewal')
.then(res => (res instanceof Error ? null : res.price))
.catch(() => null),
]);
if (!domain || domain instanceof DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
return 1;
}
const { domain, projects, renewalPrice, domainConfig } = information;
if (domain instanceof DomainPermissionDenied) {
output.error(
`You don't have access to the domain ${domainName} under ${chalk.bold(
contextName
)}`
);
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
return 1;
}
const projects = await findProjectsForDomain(client, domainName);
if (projects instanceof Error) {
output.prettyError(projects);
return 1;
}
const domainConfig = await getDomainConfig(client, contextName, domainName);
output.log(
`Domain ${domainName} found under ${chalk.bold(contextName)} ${chalk.gray(
@@ -98,26 +108,46 @@ export default async function inspect(
output.print('\n');
output.print(chalk.bold(' General\n\n'));
output.print(` ${chalk.cyan('Name')}\t\t\t${domain.name}\n`);
output.print(` ${chalk.cyan('Service Type')}\t\t${domain.serviceType}\n`);
output.print(
` ${chalk.cyan('Registrar')}\t\t\t${getDomainRegistrar(domain)}\n`
` ${chalk.cyan('Ordered At')}\t\t\t${formatDate(domain.orderedAt)}\n`
);
output.print(
` ${chalk.cyan('Expiration Date')}\t\t${formatDate(domain.expiresAt)}\n`
);
output.print(
` ${chalk.cyan('Creator')}\t\t\t${domain.creator.username}\n`
` ${chalk.cyan('Transfer Started At')}\t\t${formatDate(
domain.transferStartedAt
)}\n`
);
output.print(
` ${chalk.cyan('Created At')}\t\t\t${formatDate(domain.createdAt)}\n`
);
output.print(` ${chalk.cyan('Edge Network')}\t\tyes\n`);
output.print(
` ${chalk.cyan('Bought At')}\t\t\t${formatDate(domain.boughtAt)}\n`
);
output.print(
` ${chalk.cyan('Transferred At')}\t\t${formatDate(
domain.transferredAt
)}\n`
);
output.print(
` ${chalk.cyan('Expires At')}\t\t\t${formatDate(domain.expiresAt)}\n`
);
output.print(
` ${chalk.cyan('NS Verified At')}\t\t${formatDate(
domain.nsVerifiedAt
)}\n`
);
output.print(
` ${chalk.cyan('TXT Verified At')}\t\t${formatDate(
domain.txtVerifiedAt
)}\n`
);
if (renewalPrice && domain.boughtAt) {
output.print(
` ${chalk.cyan('Renewal Price')}\t\t$${renewalPrice} USD\n`
);
}
output.print(` ${chalk.cyan('CDN Enabled')}\t\t\t${true}\n`);
output.print('\n');
output.print(chalk.bold(' Nameservers\n\n'));
@@ -128,6 +158,26 @@ export default async function inspect(
);
output.print('\n');
if (domainConfig.misconfigured) {
output.warn(
`This domain is not configured properly. To configure it you should either:`
);
output.print(
` ${chalk.grey('a)')} ` +
`Set the following record on your DNS provider to continue: ` +
`${code(`A ${domainName} 76.76.21.21`)} ` +
`${chalk.grey('[recommended]')}\n`
);
output.print(
` ${chalk.grey('b)')} ` +
`Change your domain nameservers to the intended set detailed above.\n\n`
);
output.print(
` We will run a verification for you and you will receive an email upon completion.\n`
);
output.print(' Read more: https://vercel.link/domain-configuration\n\n');
}
if (Array.isArray(projects) && projects.length > 0) {
output.print(chalk.bold(' Projects\n'));
@@ -158,109 +208,8 @@ export default async function inspect(
.join('\n')
);
output.print('\n');
}
if (domainConfig.misconfigured) {
output.warn(
`This Domain is not configured properly. To configure it you should either:`,
null,
null,
null,
{
boxen: {
margin: {
left: 2,
right: 0,
bottom: 0,
top: 0,
},
},
}
);
output.print(
` ${chalk.grey('a)')} ` +
`Set the following record on your DNS provider to continue: ` +
`${code(`A ${domainName} 76.76.21.21`)} ` +
`${chalk.grey('[recommended]')}\n`
);
output.print(
` ${chalk.grey('b)')} ` +
`Change your Domains's nameservers to the intended set detailed above.\n\n`
);
output.print(
` We will run a verification for you and you will receive an email upon completion.\n`
);
const contextNameConst = contextName;
const projectNames = Array.from(
new Set(projects.map(project => project.name))
);
if (projectNames.length) {
projectNames.forEach((name, index) => {
const prefix = index === 0 ? ' Read more:' : ' '.repeat(12);
output.print(
`${prefix} https://vercel.com/${contextNameConst}/${name}/settings/domains\n`
);
});
} else {
output.print(` Read more: https://vercel.link/domain-configuration\n`);
}
output.print('\n');
output.print('\n\n');
}
return null;
}
async function fetchInformation({
output,
client,
contextName,
domainName,
cancelWait,
}: {
output: Output;
client: Client;
contextName: string;
domainName: string;
cancelWait: () => void;
}) {
const [domain, renewalPrice] = await Promise.all([
getDomainByName(client, contextName, domainName, { ignoreWait: true }),
getDomainPrice(client, domainName, 'renewal')
.then(res => (res instanceof Error ? null : res.price))
.catch(() => null),
]);
if (domain instanceof DomainNotFound) {
cancelWait();
output.prettyError(domain);
return 1;
}
if (domain instanceof DomainPermissionDenied) {
cancelWait();
output.prettyError(domain);
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
return 1;
}
const projects = await findProjectsForDomain(client, domainName);
if (projects instanceof Error) {
cancelWait();
output.prettyError(projects);
return 1;
}
const domainConfig = await getDomainConfig(client, domainName);
return {
domain,
projects,
renewalPrice,
domainConfig,
};
}

View File

@@ -1,8 +1,8 @@
import ms from 'ms';
import psl from 'psl';
import chalk from 'chalk';
import plural from 'pluralize';
import wait from '../../util/output/wait';
import Client from '../../util/client';
import getDomains from '../../util/domains/get-domains';
import getScope from '../../util/get-scope';
@@ -10,17 +10,28 @@ import stamp from '../../util/output/stamp';
import { Output } from '../../util/output';
import formatTable from '../../util/format-table';
import { formatDateWithoutTime } from '../../util/format-date';
import { Domain, NowContext } from '../../types';
import { Domain, Project, NowContext } from '../../types';
import { getProjectsWithDomains } from '../../util/projects/get-projects-with-domains';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import isDomainExternal from '../../util/domains/is-domain-external';
import { getDomainRegistrar } from '../../util/domains/get-domain-registrar';
import { isPublicSuffix } from '../../util/domains/is-public-suffix';
type Options = {
'--debug': boolean;
'--next': number;
};
interface DomainInfo {
domain: string;
apexDomain: string;
projectName: string | null;
dns: 'Vercel' | 'External';
configured: boolean;
expiresAt: number | null;
createdAt: number | null;
}
export default async function ls(
ctx: NowContext,
opts: Options,
@@ -64,21 +75,29 @@ export default async function ls(
return 1;
}
const cancelWait = wait(`Fetching Domains under ${chalk.bold(contextName)}`);
const [{ domains, pagination }, projects] = await Promise.all([
getDomains(client, contextName),
getProjectsWithDomains(client),
] as const);
const { domains, pagination } = await getDomains(client).finally(() => {
cancelWait();
});
if (projects instanceof Error) {
output.prettyError(projects);
return 1;
}
const domainsInfo = createDomainsInfo(domains, projects);
output.log(
`${plural('Domain', domains.length, true)} found under ${chalk.bold(
contextName
)} ${chalk.gray(lsStamp())}`
`${plural(
'project domain',
domainsInfo.length,
true
)} found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}`
);
if (domains.length > 0) {
if (domainsInfo.length > 0) {
output.print(
formatDomainsTable(domains).replace(/^(.*)/gm, `${' '.repeat(1)}$1`)
formatDomainsTable(domainsInfo).replace(/^(.*)/gm, `${' '.repeat(3)}$1`)
);
output.print('\n\n');
}
@@ -86,7 +105,7 @@ export default async function ls(
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page, run ${getCommandName(
`To display the next page run ${getCommandName(
`domains ls${flags} --next ${pagination.next}`
)}`
);
@@ -95,26 +114,92 @@ export default async function ls(
return 0;
}
function formatDomainsTable(domains: Domain[]) {
function createDomainsInfo(domains: Domain[], projects: Project[]) {
const info = new Map<string, DomainInfo>();
domains.forEach(domain => {
info.set(domain.name, {
domain: domain.name,
apexDomain: domain.name,
projectName: null,
expiresAt: domain.expiresAt || null,
createdAt: domain.createdAt,
configured: Boolean(domain.verified),
dns: isDomainExternal(domain) ? 'External' : 'Vercel',
});
projects.forEach(project => {
(project.alias || []).forEach(target => {
if (!target.domain.endsWith(domain.name)) return;
info.set(target.domain, {
domain: target.domain,
apexDomain: domain.name,
projectName: project.name,
expiresAt: domain.expiresAt || null,
createdAt: domain.createdAt || target.createdAt || null,
configured: Boolean(domain.verified),
dns: isDomainExternal(domain) ? 'External' : 'Vercel',
});
});
});
});
projects.forEach(project => {
(project.alias || []).forEach(target => {
if (info.has(target.domain)) return;
const { domain: apexDomain } = psl.parse(
target.domain
) as psl.ParsedDomain;
info.set(target.domain, {
domain: target.domain,
apexDomain: apexDomain || target.domain,
projectName: project.name,
expiresAt: null,
createdAt: target.createdAt || null,
configured: isPublicSuffix(target.domain),
dns: isPublicSuffix(target.domain) ? 'Vercel' : 'External',
});
});
});
const list = Array.from(info.values());
return list.sort((a, b) => {
if (a.apexDomain === b.apexDomain) {
if (a.apexDomain === a.domain) return -1;
if (b.apexDomain === b.domain) return 1;
return a.domain.localeCompare(b.domain);
}
return a.apexDomain.localeCompare(b.apexDomain);
});
}
function formatDomainsTable(domainsInfo: DomainInfo[]) {
const current = Date.now();
const rows: string[][] = domains.map(domain => {
const expiration = formatDateWithoutTime(domain.expiresAt);
const age = domain.createdAt ? ms(current - domain.createdAt) : '-';
const rows: string[][] = domainsInfo.map(info => {
const expiration = formatDateWithoutTime(info.expiresAt);
const age = info.createdAt ? ms(current - info.createdAt) : '-';
return [
domain.name,
getDomainRegistrar(domain),
isDomainExternal(domain) ? 'Third Party' : 'Vercel',
info.domain,
info.projectName || '-',
info.dns,
expiration,
domain.creator.username,
info.configured.toString(),
chalk.gray(age),
];
});
return formatTable(
['Domain', 'Registrar', 'Nameservers', 'Expiration Date', 'Creator', 'Age'],
const table = formatTable(
['domain', 'project', 'dns', 'expiration', 'configured', 'age'],
['l', 'l', 'l', 'l', 'l', 'l'],
[{ rows }]
);
return table;
}

View File

@@ -71,8 +71,8 @@ export default async function transferIn(
checkTransfer(client, domainName),
]);
if (domainPrice instanceof Error) {
output.prettyError(domainPrice);
if (domainPrice instanceof ERRORS.UnsupportedTLD) {
output.error(`The TLD for ${param(domainName)} is not supported.`);
return 1;
}

View File

@@ -1,33 +0,0 @@
import { NowContext } from '../../types';
import { Output } from '../../util/output';
import { NowBuildError } from '@vercel/build-utils';
import { getCommandName } from '../../util/pkg-name';
export default async function verify(
_ctx: NowContext,
_opts: {},
args: string[],
output: Output
) {
const [domainName] = args;
if (!domainName) {
output.error(
`${getCommandName(`domains verify <domain>`)} expects one argument`
);
return 1;
}
const error = new NowBuildError({
code: 'domains_verify_command_deprecated',
message: `It's not necessary to verify Domains anymore. Instead, you can run ${getCommandName(
`domains inspect ${domainName}`
)} to see what you need to do in order to configure it properly.`,
link: 'https://vercel.link/domain-verification-via-cli',
});
output.prettyError(error);
return 0;
}

View File

@@ -6,6 +6,8 @@ export default new Map([
['cert', 'certs'],
['certs', 'certs'],
['deploy', 'deploy'],
['deploy-v1', 'deploy'],
['deploy-v2', 'deploy'],
['dev', 'dev'],
['dns', 'dns'],
['domain', 'domains'],
@@ -27,6 +29,7 @@ export default new Map([
['projects', 'projects'],
['remove', 'remove'],
['rm', 'remove'],
['scale', 'scale'],
['secret', 'secrets'],
['secrets', 'secrets'],
['switch', 'teams'],

View File

@@ -1,4 +1,5 @@
import chalk from 'chalk';
import table from 'text-table';
import getArgs from '../util/get-args';
import buildsList from '../util/output/builds';
import routesList from '../util/output/routes';
@@ -8,10 +9,13 @@ import Now from '../util';
import logo from '../util/output/logo';
import elapsed from '../util/output/elapsed.ts';
import { handleError } from '../util/error';
import strlen from '../util/strlen.ts';
import Client from '../util/client.ts';
import getScope from '../util/get-scope.ts';
import { getPkgName, getCommandName } from '../util/pkg-name.ts';
const STATIC = 'STATIC';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} inspect`)} <url>
@@ -44,6 +48,7 @@ const help = () => {
};
export default async function main(ctx) {
let id;
let deployment;
let argv;
@@ -65,7 +70,7 @@ export default async function main(ctx) {
const { print, log, error } = output;
// extract the first parameter
const [, deploymentIdOrHost] = argv._;
id = argv._[1];
if (argv._.length !== 2) {
error(`${getCommandName('inspect <url>')} expects exactly one argument`);
@@ -102,24 +107,20 @@ export default async function main(ctx) {
// resolve the deployment, since we might have been given an alias
const depFetchStart = Date.now();
const cancelWait = output.spinner(
`Fetching deployment "${deploymentIdOrHost}" in ${chalk.bold(contextName)}`
`Fetching deployment "${id}" in ${chalk.bold(contextName)}`
);
try {
deployment = await now.findDeployment(deploymentIdOrHost);
deployment = await now.findDeployment(id);
} catch (err) {
cancelWait();
if (err.status === 404) {
error(
`Failed to find deployment "${deploymentIdOrHost}" in ${chalk.bold(
contextName
)}`
);
error(`Failed to find deployment "${id}" in ${chalk.bold(contextName)}`);
return 1;
}
if (err.status === 403) {
error(
`No permission to access deployment "${deploymentIdOrHost}" in ${chalk.bold(
`No permission to access deployment "${id}" in ${chalk.bold(
contextName
)}`
);
@@ -129,12 +130,35 @@ export default async function main(ctx) {
throw err;
}
const { id, name, url, created, routes, readyState } = deployment;
const {
id: finalId,
name,
state,
type,
slot,
sessionAffinity,
url,
created,
limits,
version,
routes,
readyState,
} = deployment;
const { builds } =
deployment.version === 2
? await now.fetch(`/v1/now/deployments/${id}/builds`)
: { builds: [] };
const isBuilds = version === 2;
const buildsUrl = `/v1/now/deployments/${finalId}/builds`;
const [scale, events, { builds }] = await Promise.all([
caught(
now.fetch(`/v3/now/deployments/${encodeURIComponent(finalId)}/instances`)
),
type === STATIC
? null
: caught(
now.fetch(`/v1/now/deployments/${encodeURIComponent(finalId)}/events`)
),
isBuilds ? now.fetch(buildsUrl) : { builds: [] },
]);
cancelWait();
log(
@@ -145,9 +169,21 @@ export default async function main(ctx) {
print('\n');
print(chalk.bold(' General\n\n'));
print(` ${chalk.cyan('id')}\t\t${id}\n`);
print(` ${chalk.cyan('version')}\t${version}\n`);
print(` ${chalk.cyan('id')}\t\t${finalId}\n`);
print(` ${chalk.cyan('name')}\t${name}\n`);
print(` ${chalk.cyan('readyState')}\t${stateString(readyState)}\n`);
print(
` ${chalk.cyan('readyState')}\t${stateString(state || readyState)}\n`
);
if (!isBuilds) {
print(` ${chalk.cyan('type')}\t${type}\n`);
}
if (slot) {
print(` ${chalk.cyan('slot')}\t${slot}\n`);
}
if (sessionAffinity) {
print(` ${chalk.cyan('affinity')}\t${sessionAffinity}\n`);
}
print(` ${chalk.cyan('url')}\t\t${url}\n`);
if (created) {
print(
@@ -178,7 +214,95 @@ export default async function main(ctx) {
print(`\n\n`);
}
return 0;
if (limits) {
print(chalk.bold(' Limits\n\n'));
print(
` ${chalk.dim('duration')}\t\t${limits.duration} ${elapsed(
limits.duration
)}\n`
);
print(
` ${chalk.dim('maxConcurrentReqs')}\t${limits.maxConcurrentReqs}\n`
);
print(
` ${chalk.dim('timeout')}\t\t${limits.timeout} ${elapsed(
limits.timeout
)}\n`
);
print('\n');
}
if (type === STATIC || isBuilds) {
return 0;
}
print(chalk.bold(' Scale\n\n'));
let exitCode = 0;
if (scale instanceof Error) {
error(`Scale information unavailable: ${scale}`);
exitCode = 1;
} else {
const dcs = Object.keys(scale);
const t = [['dc', 'min', 'max', 'current'].map(v => chalk.gray(v))];
for (const dc of dcs) {
const { instances } = scale[dc];
const cfg = deployment.scale[dc] || {};
t.push([dc, cfg.min || 0, cfg.max || 0, instances.length]);
}
print(
`${table(t, {
align: ['l', 'c', 'c', 'c'],
hsep: ' '.repeat(8),
stringLength: strlen,
}).replace(/^(.*)/gm, ' $1')}\n`
);
print('\n');
}
print(chalk.bold(' Events\n\n'));
if (events instanceof Error) {
error(`Events unavailable: ${scale}`);
exitCode = 1;
} else if (events) {
events.forEach(data => {
if (!data.event) return; // keepalive
print(
` ${chalk.gray(new Date(data.created).toISOString())} ${
data.event
} ${getEventMetadata(data)}\n`
);
});
print('\n');
}
return exitCode;
}
// gets the metadata that should be printed next to
// each event
function getEventMetadata({ event, payload }) {
if (event === 'state') {
return chalk.bold(payload.value);
}
if (event === 'instance-start' || event === 'instance-stop') {
if (payload.dc != null) {
return chalk.green(`(${payload.dc})`);
}
}
return '';
}
// makes sure the promise never rejects, exposing the error
// as the resolved value instead
function caught(p) {
return new Promise(r => {
p.then(r).catch(r);
});
}
// renders the state string
@@ -194,6 +318,6 @@ function stateString(s) {
return s;
default:
return chalk.gray(s || 'UNKNOWN');
return chalk.gray('UNKNOWN');
}
}

View File

@@ -2,7 +2,9 @@ import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import Now from '../util';
import getAliases from '../util/alias/get-aliases';
import getArgs from '../util/get-args';
import getDeploymentInstances from '../util/deploy/get-deployment-instances';
import createOutput from '../util/output';
import { handleError } from '../util/error';
import cmd from '../util/output/cmd.ts';
@@ -36,6 +38,7 @@ const help = () => {
'TOKEN'
)} Login token
-S, --scope Set a custom scope
-a, --all See all instances for each deployment (requires [app])
-m, --meta Filter deployments by metadata (e.g.: ${chalk.dim(
'`-m KEY=value`'
)}). Can appear many times.
@@ -69,12 +72,16 @@ const help = () => {
`);
};
// Options
// $FlowFixMe
export default async function main(ctx) {
let argv;
try {
argv = getArgs(ctx.argv.slice(2), {
'--all': Boolean,
'--meta': [String],
'-a': '--all',
'-m': '--meta',
'--next': Number,
'-N': '--next',
@@ -144,6 +151,11 @@ export default async function main(ctx) {
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam });
const start = new Date();
if (argv['--all'] && !app) {
error('You must define an app when using `-a` / `--all`');
return 1;
}
if (app && !isValidName(app)) {
error(`The provided argument "${app}" is not a valid project name`);
return 1;
@@ -210,8 +222,51 @@ export default async function main(ctx) {
}
}
if (app && !deployments.length) {
debug(
'No deployments: attempting to find aliases that matches supplied app name'
);
const { aliases } = await getAliases(now);
const item = aliases.find(e => e.uid === app || e.alias === app);
if (item) {
debug(`Found alias that matches app name: ${item.alias}`);
if (Array.isArray(item.rules)) {
now.close();
stopSpinner();
log(`Found matching path alias: ${chalk.cyan(item.alias)}`);
log(`Please run ${getCommandName(`alias ls ${item.alias}`)} instead`);
return 0;
}
const match = await now.findDeployment(item.deploymentId);
const instances = await getDeploymentInstances(
now,
item.deploymentId,
'now_cli_alias_instances'
);
match.instanceCount = Object.keys(instances).reduce(
(count, dc) => count + instances[dc].instances.length,
0
);
if (match !== null && typeof match !== 'undefined') {
deployments = Array.of(match);
}
}
}
now.close();
if (argv['--all']) {
await Promise.all(
deployments.map(async ({ uid, instanceCount }, i) => {
deployments[i].instances =
instanceCount > 0 ? await now.listInstances(uid) : [];
})
);
}
if (host) {
deployments = deployments.filter(deployment => deployment.url === host);
}
@@ -235,6 +290,12 @@ export default async function main(ctx) {
`${getCommandName('ls [project]')}`
)}`
);
} else if (!argv['--all']) {
log(
`To list deployment instances run ${cmd(
`${getCommandName('ls --all [project]')}`
)}`
);
}
print('\n');
@@ -255,6 +316,15 @@ export default async function main(ctx) {
chalk.gray(ms(Date.now() - new Date(dep.createdAt))),
dep.creator.username,
],
...(argv['--all']
? dep.instances.map(i => [
'',
` ${chalk.gray('-')} ${i.url} `,
'',
'',
'',
])
: []),
])
// flatten since the previous step returns a nested
// array of the deployment and (optionally) its instances

View File

@@ -0,0 +1,341 @@
import ms from 'ms';
import chalk from 'chalk';
import cmd from '../util/output/cmd.ts';
import createOutput from '../util/output';
import logo from '../util/output/logo';
import stamp from '../util/output/stamp.ts';
import Now from '../util';
import getArgs from '../util/get-args';
import Client from '../util/client.ts';
import getScope from '../util/get-scope.ts';
import getDCsFromArgs from '../util/scale/get-dcs-from-args';
import getDeploymentByIdOrHost from '../util/deploy/get-deployment-by-id-or-host';
import getDeploymentByIdOrThrow from '../util/deploy/get-deployment-by-id-or-throw';
import getMaxFromArgs from '../util/scale/get-max-from-args';
import getMinFromArgs from '../util/scale/get-min-from-args';
import patchDeploymentScale from '../util/scale/patch-deployment-scale';
import waitVerifyDeploymentScale from '../util/scale/wait-verify-deployment-scale';
import { handleError } from '../util/error';
import {
VerifyScaleTimeout,
DeploymentTypeUnsupported,
} from '../util/errors-ts';
import {
DeploymentNotFound,
DeploymentPermissionDenied,
ForbiddenScaleMaxInstances,
ForbiddenScaleMinInstances,
InvalidArgsForMinMaxScale,
InvalidMaxForScale,
InvalidMinForScale,
InvalidScaleMinMaxRelation,
NotSupportedMinScaleSlots,
} from '../util/errors-ts';
import { InvalidAllForScale, InvalidRegionOrDCForScale } from '../util/errors';
import handleCertError from '../util/certs/handle-cert-error';
import { getPkgName } from '../util/pkg-name.ts';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} scale`)} <url> <dc> [min] [max]
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-d, --debug Debug mode [off]
-S, --scope Set a custom scope
-n, --no-verify Skip step of waiting until instance count meets given constraints
-t, --verify-timeout How long to wait for verification to complete [5m]
${chalk.dim('Examples:')}
${chalk.gray(
''
)} Enable your deployment in all datacenters (min: 0, max: auto)
${chalk.cyan(`$ ${getPkgName()} scale my-deployment-123.now.sh all`)}
${chalk.gray(
'-'
)} Enable your deployment in the SFO datacenter (min: 0, max: auto)
${chalk.cyan(`$ ${getPkgName()} scale my-deployment-123.now.sh sfo`)}
${chalk.gray(
''
)} Scale a deployment in all datacenters to 3 instances at all times (no sleep)
${chalk.cyan(`$ ${getPkgName()} scale my-deployment-123.now.sh all 3`)}
${chalk.gray(
''
)} Enable your deployment in all datacenters, with auto-scaling
${chalk.cyan(`$ ${getPkgName()} scale my-deployment-123.now.sh all auto`)}
`);
};
export default async function main(ctx) {
let argv;
try {
argv = getArgs(ctx.argv.slice(2), {
'--verify-timeout': Number,
'--no-verify': Boolean,
'-n': '--no-verify',
});
} catch (err) {
handleError(err);
return 1;
}
if (argv['--help']) {
help();
return 2;
}
// Prepare the context
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = argv['--debug'];
// $FlowFixMe
const now = new Now({ apiUrl, token, debug, currentTeam });
const output = createOutput({ debug });
const client = new Client({ apiUrl, token, currentTeam, debug });
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;
}
// Fail if the user is providing an old command
if (argv._[1] === 'ls') {
output.error(
`${cmd(`${getPkgName()} scale ls`)} has been deprecated. Use ${cmd(
'now ls'
)} and ${cmd(`${getPkgName()} inspect <url>`)}`
);
now.close();
return 1;
}
// Ensure the number of arguments is between the allower range
if (argv._.length < 3 || argv._.length > 5) {
output.error(
`${cmd(
`${getPkgName()} scale <url> <dc> [min] [max]`
)} expects at least two arguments`
);
help();
now.close();
return 1;
}
const dcs = getDCsFromArgs(argv._);
if (dcs instanceof InvalidAllForScale) {
output.error(
'The region value "all" was used, but it cannot be used alongside other region or dc identifiers'
);
now.close();
return 1;
}
if (dcs instanceof InvalidRegionOrDCForScale) {
output.error(
`The value "${dcs.meta.regionOrDC}" is not a valid region or DC identifier`
);
now.close();
return 1;
}
const min = getMinFromArgs(argv._);
if (min instanceof InvalidMinForScale) {
output.error(
`Invalid <min> parameter "${min.meta.value}". A number or "auto" were expected`
);
now.close();
return 1;
}
const max = getMaxFromArgs(argv._);
if (max instanceof InvalidMinForScale) {
output.error(
`Invalid <min> parameter "${max.meta.value}". A number or "auto" were expected`
);
now.close();
return 1;
}
if (max instanceof InvalidArgsForMinMaxScale) {
output.error(
`Invalid number of arguments: expected <min> ("${max.meta.min}") and [max]`
);
now.close();
return 1;
}
if (max instanceof InvalidMaxForScale) {
output.error(
`Invalid <max> parameter "${max.meta.value}". A number or "auto" were expected`
);
now.close();
return 1;
}
// Fetch the deployment
const deploymentStamp = stamp();
const deployment = handleCertError(
output,
await getDeploymentByIdOrHost(now, contextName, argv._[1])
);
if (deployment === 1) {
return deployment;
}
if (deployment instanceof DeploymentPermissionDenied) {
output.error(
`No permission to access deployment ${chalk.dim(
deployment.meta.id
)} under ${chalk.bold(deployment.meta.context)}`
);
now.close();
return 1;
}
if (deployment instanceof DeploymentNotFound) {
output.error(
`Failed to find deployment "${argv._[1]}" in ${chalk.bold(contextName)}`
);
now.close();
return 1;
}
output.log(`Fetched deployment "${deployment.url}" ${deploymentStamp()}`);
// Make sure the deployment can be scaled
if (deployment.type === 'STATIC') {
output.error('Scaling rules cannot be set on static deployments');
now.close();
return 1;
}
if (deployment.state === 'ERROR') {
output.error('Cannot scale a deployment in the ERROR state');
now.close();
return 1;
}
if (deployment.version === 2) {
output.error('Cannot scale a Now 2.0 deployment');
now.close();
return 1;
}
const scaleArgs = dcs.reduce(
(result, dc) => ({ ...result, [dc]: { min, max } }),
{}
);
output.debug(
`Setting scale deployment presets to ${JSON.stringify(scaleArgs)}`
);
// Set the deployment scale
const scaleStamp = stamp();
const result = await patchDeploymentScale(
output,
now,
deployment.uid,
scaleArgs,
deployment.url
);
if (result instanceof ForbiddenScaleMinInstances) {
output.error(
`You can't scale to more than ${result.meta.max} min instances with your current plan.`
);
now.close();
return 1;
}
if (result instanceof ForbiddenScaleMaxInstances) {
output.error(
`You can't scale to more than ${result.meta.max} max instances with your current plan.`
);
now.close();
return 1;
}
if (result instanceof InvalidScaleMinMaxRelation) {
output.error(`Min number of instances can't be higher than max.`);
now.close();
return 1;
}
if (result instanceof NotSupportedMinScaleSlots) {
output.error(
`Cloud v2 does not yet support setting a non-zero min number of instances.`
);
output.log('Read more: https://err.sh/now/v2-no-min');
now.close();
return 1;
}
if (result instanceof DeploymentTypeUnsupported) {
output.error(`This region only accepts Serverless Docker Deployments.`);
now.close();
return 1;
}
console.log(
`${chalk.gray('>')} Scale rules for ${dcs
.map(d => chalk.bold(d))
.join(', ')} (min: ${chalk.bold(min)}, max: ${chalk.bold(
max
)}) saved ${scaleStamp()}`
);
if (argv['--no-verify']) {
now.close();
return 0;
}
// Verify that the scale presets are there
const verifyStamp = stamp();
const updatedDeployment = await getDeploymentByIdOrThrow(
now,
contextName,
deployment.uid
);
if (updatedDeployment.type === 'NPM' || updatedDeployment.type === 'DOCKER') {
const result = await waitVerifyDeploymentScale(
output,
now,
deployment.uid,
updatedDeployment.scale
);
if (result instanceof VerifyScaleTimeout) {
output.error(
`Instance verification timed out (${ms(result.meta.timeout)})`,
'verification-timeout'
);
now.close();
return 1;
}
output.success(`Scale state verified ${verifyStamp()}`);
}
now.close();
return 0;
}

View File

@@ -1,6 +1,7 @@
import chalk from 'chalk';
import { email as regexEmail } from '../../util/input/regexes';
import wait from '../../util/output/wait';
import fatalError from '../../util/fatal-error';
import cmd from '../../util/output/cmd.ts';
import info from '../../util/output/info';
import stamp from '../../util/output/stamp.ts';
@@ -14,7 +15,6 @@ import success from '../../util/output/success';
import getUser from '../../util/get-user.ts';
import Client from '../../util/client.ts';
import { getCommandName } from '../../util/pkg-name.ts';
import createOutput from '../../util/output';
const validateEmail = data => regexEmail.test(data.trim()) || data.length === 0;
@@ -57,7 +57,7 @@ const emailAutoComplete = (value, teamSlug) => {
return false;
};
export default async function ({
export default async function({
teams,
args,
config,
@@ -66,7 +66,6 @@ export default async function ({
apiUrl,
token,
} = {}) {
const output = createOutput();
const { currentTeam: currentTeamId } = config;
const stopSpinner = wait('Fetching teams');
@@ -101,8 +100,7 @@ export default async function ({
)}.\nPlease select a team scope using ${getCommandName(
`switch`
)} or use ${cmd('--scope')}`;
output.error(err);
return 1;
return fatalError(err);
}
console.log(

View File

@@ -640,11 +640,6 @@ const main = async argv_ => {
return 1;
}
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.prettyError(err);
return 1;
}
if (err instanceof APIError && 400 <= err.status && err.status <= 499) {
err.message = err.serverMessage;
output.prettyError(err);

View File

@@ -30,6 +30,7 @@ export type User = {
bio?: string;
date: number;
email: string;
platformVersion: number;
username: string;
website?: string;
billingChecked: boolean;
@@ -55,6 +56,7 @@ export type Team = {
creatorId: string;
membership: { uid: string; role: 'MEMBER' | 'OWNER'; created: number };
name: string;
platformVersion: number;
slug: string;
};
@@ -100,23 +102,59 @@ export type Cert = {
expiration: string;
};
export type Deployment = {
export type DeploymentScale = {
[dc: string]: {
min: number;
max: number;
};
};
export type NpmDeployment = {
uid: string;
url: string;
name: string;
type: 'LAMBDAS';
state:
| 'BUILDING'
| 'ERROR'
| 'INITIALIZING'
| 'QUEUED'
| 'READY'
| 'CANCELED';
type: 'NPM';
state: 'INITIALIZING' | 'FROZEN' | 'READY' | 'ERROR';
version?: number;
created: number;
creator: { uid: string };
sessionAffinity: string;
scale: DeploymentScale;
};
export type StaticDeployment = {
uid: string;
url: string;
name: string;
type: 'STATIC';
state: 'INITIALIZING' | 'FROZEN' | 'READY' | 'ERROR';
version?: number;
created: number;
creator: { uid: string };
sessionAffinity: string;
};
export type DockerDeployment = {
uid: string;
url: string;
name: string;
type: 'DOCKER';
state: 'INITIALIZING' | 'FROZEN' | 'READY' | 'ERROR';
version?: number;
created: number;
creator: { uid: string };
sessionAffinity: string;
scale: DeploymentScale;
limits?: {
maxConcurrentReqs: number;
timeout: number;
duration: number;
};
slot?: string;
};
export type Deployment = NpmDeployment | StaticDeployment | DockerDeployment;
type PathAliasRule = {
pathname: string;
method: Array<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>;

View File

@@ -1,20 +1,89 @@
import { Deployment } from '../../types';
import { Output } from '../output';
import * as ERRORS from '../errors-ts';
import Client from '../client';
import createAlias from './create-alias';
import deploymentShouldCopyScale from './deployment-should-copy-scale';
import deploymentShouldDownscale from './deployment-should-downscale';
import findAliasByAliasOrId from './find-alias-by-alias-or-id';
import getDeploymentDownscalePresets from './get-deployment-downscale-presets';
import getDeploymentFromAlias from './get-deployment-from-alias';
import isDomainExternal from '../domains/is-domain-external';
import setDeploymentScale from '../scale/set-deployment-scale';
import setupDomain from '../domains/setup-domain';
import stamp from '../output/stamp';
import waitForScale from '../scale/wait-verify-deployment-scale';
export default async function assignAlias(
output: Output,
client: Client,
deployment: Deployment,
alias: string,
contextName: string
contextName: string,
noVerify: boolean
) {
const prevAlias = await findAliasByAliasOrId(output, client, alias);
let externalDomain = false;
// Check if the alias is a custom domain, because then
// If there was a previous deployment, we should fetch it to scale and downscale later
let prevDeployment = await getDeploymentFromAlias(
client,
contextName,
prevAlias,
deployment
);
// If there is an alias laying around that points to a deleted
// deployment, we need to account for it here.
if (prevDeployment instanceof ERRORS.DeploymentNotFound) {
prevDeployment = null;
}
if (prevDeployment instanceof Error) {
return prevDeployment;
}
// If there was a prev deployment that wasn't static we have to check if we should scale
if (
prevDeployment !== null &&
prevDeployment.type !== 'STATIC' &&
deployment.type !== 'STATIC'
) {
if (deploymentShouldCopyScale(prevDeployment, deployment)) {
const scaleStamp = stamp();
const result = await setDeploymentScale(
output,
client,
deployment.uid,
prevDeployment.scale,
deployment.url
);
if (result instanceof Error) {
return result;
}
output.log(
`Scale rules copied from previous alias ${
prevDeployment.url
} ${scaleStamp()}`
);
if (!noVerify) {
const result = await waitForScale(
output,
client,
deployment.uid,
prevDeployment.scale
);
if (result instanceof ERRORS.VerifyScaleTimeout) {
return result;
}
}
} else {
output.debug(`Both deployments have the same scaling rules.`);
}
}
// Check if the alias is a custom domain and if case we have a positive
// we have to configure the DNS records and certificate
if (
alias.indexOf('.') !== -1 &&
@@ -40,6 +109,23 @@ export default async function assignAlias(
alias,
externalDomain
);
if (record instanceof Error) {
return record;
}
// Downscale if the previous deployment is not static and doesn't have the minimal presets
if (prevDeployment !== null && prevDeployment.type !== 'STATIC') {
if (await deploymentShouldDownscale(output, client, prevDeployment)) {
await setDeploymentScale(
output,
client,
prevDeployment.uid,
getDeploymentDownscalePresets(prevDeployment),
prevDeployment.url
);
output.log(`Previous deployment ${prevDeployment.url} downscaled`);
}
}
return record;
}

View File

@@ -0,0 +1,11 @@
import { Deployment } from '../../types';
import Client from '../client';
import getAliases from './get-aliases';
export default async function deploymentIsAliased(
client: Client,
deployment: Deployment
) {
const { aliases } = await getAliases(client);
return aliases.some(alias => alias.deploymentId === deployment.uid);
}

View File

@@ -0,0 +1,23 @@
import { Deployment } from '../../types';
import getScaleForDC from '../scale/get-scale-for-dc';
export default function shouldCopyScalingAttributes(
origin: Deployment,
dest: Deployment
) {
if (origin.version === 2 || dest.version === 2) {
return false;
}
return (
(origin.type !== 'STATIC' &&
getScaleForDC('bru1', origin).min !== getScaleForDC('bru1', dest).min) ||
getScaleForDC('bru1', origin).max !== getScaleForDC('bru1', dest).max ||
getScaleForDC('gru1', origin).min !== getScaleForDC('gru1', dest).min ||
getScaleForDC('gru1', origin).max !== getScaleForDC('gru1', dest).max ||
getScaleForDC('sfo1', origin).min !== getScaleForDC('sfo1', dest).min ||
getScaleForDC('sfo1', origin).max !== getScaleForDC('sfo1', dest).max ||
getScaleForDC('iad1', origin).min !== getScaleForDC('iad1', dest).min ||
getScaleForDC('iad1', origin).max !== getScaleForDC('iad1', dest).max
);
}

View File

@@ -0,0 +1,34 @@
import deploymentIsAliased from './deployment-is-aliased';
import { Output } from '../output';
import Client from '../client';
import getScaleForDC from '../scale/get-scale-for-dc';
import { Deployment } from '../../types';
export default async function deploymentShouldDownscale(
output: Output,
client: Client,
deployment: Deployment
) {
const isAliased = await deploymentIsAliased(client, deployment);
output.debug(`Previous deployment is aliased: ${isAliased.toString()}`);
if (
(deployment.type === 'DOCKER' && !!deployment.slot) ||
deployment.version === 2 ||
deployment.type === 'STATIC'
) {
// Don't downscale a previous slot or builds deployment
return false;
}
return (
!isAliased &&
Object.keys(deployment.scale).reduce(
(result, dc) =>
result ||
getScaleForDC(dc, deployment).min !== 0 ||
getScaleForDC(dc, deployment).max !== 1,
false
)
);
}

View File

@@ -0,0 +1,13 @@
import { DockerDeployment, NpmDeployment } from '../../types';
export default function getDeploymentDownscalePresets(
deployment: DockerDeployment | NpmDeployment
) {
return Object.keys(deployment.scale).reduce(
(result, dc) =>
Object.assign(result, {
[dc]: { min: 0, max: 1 },
}),
{}
);
}

View File

@@ -0,0 +1,51 @@
import chalk from 'chalk';
import getAppLastDeployment from '../deploy/get-app-last-deployment';
import getAppName from '../deploy/get-app-name';
import fetchDeploymentByIdOrHost from '../deploy/get-deployment-by-id-or-host';
import Client from '../client';
import { Output } from '../output';
import { User } from '../../types';
import { NowConfig } from '../dev/types';
export default async function getDeploymentForAlias(
client: Client,
output: Output,
args: string[],
localConfigPath: string | undefined,
user: User,
contextName: string,
localConfig: NowConfig
) {
const cancelWait = output.spinner(
`Fetching deployment to alias in ${chalk.bold(contextName)}`
);
// When there are no args at all we try to get the targets from the config
if (args.length === 2) {
const [deploymentId] = args;
const deployment = await fetchDeploymentByIdOrHost(
client,
contextName,
deploymentId
);
cancelWait();
return deployment;
}
const appName = await getAppName(output, localConfig, localConfigPath);
if (!appName) {
return null;
}
const deployment = await getAppLastDeployment(
output,
client,
appName,
user,
contextName
);
cancelWait();
return deployment;
}

View File

@@ -0,0 +1,16 @@
import Client from '../client';
import { Deployment, Alias } from '../../types';
import fetchDeploymentByIdOrHost from '../deploy/get-deployment-by-id-or-host';
export default async function fetchDeploymentFromAlias(
client: Client,
contextName: string,
prevAlias: Alias | null,
currentDeployment: Deployment
) {
return prevAlias &&
prevAlias.deploymentId &&
prevAlias.deploymentId !== currentDeployment.uid
? fetchDeploymentByIdOrHost(client, contextName, prevAlias.deploymentId)
: null;
}

View File

@@ -0,0 +1,37 @@
import path from 'path';
import * as ERRORS from '../errors-ts';
import humanizePath from '../humanize-path';
import readJSONFile from '../read-json-file';
import validatePathAliasRules from './validate-path-alias-rules';
import { PathRule } from '../../types';
export default async function getRulesFromFile(filePath: string) {
return typeof filePath === 'string' ? readRulesFile(filePath) : null;
}
async function readRulesFile(rulesPath: string) {
const fullPath = path.resolve(process.cwd(), rulesPath);
const result = (await readJSONFile(fullPath)) as { [key: string]: any };
if (result instanceof ERRORS.CantParseJSONFile) {
return result;
}
if (result === null) {
return new ERRORS.FileNotFound(fullPath);
}
if (!result.rules) {
return new ERRORS.RulesFileValidationError(
humanizePath(fullPath),
'Your rules file must include a rules field'
);
}
const rules = result.rules as PathRule[];
const error = validatePathAliasRules(humanizePath(fullPath), rules);
if (error instanceof ERRORS.RulesFileValidationError) {
return error;
}
return rules;
}

View File

@@ -0,0 +1,28 @@
import toHost from '../to-host';
import { NowConfig } from '../dev/types';
import * as ERRORS from '../errors-ts';
export function getTargetsForAlias(args: string[], { alias }: NowConfig) {
if (args.length) {
return targetsToHosts([args[args.length - 1]]);
}
if (!alias) {
return new ERRORS.NoAliasInConfig();
}
// Check the type for the option aliases
if (typeof alias !== 'string' && !Array.isArray(alias)) {
return new ERRORS.InvalidAliasInConfig(alias);
}
return typeof alias === 'string' ? [alias] : alias;
}
function targetsToHosts(targets: string[]) {
return targets.map(targetToHost).filter(item => item) as string[];
}
function targetToHost(target: string) {
return target.indexOf('.') !== -1 ? toHost(target) : target;
}

View File

@@ -0,0 +1,100 @@
import { Output } from '../output';
import { PathRule } from '../../types';
import * as ERRORS from '../errors-ts';
import Client from '../client';
import createCertForAlias from '../certs/create-cert-for-alias';
import setupDomain from '../domains/setup-domain';
type AliasRecord = {
uid: string;
alias: string;
created?: string;
oldDeploymentId?: string;
};
export default async function upsertPathAlias(
output: Output,
client: Client,
rules: PathRule[],
alias: string,
contextName: string
) {
let externalDomain = false;
if (!alias.endsWith('.now.sh') && !alias.endsWith('.vercel.app')) {
const domainInfo = await setupDomain(output, client, alias, contextName);
if (domainInfo instanceof Error) {
return domainInfo;
}
externalDomain = domainInfo.serviceType === 'external';
}
const result = await performUpsertPathAlias(
output,
client,
alias,
rules,
contextName
);
if (result instanceof ERRORS.CertMissing) {
const cert = await createCertForAlias(
output,
client,
contextName,
alias,
!externalDomain
);
if (cert instanceof Error) {
return cert;
}
return performUpsertPathAlias(output, client, alias, rules, contextName);
}
return result;
}
async function performUpsertPathAlias(
output: Output,
client: Client,
alias: string,
rules: PathRule[],
contextName: string
) {
const cancelMessage = output.spinner(
`Updating path alias rules for ${alias}`
);
try {
const record = await client.fetch<AliasRecord>(`/now/aliases`, {
body: { alias, rules },
method: 'POST',
});
cancelMessage();
return record;
} catch (error) {
cancelMessage();
if (error.code === 'cert_missing' || error.code === 'cert_expired') {
return new ERRORS.CertMissing(alias);
}
if (error.status === 409) {
return { uid: error.uid, alias: error.alias } as AliasRecord;
}
if (error.code === 'rule_validation_failed') {
return new ERRORS.RuleValidationFailed(error.serverMessage);
}
if (error.code === 'invalid_alias') {
return new ERRORS.InvalidAlias(alias);
}
if (error.status === 403) {
if (error.code === 'alias_in_use') {
console.log(error);
return new ERRORS.AliasInUse(alias);
}
if (error.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(alias, contextName);
}
}
throw error;
}
}

View File

@@ -0,0 +1,29 @@
import { RulesFileValidationError } from '../errors-ts';
import { PathRule } from '../../types';
export default function validatePathAliasRules(
location: string,
rules: PathRule[]
) {
if (!Array.isArray(rules)) {
return new RulesFileValidationError(location, 'rules must be an array');
}
if (rules.length === 0) {
return new RulesFileValidationError(location, 'empty rules');
}
for (const rule of rules) {
if (!(rule instanceof Object)) {
return new RulesFileValidationError(
location,
'all rules must be objects'
);
}
if (!rule.dest) {
return new RulesFileValidationError(
location,
'all rules must have a dest field'
);
}
}
}

View File

@@ -0,0 +1,8 @@
//
export default async function getCreditCards(now) {
const payload = await now.fetch('/stripe/sources/');
const cards = payload.sources;
return cards;
}

View File

@@ -6,7 +6,7 @@ export default async function deleteCertById(
client: Client,
id: string
) {
return client.fetch(`/v5/now/certs/${id}`, {
method: 'DELETE',
return client.fetch(`/v3/now/certs/${id}`, {
method: 'DELETE'
});
}

View File

@@ -4,7 +4,7 @@ import * as ERRORS from '../errors-ts';
export default async function getCertById(client: Client, id: string) {
try {
return await client.fetch<Cert>(`/v5/now/certs/${id}`);
return await client.fetch<Cert>(`/v3/now/certs/${id}`);
} catch (error) {
if (error.code === 'cert_not_found') {
return new ERRORS.CertNotFound(id);

View File

@@ -1,5 +1,6 @@
import { stringify } from 'querystring';
import { Cert } from '../../types';
import { Output } from '../output';
import * as ERRORS from '../errors-ts';
import Client from '../client';
@@ -7,14 +8,15 @@ type Response = {
certs: Cert[];
};
export async function getCustomCertsForDomain(
export default async function getCertsForDomain(
output: Output,
client: Client,
context: string,
domain: string
) {
try {
const { certs } = await client.fetch<Response>(
`/v5/now/certs?${stringify({ domain, custom: true })}`
`/v3/now/certs?${stringify({ domain })}`
);
return certs;
} catch (error) {

View File

@@ -0,0 +1,50 @@
// Native
import os from 'os';
import path from 'path';
const checkPath = async dir => {
if (!dir) {
return;
}
const home = os.homedir();
let location;
const paths = {
home,
desktop: path.join(home, 'Desktop'),
downloads: path.join(home, 'Downloads'),
};
for (const locationPath in paths) {
if (!{}.hasOwnProperty.call(paths, locationPath)) {
continue;
}
if (dir === paths[locationPath]) {
location = locationPath;
}
}
if (!location) {
return;
}
let locationName;
switch (location) {
case 'home':
locationName = 'user directory';
break;
case 'downloads':
locationName = 'downloads directory';
break;
default:
locationName = location;
}
throw new Error(`You're trying to deploy your ${locationName}.`);
};
export default checkPath;

View File

@@ -1,4 +1,4 @@
import { URLSearchParams } from 'url';
import qs from 'querystring';
import { EventEmitter } from 'events';
import { parse as parseUrl } from 'url';
import fetch, { RequestInit } from 'node-fetch';
@@ -7,7 +7,7 @@ import createOutput, { Output } from './output/create-output';
import responseError from './response-error';
import ua from './ua';
export interface FetchOptions {
export type FetchOptions = {
body?: NodeJS.ReadableStream | object | string;
headers?: { [key: string]: string };
json?: boolean;
@@ -15,7 +15,7 @@ export interface FetchOptions {
retry?: RetryOptions;
useCurrentTeam?: boolean;
accountId?: string;
}
};
export default class Client extends EventEmitter {
_apiUrl: string;
@@ -67,19 +67,19 @@ export default class Client extends EventEmitter {
: '';
if (opts.accountId || opts.useCurrentTeam !== false) {
const query = new URLSearchParams(parsedUrl.query);
const query = parsedUrl.query;
if (opts.accountId) {
if (opts.accountId.startsWith('team_')) {
query.set('teamId', opts.accountId);
query.teamId = opts.accountId;
} else {
query.delete('teamId');
delete query.teamId;
}
} else if (opts.useCurrentTeam !== false && this.currentTeam) {
query.set('teamId', this.currentTeam);
query.teamId = this.currentTeam;
}
_url = `${apiUrl}${parsedUrl.pathname}?${query}`;
_url = `${apiUrl}${parsedUrl.pathname}?${qs.stringify(query)}`;
delete opts.useCurrentTeam;
delete opts.accountId;

View File

@@ -0,0 +1,26 @@
// Combines two async generators into one that stops when all the generators
// passed are done.
export default async function* combineAsyncIterators(...args: any[]) {
let nextPromises = args.map(i => i.next());
while (nextPromises.length > 0) {
yield new Promise(resolve => {
let resolved = false;
nextPromises.forEach((nextPromise, idx) => {
nextPromise.then(({ value, done }: { value: any; done: boolean }) => {
if (!resolved) {
resolved = true;
resolve(value);
if (!done) {
nextPromises[idx] = args[idx].next();
} else {
nextPromises = [
...nextPromises.slice(0, idx),
...nextPromises.slice(idx + 1),
];
}
}
});
});
});
}
}

View File

@@ -38,45 +38,48 @@ export default class CreditCards extends Now {
return true;
}
async add(card) {
if (!card.expDate) {
throw new Error(`Please define an expiration date for your card`);
}
const expDateParts = card.expDate.split(' / ');
card = {
name: card.name,
number: card.cardNumber,
cvc: card.ccv,
};
card.exp_month = expDateParts[0];
card.exp_year = expDateParts[1];
try {
const token = (await stripe.tokens.create({ card })).id;
const res = await this._fetch('/stripe/sources/', {
method: 'POST',
body: {
source: token,
},
});
const { source, error } = await res.json();
if (source && source.id) {
return {
last4: source.last4,
};
} else if (error && error.message) {
throw new Error(error.message);
} else {
throw new Error('Unknown error');
add(card) {
return new Promise(async (resolve, reject) => {
if (!card.expDate) {
reject(new Error(`Please define a expiration date for your card`));
return;
}
} catch (err) {
throw new Error(err.message || 'Unknown error');
}
const expDateParts = card.expDate.split(' / ');
card = {
name: card.name,
number: card.cardNumber,
cvc: card.ccv,
};
card.exp_month = expDateParts[0];
card.exp_year = expDateParts[1];
try {
const token = (await stripe.tokens.create({ card })).id;
const res = await this._fetch('/stripe/sources/', {
method: 'POST',
body: {
source: token,
},
});
const { source, error } = await res.json();
if (source && source.id) {
resolve({
last4: source.last4,
});
} else if (error && error.message) {
reject(new Error(error.message));
} else {
reject(new Error('Unknown error'));
}
} catch (err) {
reject(new Error(err.message || 'Unknown error'));
}
});
}
}

View File

@@ -0,0 +1,26 @@
import getDeploymentByIdOrHost from './get-deployment-by-id-or-host';
import getDeploymentsByAppName from './get-deployments-by-appname';
import { Output } from '../output';
import Client from '../client';
import { User } from '../../types';
export default async function getAppLastDeployment(
output: Output,
client: Client,
appName: string,
user: User,
contextName: string
) {
output.debug(`Looking for deployments matching app ${appName}`);
const deployments = await getDeploymentsByAppName(client, appName);
const deploymentItem = deployments
.sort((a, b) => b.created - a.created)
.filter(dep => dep.state === 'READY' && dep.creator.uid === user.uid)[0];
// Try to fetch deployment details
if (deploymentItem) {
return getDeploymentByIdOrHost(client, contextName, deploymentItem.uid);
}
return null;
}

View File

@@ -0,0 +1,28 @@
import path from 'path';
import { NowError } from '../now-error';
import { Output } from '../output';
import { NowConfig } from '../dev/types';
import readPackage from '../read-package';
export default async function getAppName(
output: Output,
config: NowConfig,
localConfigPath?: string
) {
// If the name is in the configuration, return it
if (config.name) {
return config.name;
}
// Otherwise try to get it from the package
if (!config.type || config.type === 'npm') {
const pkg = await readPackage();
if (!(pkg instanceof NowError) && pkg) {
return pkg.name;
}
}
// Finally fallback to directory
return path.basename(path.resolve(process.cwd(), localConfigPath || ''));
}

View File

@@ -2,7 +2,7 @@ import { NowError } from '../now-error';
import Client from '../client';
import getDeploymentByIdOrHost from './get-deployment-by-id-or-host';
export default async function getDeploymentByIdOrThrow(
export default async function getDeploymentOrFail(
client: Client,
contextName: string,
idOrHost: string

View File

@@ -0,0 +1,19 @@
import Client from '../client';
type InstancesInfo = {
[dc: string]: {
instances: Array<{}>;
};
};
export default async function getDeploymentInstances(
now: Client,
deploymentId: string,
requestId: string
) {
return now.fetch<InstancesInfo>(
`/v3/now/deployments/${encodeURIComponent(
deploymentId
)}/instances?init=1&requestId=${requestId}`
);
}

View File

@@ -0,0 +1,34 @@
//
import through2 from 'through2';
import jsonlines from 'jsonlines';
import { stringify } from 'querystring';
import noop from '../noop';
async function getEventsStream(now, idOrHost, options) {
const response = await now.fetch(
`/v2/now/deployments/${idOrHost}/events?${stringify({
direction: options.direction,
follow: options.follow ? '1' : '',
format: options.format || 'lines',
limit: options.limit,
since: options.since,
until: options.until,
})}`
);
const stream = response.readable ? await response.readable() : response.body;
const pipeStream = stream.pipe(jsonlines.parse()).pipe(ignoreEmptyObjects);
stream.on('error', noop);
pipeStream.on('error', noop);
return pipeStream;
}
// Since we will be receiving empty object from the stream, this transform will ignore them
const ignoreEmptyObjects = through2.obj(function(chunk, enc, cb) {
if (Object.keys(chunk).length !== 0) {
this.push(chunk);
}
cb();
});
export default getEventsStream;

View File

@@ -0,0 +1,14 @@
//
export default function getInstanceIndex() {
const instancesIndex = {};
let items = 0;
return instanceId => {
if (instancesIndex[instanceId] === undefined) {
instancesIndex[instanceId] = items;
items += 1;
}
return instancesIndex[instanceId];
};
}

View File

@@ -12,6 +12,7 @@ import Now from '../../util';
import { NowConfig } from '../dev/types';
import { Org } from '../../types';
import ua from '../ua';
import processLegacyDeployment from './process-legacy-deployment';
import { linkFolderToProject } from '../projects/link';
import { prependEmoji, emoji } from '../emoji';
@@ -46,6 +47,7 @@ function printInspectUrl(
}
export default async function processDeployment({
isLegacy,
org,
cwd,
projectName,
@@ -60,6 +62,7 @@ export default async function processDeployment({
requestBody: DeploymentOptions;
uploadStamp: () => string;
deployStamp: () => string;
isLegacy: boolean;
quiet: boolean;
nowConfig?: NowConfig;
force?: boolean;
@@ -70,6 +73,8 @@ export default async function processDeployment({
skipAutoDetectionConfirmation?: boolean;
cwd?: string;
}) {
if (isLegacy) return processLegacyDeployment(args);
let {
now,
output,

View File

@@ -0,0 +1,146 @@
import bytes from 'bytes';
import Progress from 'progress';
import chalk from 'chalk';
import pluralize from 'pluralize';
import {
createLegacyDeployment,
DeploymentOptions,
NowClientOptions,
} from '@vercel/client';
import { Output } from '../output';
// @ts-ignore
import Now from '../../util';
import { NowConfig } from '../dev/types';
import ua from '../ua';
export default async function processLegacyDeployment({
now,
output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
force,
nowConfig,
}: {
now: Now;
output: Output;
hashes: { [key: string]: any };
paths: string[];
requestBody: DeploymentOptions;
uploadStamp: () => string;
deployStamp: () => string;
quiet: boolean;
nowConfig?: NowConfig;
force?: boolean;
}) {
let platformVersion = 1;
const { log, debug, note, warn } = output;
let bar: Progress | null = null;
const { env = {} } = requestBody;
const nowClientOptions: NowClientOptions = {
teamId: now.currentTeam,
apiUrl: now._apiUrl,
token: now._token,
debug: now._debug,
userAgent: ua,
path: paths[0],
force,
};
let fileCount = null;
for await (const event of createLegacyDeployment(
nowClientOptions,
requestBody,
nowConfig
)) {
if (event.type === 'notice') {
note(event.payload);
}
if (event.type === 'warning') {
warn(event.payload);
}
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
if (event.type === 'file-count') {
debug(
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
);
fileCount = event.payload.missing.length;
const missingSize = event.payload.missing
.map((sha: string) => event.payload.total.get(sha).data.length)
.reduce((a: number, b: number) => a + b, 0);
bar = new Progress(`${chalk.gray('>')} Upload [:bar] :percent :etas`, {
width: 20,
complete: '=',
incomplete: '',
total: missingSize,
clear: true,
});
}
if (event.type === 'file-uploaded') {
debug(
`Uploaded: ${event.payload.file.names.join(' ')} (${bytes(
event.payload.file.data.length
)})`
);
if (bar) {
bar.tick(event.payload.file.data.length);
}
}
if (event.type === 'created') {
now._host = event.payload.url;
if (typeof event.payload.version === 'number') {
platformVersion = event.payload.version;
}
if (!quiet) {
if (fileCount) {
log(`Synced ${pluralize('file', fileCount, true)} ${uploadStamp()}`);
}
log(
chalk`https://${event.payload.url} {gray [v${String(
platformVersion
)}]} ${deployStamp()}`
);
} else {
process.stdout.write(`https://${event.payload.url}`);
}
}
// Handle error events
if (event.type === 'error') {
throw await now.handleDeploymentError(event.payload, { hashes, env });
}
// Handle ready event
if (event.type === 'ready') {
log(`Build completed`);
if (platformVersion === 1) {
return event.payload;
}
}
// Handle alias-assigned event
if (platformVersion > 1 && event.type === 'alias-assigned') {
log(`Alias assigned`);
return event.payload;
}
}
}

View File

@@ -0,0 +1,18 @@
import { homedir } from 'os';
import promptBool from '../input/prompt-bool';
import { Output } from '../output';
export default async function shouldDeployDir(argv0: string, output: Output) {
let yes = true;
if (argv0 === homedir()) {
if (
!(await promptBool(
'You are deploying your home directory. Do you want to continue?'
))
) {
output.log('Aborted');
yes = false;
}
}
return yes;
}

View File

@@ -118,16 +118,7 @@ export async function devRouter(
continue;
}
// if the destination is an external URL (rewrite or redirect)
const isDestUrl = isURL(destPath);
if (
routeConfig.check &&
devServer &&
nowConfig &&
phase !== 'hit' &&
!isDestUrl
) {
if (routeConfig.check && devServer && nowConfig && phase !== 'hit') {
const { pathname = '/' } = url.parse(destPath);
const hasDestFile = await devServer.hasFilesystem(
pathname,
@@ -165,6 +156,7 @@ export async function devRouter(
}
}
const isDestUrl = isURL(destPath);
if (isDestUrl) {
result = {
found: true,

View File

@@ -20,7 +20,6 @@ import { ChildProcess } from 'child_process';
import isPortReachable from 'is-port-reachable';
import deepEqual from 'fast-deep-equal';
import which from 'which';
import npa from 'npm-package-arg';
import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
import {
@@ -41,7 +40,6 @@ import {
spawnCommand,
isOfficialRuntime,
} from '@vercel/build-utils';
import _frameworks, { Framework } from '@vercel/frameworks';
import link from '../output/link';
import { Output } from '../output';
@@ -87,11 +85,6 @@ import {
} from './types';
import { ProjectSettings } from '../../types';
const frameworkList = _frameworks as Framework[];
const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
);
interface FSEvent {
type: string;
path: string;
@@ -557,8 +550,8 @@ export default class DevServer {
const featHandleMiss = true; // enable for zero config
const { projectSettings, cleanUrls, trailingSlash } = config;
const opts = { output: this.output };
const files = (await getFiles(this.cwd, opts)).map(f =>
const opts = { output: this.output, isBuilds: true };
const files = (await getFiles(this.cwd, config, opts)).map(f =>
relative(this.cwd, f)
);
@@ -840,7 +833,8 @@ export default class DevServer {
const nowConfig = await this.getNowConfig();
const devCommandPromise = this.runDevCommand();
const files = await getFiles(this.cwd, { output: this.output });
const opts = { output: this.output, isBuilds: true };
const files = await getFiles(this.cwd, nowConfig, opts);
this.files = {};
for (const fsPath of files) {
let path = relative(this.cwd, fsPath);
@@ -2332,8 +2326,10 @@ async function checkForPort(
}
function filterFrontendBuilds(build: Builder) {
const { name } = npa(build.use);
return !frontendRuntimeSet.has(name || '');
return (
!isOfficialRuntime('static-build', build.use) &&
!isOfficialRuntime('next', build.use)
);
}
function hasNewRoutingProperties(nowConfig: NowConfig) {

View File

@@ -4,8 +4,6 @@ import { Output } from '../output';
import Client from '../client';
import getDomainDNSRecords from './get-domain-dns-records';
import getDomains from '../domains/get-domains';
import wait from '../output/wait';
import chalk from 'chalk';
export type DomainRecordsItem = {
domainName: string;
@@ -60,11 +58,6 @@ async function getDomainNames(
contextName: string,
next?: number
) {
const cancelWait = wait(`Fetching domains under ${chalk.bold(contextName)}`);
try {
const { domains, pagination } = await getDomains(client, next);
return { domainNames: domains.map(domain => domain.name), pagination };
} finally {
cancelWait();
}
const { domains, pagination } = await getDomains(client, contextName, next);
return { domainNames: domains.map(domain => domain.name), pagination };
}

View File

@@ -38,7 +38,7 @@ export default async function importZonefile(
} catch (error) {
cancelWait();
if (error.code === 'not_found') {
return new DomainNotFound(domain, contextName);
return new DomainNotFound(domain);
}
if (error.code === 'invalid_domain') {

View File

@@ -8,26 +8,24 @@ type Response = {
domain: Domain;
};
export default async function getDomainByName(
async function getDomainByName(
client: Client,
contextName: string,
domainName: string,
options: {
ignoreWait?: boolean;
} = {}
domainName: string
) {
const cancelWait = options.ignoreWait
? null
: wait(`Fetching domain ${domainName} under ${chalk.bold(contextName)}`);
const cancelWait = wait(
`Fetching domain ${domainName} under ${chalk.bold(contextName)}`
);
try {
const { domain } = await client.fetch<Response>(
`/v4/domains/${encodeURIComponent(domainName)}`
);
cancelWait();
return domain;
} catch (error) {
cancelWait();
if (error.status === 404) {
return new DomainNotFound(domainName, contextName);
return new DomainNotFound(domainName);
}
if (error.status === 403) {
@@ -35,7 +33,7 @@ export default async function getDomainByName(
}
throw error;
} finally {
cancelWait?.();
}
}
export default getDomainByName;

View File

@@ -1,7 +1,16 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { DomainConfig } from '../../types';
export async function getDomainConfig(client: Client, domainName: string) {
export async function getDomainConfig(
client: Client,
contextName: string,
domainName: string
) {
const cancelWait = wait(
`Fetching domain config ${domainName} under ${chalk.bold(contextName)}`
);
try {
const config = await client.fetch<DomainConfig>(
`/v4/domains/${domainName}/config`
@@ -14,5 +23,7 @@ export async function getDomainConfig(client: Client, domainName: string) {
}
throw error;
} finally {
cancelWait();
}
}

View File

@@ -19,11 +19,6 @@ export default async function getDomainPrice(
if (error.code === 'unsupported_tld') {
return new UnsupportedTLD(name);
}
if (error.status < 500) {
return error;
}
throw error;
}
}

View File

@@ -1,15 +0,0 @@
import { Domain } from '../../types';
export type DomainRegistrar = 'Vercel' | 'Purchase in Process' | 'Third Party';
export function getDomainRegistrar(domain: Domain): DomainRegistrar {
if (domain.boughtAt) {
return 'Vercel';
}
if (typeof domain.orderedAt === 'number' && !domain.boughtAt) {
return 'Purchase in Process';
}
return 'Third Party';
}

View File

@@ -1,15 +1,24 @@
import chalk from 'chalk';
import { Domain, PaginationOptions } from '../../types';
import Client from '../client';
import wait from '../output/wait';
type Response = {
domains: Domain[];
pagination: PaginationOptions;
};
export default async function getDomains(client: Client, next?: number) {
export default async function getDomains(
client: Client,
contextName: string,
next?: number
) {
let domainUrl = `/v5/domains?limit=20`;
if (next) {
domainUrl += `&until=${next}`;
}
return await client.fetch<Response>(domainUrl);
const cancelWait = wait(`Fetching domains under ${chalk.bold(contextName)}`);
const domains = await client.fetch<Response>(domainUrl);
cancelWait();
return domains;
}

View File

@@ -30,19 +30,12 @@ export default async function purchaseDomainIfAvailable(
}
output.debug(`Domain ${domain} is available to be purchased`);
const domainPrice = await getDomainPrice(client, domain).finally(() => {
cancelWait();
});
const domainPrice = await getDomainPrice(client, domain);
cancelWait();
if (domainPrice instanceof ERRORS.UnsupportedTLD) {
return domainPrice;
}
if (domainPrice instanceof Error) {
throw domainPrice;
}
const { price, period } = domainPrice;
output.log(
`Domain not found, but you can buy it under ${chalk.bold(
@@ -75,5 +68,6 @@ export default async function purchaseDomainIfAvailable(
}
output.debug(`Domain ${domain} is not available to be purchased`);
cancelWait();
return false;
}

View File

@@ -4,7 +4,6 @@ import { NowBuildError } from '@vercel/build-utils';
import { NowError } from './now-error';
import code from './output/code';
import { getCommandName } from './pkg-name';
import chalk from 'chalk';
/**
* This error is thrown when there is an API error with a payload. The error
@@ -69,9 +68,7 @@ export class InvalidToken extends NowError<'NOT_AUTHORIZED', {}> {
constructor() {
super({
code: `NOT_AUTHORIZED`,
message: `The specified token is not valid. Use ${getCommandName(
`login`
)} to generate a new token.`,
message: `The specified token is not valid`,
meta: {},
});
}
@@ -186,13 +183,11 @@ export class DomainNotFound extends NowError<
'DOMAIN_NOT_FOUND',
{ domain: string }
> {
constructor(domain: string, contextName?: string) {
constructor(domain: string) {
super({
code: 'DOMAIN_NOT_FOUND',
meta: { domain },
message: `Domain not found by "${domain}"${
contextName ? ` under ${chalk.bold(contextName)}` : ''
}.`,
message: `The domain ${domain} can't be found.`,
});
}
}
@@ -644,6 +639,19 @@ export class DeploymentPermissionDenied extends NowError<
}
}
export class DeploymentTypeUnsupported extends NowError<
'DEPLOYMENT_TYPE_UNSUPPORTED',
{}
> {
constructor() {
super({
code: 'DEPLOYMENT_TYPE_UNSUPPORTED',
meta: {},
message: `This region only accepts Serverless Docker Deployments`,
});
}
}
/**
* Returned when we try to create an alias but the API returns an error telling
* that the given alias is not valid.
@@ -687,6 +695,71 @@ export class CertMissing extends NowError<'ALIAS_IN_USE', { domain: string }> {
}
}
export class ForbiddenScaleMinInstances extends NowError<
'FORBIDDEN_SCALE_MIN_INSTANCES',
{ url: string; max: number }
> {
constructor(url: string, max: number) {
super({
code: 'FORBIDDEN_SCALE_MIN_INSTANCES',
meta: { url, max },
message: `You can't scale to more than ${max} min instances with your current plan.`,
});
}
}
export class ForbiddenScaleMaxInstances extends NowError<
'FORBIDDEN_SCALE_MAX_INSTANCES',
{ url: string; max: number }
> {
constructor(url: string, max: number) {
super({
code: 'FORBIDDEN_SCALE_MAX_INSTANCES',
meta: { url, max },
message: `You can't scale to more than ${max} max instances with your current plan.`,
});
}
}
export class InvalidScaleMinMaxRelation extends NowError<
'INVALID_SCALE_MIN_MAX_RELATION',
{ url: string }
> {
constructor(url: string) {
super({
code: 'INVALID_SCALE_MIN_MAX_RELATION',
meta: { url },
message: `Min number of instances can't be higher than max.`,
});
}
}
export class NotSupportedMinScaleSlots extends NowError<
'NOT_SUPPORTED_MIN_SCALE_SLOTS',
{ url: string }
> {
constructor(url: string) {
super({
code: 'NOT_SUPPORTED_MIN_SCALE_SLOTS',
meta: { url },
message: `Cloud v2 does not yet support setting a non-zero min scale setting.`,
});
}
}
export class VerifyScaleTimeout extends NowError<
'VERIFY_SCALE_TIMEOUT',
{ timeout: number }
> {
constructor(timeout: number) {
super({
code: 'VERIFY_SCALE_TIMEOUT',
meta: { timeout },
message: `Instance verification timed out (${timeout}ms)`,
});
}
}
export class CantParseJSONFile extends NowError<
'CANT_PARSE_JSON_FILE',
{ file: string }
@@ -750,6 +823,19 @@ export class FileNotFound extends NowError<'FILE_NOT_FOUND', { file: string }> {
}
}
export class RulesFileValidationError extends NowError<
'PATH_ALIAS_VALIDATION_ERROR',
{ location: string; message: string }
> {
constructor(location: string, message: string) {
super({
code: 'PATH_ALIAS_VALIDATION_ERROR',
meta: { location, message },
message: `The provided rules format in file for path alias are invalid`,
});
}
}
export class NoAliasInConfig extends NowError<'NO_ALIAS_IN_CONFIG', {}> {
constructor() {
super({
@@ -773,6 +859,58 @@ export class InvalidAliasInConfig extends NowError<
}
}
export class RuleValidationFailed extends NowError<
'RULE_VALIDATION_FAILED',
{ message: string }
> {
constructor(message: string) {
super({
code: 'RULE_VALIDATION_FAILED',
meta: { message },
message: `The server validation for rules failed`,
});
}
}
export class InvalidMinForScale extends NowError<
'INVALID_MIN_FOR_SCALE',
{ value: string }
> {
constructor(value: string) {
super({
code: 'INVALID_MIN_FOR_SCALE',
meta: { value },
message: `Invalid <min> parameter "${value}". A number or "auto" were expected`,
});
}
}
export class InvalidArgsForMinMaxScale extends NowError<
'INVALID_ARGS_FOR_MIN_MAX_SCALE',
{ min: string }
> {
constructor(min: string) {
super({
code: 'INVALID_ARGS_FOR_MIN_MAX_SCALE',
meta: { min },
message: `Invalid number of arguments: expected <min> ("${min}") and [max]`,
});
}
}
export class InvalidMaxForScale extends NowError<
'INVALID_MAX_FOR_SCALE',
{ value: string }
> {
constructor(value: string) {
super({
code: 'INVALID_MAX_FOR_SCALE',
meta: { value },
message: `Invalid <max> parameter "${value}". A number or "auto" were expected`,
});
}
}
export class InvalidCert extends NowError<'INVALID_CERT', {}> {
constructor() {
super({

View File

@@ -25,6 +25,39 @@ export class SchemaValidationFailed extends NowError<
}
}
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
interface InvalidAllForScaleMeta {}
export class InvalidAllForScale extends NowError<
'INVALID_ALL_FOR_SCALE',
InvalidAllForScaleMeta
> {
constructor() {
super({
code: 'INVALID_ALL_FOR_SCALE',
meta: {},
message: `You can't use all in the regions list mixed with other regions`,
});
}
}
interface InvalidRegionOrDCForScaleMeta {
regionOrDC: string;
}
export class InvalidRegionOrDCForScale extends NowError<
'INVALID_REGION_OR_DC_FOR_SCALE',
InvalidRegionOrDCForScaleMeta
> {
constructor(regionOrDC: string) {
super({
code: 'INVALID_REGION_OR_DC_FOR_SCALE',
meta: { regionOrDC },
message: `Invalid region or DC "${regionOrDC}" provided`,
});
}
}
interface InvalidLocalConfigMeta {
value: string[];
}

View File

@@ -1,5 +1,5 @@
// Native
import { URLSearchParams } from 'url';
import qs from 'querystring';
// Packages
import { eraseLines } from 'ansi-escapes';
@@ -25,7 +25,7 @@ async function printEvents(
onOpen();
}
const query = new URLSearchParams({
const q = qs.stringify({
direction: findOpts.direction,
limit: findOpts.limit,
since: findOpts.since,
@@ -34,7 +34,7 @@ async function printEvents(
format: 'lines',
});
let eventsUrl = `/v1/now/deployments/${deploymentIdOrURL}/events?${query}`;
let eventsUrl = `/v1/now/deployments/${deploymentIdOrURL}/events?${q}`;
let pollUrl = `/v3/now/deployments/${deploymentIdOrURL}`;
if (currentTeam) {

View File

@@ -0,0 +1,7 @@
import error from './output/error';
import exit from './exit';
export default (msg, code = 1) => {
console.log(error(msg));
exit(code);
};

View File

@@ -1,5 +1,6 @@
import { resolve } from 'path';
import ignore from 'ignore';
import dockerignore from '@zeit/dockerignore';
import _glob, { IOptions as GlobOptions } from 'glob';
import fs from 'fs-extra';
import { getVercelIgnore } from '@vercel/client';
@@ -146,8 +147,15 @@ const asAbsolute = function (path: string, parent: string) {
return resolve(parent, path);
};
export async function createIgnore(ignoreFilePath: string) {
const ignoreFile = await maybeRead(ignoreFilePath, '');
const ig = ignore().add(`${IGNORED}\n${clearRelative(ignoreFile)}`);
return ig;
}
interface StaticFilesOptions {
output: Output;
isBuilds: boolean;
src?: string;
}
@@ -158,6 +166,7 @@ interface StaticFilesOptions {
*
* @param {String} full path to directory
* @param {Object} options:
* - `isBuilds` {boolean} true for Now 2.0 builders
* - `output` {Object} "output" helper object
* - `src` {string|undefined} optional builder source
* @return {Array} comprehensive list of paths to sync
@@ -165,51 +174,58 @@ interface StaticFilesOptions {
export async function staticFiles(
path: string,
{ output, src }: StaticFilesOptions
nowConfig: NowConfig = {},
{ output, isBuilds, src }: StaticFilesOptions
) {
const { debug, time } = output;
let files: string[] = [];
// The package.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
const source = src || '.';
// Convert all filenames into absolute paths
const search = await glob(source, { cwd: path, absolute: true, dot: true });
if (!isBuilds && nowConfig.files && Array.isArray(nowConfig.files)) {
files = await getFilesInWhitelist(nowConfig.files, path, { output });
} else {
// The package.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
const source = src || '.';
// Convert all filenames into absolute paths
const search = await glob(source, { cwd: path, absolute: true, dot: true });
// Compile list of ignored patterns and files
const { ig } = await getVercelIgnore(path);
const filter = ig.createFilter();
// Compile list of ignored patterns and files
const ig = isBuilds
? (await getVercelIgnore(path)).ig
: await createIgnore(resolve(path, '.gitignore'));
const filter = ig.createFilter();
const prefixLength = path.length + 1;
const prefixLength = path.length + 1;
// The package.json `files` whitelist still
// honors npmignores: https://docs.npmjs.com/files/package.json#files
// but we don't ignore if the user is explicitly listing files
// under the now namespace, or using files in combination with gitignore
const accepts = (file: string) => {
const relativePath = file.substr(prefixLength);
// The package.json `files` whitelist still
// honors npmignores: https://docs.npmjs.com/files/package.json#files
// but we don't ignore if the user is explicitly listing files
// under the now namespace, or using files in combination with gitignore
const accepts = (file: string) => {
const relativePath = file.substr(prefixLength);
if (relativePath === '') {
return true;
}
if (relativePath === '') {
return true;
}
const accepted = filter(relativePath);
const accepted = filter(relativePath);
if (!accepted) {
debug(`Ignoring ${file}`);
}
if (!accepted) {
debug(`Ignoring ${file}`);
}
return accepted;
};
return accepted;
};
// Locate files
files = await time(
`Locating files ${path}`,
explode(search, {
accepts,
output,
})
);
// Locate files
files = await time(
`Locating files ${path}`,
explode(search, {
accepts,
output,
})
);
}
// Get files
return uniqueStrings(files);
@@ -252,7 +268,7 @@ export async function npm(
const search = Array.prototype.concat.apply(
[],
await Promise.all(
search_.map(file =>
search_.map((file) =>
glob(file, { cwd: path, absolute: true, dot: true })
)
)
@@ -309,6 +325,92 @@ export async function npm(
return uniqueStrings(files);
}
interface DockerOptions {
hasNowJson: boolean;
output: Output;
}
/**
* Returns a list of files in the given
* directory that are subject to be
* sent to docker as build context.
*
* @param {String} full path to directory
* @param {String} contents of `Dockerfile`
* @param {Object} options:
* - `limit` {Number|null} byte limit
* - `output` {Object} "output" helper object
* @return {Array} comprehensive list of paths to sync
*/
export async function docker(
path: string,
nowConfig: NowConfig = {},
{ hasNowJson = false, output }: DockerOptions
) {
const { debug, time } = output;
let files: string[] = [];
if (nowConfig.files) {
files = await getFilesInWhitelist(nowConfig.files, path, { output });
} else {
// Base search path
// the now.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
const search_ = ['.'];
// Convert all filenames into absolute paths
const search = search_.map((file) => asAbsolute(file, path));
// Compile list of ignored patterns and files
const dockerIgnore = await maybeRead(resolve(path, '.dockerignore'), null);
const ignoredFiles = clearRelative(
dockerIgnore === null
? await maybeRead(resolve(path, '.gitignore'), '')
: dockerIgnore
);
const ignoreInit = ((dockerIgnore === null
? ignore
: dockerignore) as any) as typeof ignore;
const filter = ignoreInit()
.add(`${IGNORED}\n${ignoredFiles}`)
.createFilter();
const prefixLength = path.length + 1;
const accepts = function (file: string) {
const relativePath = file.substr(prefixLength);
if (relativePath === '') {
return true;
}
const accepted = filter(relativePath);
if (!accepted) {
debug(`Ignoring ${file}`);
}
return accepted;
};
// Locate files
files = await time(
`Locating files ${path}`,
explode(search, { accepts, output })
);
}
if (hasNowJson) {
files.push(asAbsolute(getLocalConfigPath(path), path));
}
// Always include manifest as npm does not allow ignoring it
// source: https://docs.npmjs.com/files/package.json#files
files.push(asAbsolute('Dockerfile', path));
// Get files
return uniqueStrings(files);
}
interface ExplodeOptions {
accepts: (file: string) => boolean;
output: Output;
@@ -358,7 +460,7 @@ async function explode(
if (s.isDirectory()) {
const all = await fs.readdir(file);
/* eslint-disable no-use-before-define */
const recursive = many(all.map(subdir => asAbsolute(subdir, file)));
const recursive = many(all.map((subdir) => asAbsolute(subdir, file)));
return (recursive as any) as Promise<string | null>;
/* eslint-enable no-use-before-define */
}
@@ -370,7 +472,7 @@ async function explode(
return path;
};
const many = (all: string[]) => Promise.all(all.map(file => list(file)));
const many = (all: string[]) => Promise.all(all.map((file) => list(file)));
const arrayOfArrays = await many(paths);
return flatten(arrayOfArrays).filter(notNull);
}

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