Compare commits

..

26 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
Steven
178f5e7f00 Publish Canary
- @vercel/build-utils@2.4.3-canary.2
 - vercel@20.0.0-canary.4
 - @vercel/client@8.2.2-canary.3
2020-07-23 21:00:25 -04:00
Steven
dbfdf20c9d [build-utils] Fix redwood /api detection (#4916)
Previously, the zero detection would see js files in the `/api` directory and attempt to use `@vercel/node`. However, this doesn't work with RedwoodJS which uses a different format for [`/api`](https://github.com/vercel/vercel/tree/master/examples/redwoodjs/api) so we must only use `@vercel/redwood` when RedwoodJS is detected.
2020-07-24 00:33:06 +00:00
Juan Campa
a80ed4ec9b [api] Fix api-framework cache header (#4913)
## Problem

`api-frameworks` is not being cached by our edge when used via the fallback-to-front mechanism. That's because the headers are only applied to `/api/frameworks` but not `/api/v1/frameworks` which is the actual path being used.

## Solution

Also apply the caching headers to `/api/<version>/frameworks`
2020-07-23 23:48:47 +00:00
Steven
833c3d7c01 Publish Canary
- @vercel/frameworks@0.0.18-canary.0
 - @vercel/build-utils@2.4.3-canary.1
 - vercel@20.0.0-canary.3
 - @vercel/client@8.2.2-canary.2
 - @vercel/redwood@0.0.2-canary.1
2020-07-23 18:29:16 -04:00
Steven
19d0db7270 [frameworks] Add autodetection for RedwoodJS (#4915)
- Supersedes #4539
- Related to #4676

Co-authored-by: Lachlan Campbell <lachlan@hackclub.com>
2020-07-23 18:02:50 -04:00
Steven
041e8cc601 Publish Canary
- @vercel/build-utils@2.4.3-canary.0
 - vercel@20.0.0-canary.2
 - @vercel/client@8.2.2-canary.1
 - @vercel/static-build@0.17.7-canary.1
 - @vercel/redwood@0.0.2-canary.0
2020-07-23 16:28:44 -04:00
Steven
b118e461b1 [redwoood] Add runtime @vercel/redwood (#4676)
### Changes
- Add new Runtime: `@vercel/redwood`
- Update `run.js` script to sort packages properly

### Related
- Related to #4539 
- Related to #4535
- Depends on https://github.com/apollographql/apollo-server/pull/4311
2020-07-23 16:22:48 -04:00
Andy Bitz
e519d49d7b Publish Canary
- vercel@20.0.0-canary.1
2020-07-23 15:42:46 +02:00
Andy Bitz
27683818ba Bump vercel to 20.0.0-canary.0 2020-07-23 15:42:30 +02:00
Andy
e016e38229 [cli] (Major) Change vc domains add workflow to match the dashboard (#3325)
* [now-cli] Change `now domains add` workflow to match dashboard

* Fix error throwing and add --force

* Remove `now domains verify`

* Count projects when removing a domain

* Include projects for `now inspect`

* Updated `now domains ls`

* Fix `now domains ls`

* Use Finally

* Add tests

* Use --force for domain tests

* Adjust tests

* Adjust test and fix output

* Fix padding

* Adjust more tests

* Ensure the project exists

* Log the deployment

* Fix tests again

* Remove project after test

* Fix command

* Fix test

* Change user after a couple of tests

* Add check

* Use random name for domain

* More logging

* Add more logging

* Remove logging

* Remove .now/project.json every time

* Skip test

* Add test to change user

* More logging

* Use now login

* Remove duplicated test

* Consider linked project for `domains add`

* Fix linting

* Remove unused docs

* Undo changes

* Undo more changes

* Fix typo

* Do not sort projects when there is only one record

* Fix loop

* Deploy with `-V 2`

* Remove verification

* Remove outdated link

* Update packages/now-cli/src/commands/domains/add.ts

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

* Update packages/now-cli/src/commands/domains/add.ts

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

* Use utils for public suffix and service type

* Update packages/now-cli/src/commands/domains/ls.ts

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

* Update packages/now-cli/src/commands/domains/add.ts

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

* Change link

* Undo test changes

* Fix type issues

* Update packages/now-cli/src/commands/domains/add.ts

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

* Use domain config to print info

* Check apex domain when removing a domain

* Remove psl

* List projects when removing a domain

* Do not list projects

* Change user earlier

* Update packages/now-cli/src/commands/domains/inspect.ts

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

* Apply suggestions from code review

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

* Apply suggestions from code review

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

Co-authored-by: Mark Glagola <mark.glagola@gmail.com>
Co-authored-by: Steven <steven@ceriously.com>
2020-07-23 15:41:25 +02:00
Max Leiter
5db1c5e610 [tests] Skip try to purchase a domain CLI test if a VERCEL_TOKEN is provided (#4909)
I placed the check inside the test because otherwise `test.only` and other test functions break with this test.
2020-07-23 01:39:05 +00:00
Steven
24c228569f Publish Canary
- vercel@19.2.1-canary.2
 - @vercel/next@2.6.14-canary.1
2020-07-22 16:09:26 -04:00
Steven
963de9b64f [cli] Update "set up" message (#4907)
Follow up to #4897 

* `vc link` - Update message to say `Set up ~/proj`
* `vc dev` - Update message  to say `Set up and develop ~/proj`
* `vc link --help` - Updated example message to include `vc link ~/proj` usage
2020-07-22 19:13:41 +00:00
JJ Kasper
ab7fd52305 [next] Don't trace pages for lambdas when not needed (#4841)
When pages are fully static and don't leverage preview mode or revalidation we can skip tracing the pages for generating the lambdas since lambdas aren't needed in this case
2020-07-22 18:14:47 +00:00
Nathan Rajlich
0fdb0dac91 [cli] Send the Vercel platform proxy request headers to frontend dev server (#4874)
This is a follow up to #4850 that does the same thing when
proxying to the frontend dev server.
2020-07-22 14:36:15 +00:00
Andy Bitz
bb0b632dcf Publish Canary
- vercel@19.2.1-canary.1
 - @vercel/go@1.1.5-canary.0
 - @vercel/next@2.6.14-canary.0
 - @vercel/node@1.7.4-canary.0
 - @vercel/static-build@0.17.7-canary.0
2020-07-22 13:34:40 +02:00
Andy
ced9495143 Revert "Revert "[go] Implement startDevServer() function (#4662)" (#4899)" (#4906)
This reverts commit 8d1afc026f.
2020-07-22 13:33:36 +02:00
Nathan Rajlich
fadc3f2588 [next][node][static-build] Execute "vercel-build" script if defined (#4863)
* [build-utils] Make `runPackageJsonScript()` run the `vercel-` or `now-` if defined in `package.json`

* [build-utils] Export `getScriptName()`

* [next] Use `getScriptName()` and `remove()`

* [node] Update for `vercel-`

* [static-build] Update for `vercel-`

* Remove debug

* Add `getScriptName()` unit tests

* Test for `vercel-build` in e2e

* Make platform name behavior be opt-in

So that it's not a breaking behavior change.

* Check for only "vercel-build" or "now-build", but not "build"

* Simplify `getScriptName()` to return the first existing script in a possible set

* Revert change

* Fix test

Co-authored-by: Joe Haddad <joe.haddad@zeit.co>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-07-21 20:04:24 -07:00
Steven
a1d548dfef Publish Canary
- vercel@19.2.1-canary.0
 - @vercel/client@8.2.2-canary.0
2020-07-21 18:56:21 -04:00
Steven
754090a8ab [cli] Add vercel link command (#4897)
- [x] Add `vercel link` command
- [x] Update `vercel dev` to automatically link 
- [x] Updated `vercel env` error message to mention `vercel link`
- [x] Test `vercel link`
- [x] Test `vercel dev`
- [x] Add `--confirm` flag

### Related

- Fixes #3767
- Fixes #4889 
- Fixes #3991 
- Fixes #4534 
- Related to #3989
2020-07-21 18:52:46 -04:00
dependabot[bot]
8269a48ee0 Bump codecov from 3.6.5 to 3.7.1 (#4900)
Bumps [codecov](https://github.com/codecov/codecov-node) from 3.6.5 to 3.7.1.
<details>
<summary>Commits</summary>
<ul>
<li><a href="29dd5b6b03"><code>29dd5b6</code></a> 3.7.1</li>
<li><a href="c0711c6566"><code>c0711c6</code></a> Switch from execSync to execFileSync (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/180">#180</a>)</li>
<li><a href="5f6cc62d4f"><code>5f6cc62</code></a> Bump lodash from 4.17.15 to 4.17.19 (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/183">#183</a>)</li>
<li><a href="0c4d7f3c6f"><code>0c4d7f3</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/182">#182</a> from codecov/update-readme-badges</li>
<li><a href="cc5e121842"><code>cc5e121</code></a> Update depstat image and urls</li>
<li><a href="b44b44e1f8"><code>b44b44e</code></a> Update readme with 400 error info (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/181">#181</a>)</li>
<li><a href="bb79335719"><code>bb79335</code></a> V3.7.0 (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/179">#179</a>)</li>
<li><a href="0d7b9b01cb"><code>0d7b9b0</code></a> Remove <code>'x-amz-acl': 'public-read'</code> header (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/178">#178</a>)</li>
<li><a href="eeff4e1953"><code>eeff4e1</code></a> Bump acorn from 5.7.3 to 5.7.4 (<a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/174">#174</a>)</li>
<li><a href="eb8a527470"><code>eb8a527</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/codecov/codecov-node/issues/172">#172</a> from RoboCafaz/bugfix/codebuild-pr-parser</li>
<li>Additional commits viewable in <a href="https://github.com/codecov/codecov-node/compare/v3.6.5...v3.7.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov&package-manager=npm_and_yarn&previous-version=3.6.5&new-version=3.7.1)](https://help.github.com/articles/configuring-automated-security-fixes)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/vercel/vercel/network/alerts).

</details>
2020-07-21 17:25:56 +00:00
160 changed files with 18433 additions and 627 deletions

View File

@@ -19,6 +19,11 @@ indent_style = space
[*.py]
indent_size = 4
[*.go]
indent_style = tab
indent_size = 4
tab_width = 4
[*.asm]
indent_size = 8

View File

@@ -34,3 +34,6 @@ packages/now-node-bridge/bridge.*
# now-static-build
packages/now-static-build/test/fixtures
# redwood
packages/redwood/test/fixtures

View File

@@ -2,16 +2,14 @@
#### Why This Error Occurred
The domain you supplied cannot be verified using either the intended set of nameservers or the given verification TXT record.
The domain you supplied cannot be verified using the intended nameservers.
#### Possible Ways to Fix It
#### Possible Way to Fix It
Apply the intended set of nameservers to your domain or add the given TXT verification record through your domain provider.
Apply the intended set of nameservers to your domain.
You can retrieve both the intended nameservers and TXT verification record for the domain you wish to verify by running `vercel domains inspect <domain>`.
When you have added either verification method to your domain, you can run `vercel domains verify <domain>` again to complete verification for your domain.
Vercel will also automatically check periodically that your domain has been verified and automatically mark it as such if we detect either verification method on the domain.
If you would not like to verify your domain, you can remove it from your account using `vercel domains rm <domain>`.

View File

@@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -0,0 +1,7 @@
# These environment variables will be used by default if you do not create any
# yourself in .env. This file should be safe to check into your version control
# system. Any custom values should go in .env and .env should *not* be checked
# into version control.
DATABASE_URL=file:./dev.db
BINARY_TARGET=native

10
examples/redwoodjs/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
.env
.netlify
dev.db
dist
dist-babel
node_modules
yarn-error.log
.vercel

View File

@@ -0,0 +1 @@
lts/*

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Redwood
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,29 @@
![RedwoodJS Logo](https://github.com/vercel/vercel/blob/master/packages/frameworks/logos/redwood.svg)
# RedwoodJS Example
This directory is a brief example of a [RedwoodJS](https://redwoodjs.com) app with [Serverless Functions](https://vercel.com/docs/v2/serverless-functions/introduction) that can be deployed with Vercel and zero configuration.
## Deploy Your Own
Deploy your own RedwoodJS project, along with Serverless Functions, with Vercel.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/vercel/tree/master/examples/redwoodjs)
_Live Example: https://redwoodjs.now-examples.now.sh_
### How We Created This Example
To get started with RedwoodJS on Vercel, you can [use Yarn to initialize](https://redwoodjs.com/tutorial/installation-starting-development) the project:
```shell
$ yarn create redwood-app ./my-redwood-app
```
### Deploying From Your Terminal
You can deploy your new RedwoodJS project, along with [Serverless Functions](https://vercel.com/docs/v2/serverless-functions/introduction), with a single command from your terminal using [Vercel CLI](https://vercel.com/download):
```shell
$ vercel
```

View File

@@ -0,0 +1 @@
module.exports = { extends: '../babel.config.js' }

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,8 @@
{
"name": "api",
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "0.14.0"
}
}

View File

@@ -0,0 +1,18 @@
datasource DS {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = env("BINARY_TARGET")
}
// Define your own datamodels here and run `yarn redwood db save` to create
// migrations for them.
// TODO: Please remove the following example:
model UserExample {
id Int @id @default(autoincrement())
email String @unique
name String?
}

View File

@@ -0,0 +1,26 @@
/* eslint-disable no-console */
const { PrismaClient } = require('@prisma/client')
const dotenv = require('dotenv')
dotenv.config()
const db = new PrismaClient()
async function main() {
// Seed data is database data that needs to exist for your app to run.
// Ideally this file should be idempotent: running it multiple times
// will result in the same database state (usually by checking for the
// existence of a record before trying to create it). For example:
//
// const existing = await db.user.findMany({ where: { email: 'admin@email.com' }})
// if (!existing.length) {
// await db.user.create({ data: { name: 'Admin', email: 'admin@email.com' }})
// }
console.info('No data to seed. See api/prisma/seeds.js for info.')
}
main()
.catch((e) => console.error(e))
.finally(async () => {
await db.disconnect()
})

View File

@@ -0,0 +1,19 @@
import {
createGraphQLHandler,
makeMergedSchema,
makeServices,
} from '@redwoodjs/api'
import importAll from '@redwoodjs/api/importAll.macro'
import { db } from 'src/lib/db'
const schemas = importAll('api', 'graphql')
const services = importAll('api', 'services')
export const handler = createGraphQLHandler({
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
}),
db,
})

View File

View File

@@ -0,0 +1,6 @@
// See https://github.com/prisma/prisma2/blob/master/docs/prisma-client-js/api.md#constructor
// for options.
import { PrismaClient } from '@prisma/client'
export const db = new PrismaClient()

View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ['@redwoodjs/core/config/babel-preset'],
}

View File

@@ -0,0 +1,19 @@
{
"private": true,
"workspaces": {
"packages": [
"api",
"web"
]
},
"devDependencies": {
"@redwoodjs/core": "0.14.0"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config"
},
"engines": {
"node": ">=12",
"yarn": ">=1.15"
}
}

View File

@@ -0,0 +1,9 @@
// https://prettier.io/docs/en/options.html
module.exports = {
trailingComma: 'es5',
bracketSpacing: true,
tabWidth: 2,
semi: false,
singleQuote: true,
arrowParens: 'always',
}

View File

@@ -0,0 +1,7 @@
[web]
port = 8910
apiProxyPath = "/api"
[api]
port = 8911
[browser]
open = false

View File

@@ -0,0 +1 @@
module.exports = { extends: '../babel.config.js' }

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
},
"jsx": "preserve"
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,15 @@
{
"name": "web",
"version": "0.0.0",
"private": true,
"browserslist": [
"defaults"
],
"dependencies": {
"@redwoodjs/router": "0.14.0",
"@redwoodjs/web": "0.14.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1"
}
}

View File

@@ -0,0 +1,36 @@
# Static Assets
Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Webpack builds for production). They will also be available during development when you run `yarn rw dev`.
>Note: files will *not* hot reload while the development server is running. You'll need to manually stop/start to access file changes.
### Example Use
A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g.
```
<link rel="icon" type="image/png" href="/favicon.png" />
```
and
```
<img src="/static-files/my-logo.jpg"> alt="Logo" />
```
Behind the scenes, we are using Webpack's ["copy-webpack-plugin"](https://github.com/webpack-contrib/copy-webpack-plugin).
## Best Practices
Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Webpack, etc.
In general, it's best to import files directly into a template, page, or component. This allows Webpack to include that file in the bundle, which ensures Webpack will correctly process and move assets into the distribution folder, providing error checks and correct paths along the way.
### Example Asset Import with Webpack
Instead of handling our logo image as a static file per the example above, we can do the following:
```
import React from "react"
import logo from "./my-logo.jpg"
function Header() {
return <img src={logo} alt="Logo" />
}
export default Header
```
Behind the scenes, we are using Webpack's ["file-loader"](https://webpack.js.org/loaders/file-loader/) and ["url-loader](https://webpack.js.org/loaders/url-loader/) (for files smaller than 10kb).

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,22 @@
// In this file, all Page components from 'src/pages` are auto-imported. Nested
// directories are supported, and should be uppercase. Each subdirectory will be
// prepended onto the component name.
//
// Examples:
//
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
import { Router, Route } from '@redwoodjs/router'
const Routes = () => {
return (
<Router>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="about" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes

View File

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="redwood-app"></div>
</body>
</html>

View File

@@ -0,0 +1,16 @@
import ReactDOM from 'react-dom'
import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
import './index.css'
ReactDOM.render(
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider>
<Routes />
</RedwoodProvider>
</FatalErrorBoundary>,
document.getElementById('redwood-app')
)

View File

View File

@@ -0,0 +1,44 @@
export default () => (
<main>
<style
dangerouslySetInnerHTML={{
__html: `
html, body {
margin: 0;
}
html * {
box-sizing: border-box;
}
main {
display: flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
text-align: center;
background-color: #E2E8F0;
height: 100vh;
}
section {
background-color: white;
border-radius: 0.25rem;
width: 32rem;
padding: 1rem;
margin: 0 auto;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: 500;
line-height: 1;
color: #2D3748;
}
`,
}}
/>
<section>
<h1>
<span>About RedwoodJS</span>
</h1>
</section>
</main>
)

View File

@@ -0,0 +1,53 @@
// This page will be rendered when an error makes it all the way to the top of the
// application without being handled by a Javascript catch statement or React error
// boundary.
//
// You can modify this page as you wish, but it is important to keep things simple to
// avoid the possibility that it will cause its own error. If it does, Redwood will
// still render a generic error page, but your users will prefer something a bit more
// thoughtful. =)
export default () => (
<main>
<style
dangerouslySetInnerHTML={{
__html: `
html, body {
margin: 0;
}
html * {
box-sizing: border-box;
}
main {
display: flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
text-align: center;
background-color: #E2E8F0;
height: 100vh;
}
section {
background-color: white;
border-radius: 0.25rem;
width: 32rem;
padding: 1rem;
margin: 0 auto;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: 500;
line-height: 1;
color: #2D3748;
}
`,
}}
/>
<section>
<h1>
<span>Something went wrong</span>
</h1>
</section>
</main>
)

View File

@@ -0,0 +1,44 @@
export default () => (
<main>
<style
dangerouslySetInnerHTML={{
__html: `
html, body {
margin: 0;
}
html * {
box-sizing: border-box;
}
main {
display: flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
text-align: center;
background-color: #E2E8F0;
height: 100vh;
}
section {
background-color: white;
border-radius: 0.25rem;
width: 32rem;
padding: 1rem;
margin: 0 auto;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: 500;
line-height: 1;
color: #2D3748;
}
`,
}}
/>
<section>
<h1>
<span>Welcome to RedwoodJS!</span>
</h1>
</section>
</main>
)

View File

@@ -0,0 +1,44 @@
export default () => (
<main>
<style
dangerouslySetInnerHTML={{
__html: `
html, body {
margin: 0;
}
html * {
box-sizing: border-box;
}
main {
display: flex;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
text-align: center;
background-color: #E2E8F0;
height: 100vh;
}
section {
background-color: white;
border-radius: 0.25rem;
width: 32rem;
padding: 1rem;
margin: 0 auto;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: 500;
line-height: 1;
color: #2D3748;
}
`,
}}
/>
<section>
<h1>
<span>404 Page Not Found</span>
</h1>
</section>
</main>
)

View File

@@ -682,6 +682,33 @@
}
}
},
{
"name": "RedwoodJS",
"slug": "redwoodjs",
"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",
"detectors": {
"every": [
{
"path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@redwoodjs\\/core\":\\s*\".+?\"[^}]*}"
}
]
},
"settings": {
"buildCommand": {
"value": "yarn rw db up --no-db-client --auto-approve && yarn rw build"
},
"devCommand": {
"value": "yarn rw dev"
},
"outputDirectory": {
"value": "RedwoodJS default"
}
}
},
{
"name": "Hugo",
"slug": "hugo",

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,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.0.17",
"version": "0.0.18-canary.0",
"main": "frameworks.json",
"license": "UNLICENSED",
"scripts": {

View File

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

View File

@@ -309,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,
@@ -468,6 +475,10 @@ function detectFrontBuilder(
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
// TODO - What if just a build script is provided, but no entrypoint.
const entrypoints = new Set([

View File

@@ -15,7 +15,9 @@ async function readFileOrNull(file: string) {
return null;
}
export async function readConfigFile<T>(files: string | string[]) {
export async function readConfigFile<T>(
files: string | string[]
): Promise<T | null> {
files = Array.isArray(files) ? files : [files];
for (const name of files) {
@@ -24,11 +26,11 @@ export async function readConfigFile<T>(files: string | string[]) {
if (data) {
const str = data.toString('utf8');
if (name.endsWith('.json')) {
return JSON.parse(str);
return JSON.parse(str) as T;
} else if (name.endsWith('.toml')) {
return (toml.parse(str) as unknown) as T;
} else if (name.endsWith('.yaml') || name.endsWith('.yml')) {
return yaml.safeLoad(str, { filename: name });
return yaml.safeLoad(str, { filename: name }) as T;
}
}
}

View File

@@ -1808,6 +1808,36 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect((errorRoutes![0] as Source).status).toBe(404);
});
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, errorRoutes } = await detectBuilders(files, null, {
projectSettings,
featHandleMiss,
});
expect(builders).toEqual([
{
use: '@vercel/redwood',
src: 'package.json',
config: {
zeroConfig: true,
framework: 'redwoodjs',
},
},
]);
expect(errorRoutes!.length).toBe(1);
expect((errorRoutes![0] as Source).status).toBe(404);
});
it('No framework, only package.json', async () => {
const files = ['package.json'];
const pkg = {

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "19.2.0",
"version": "20.0.0-canary.4",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -62,13 +62,14 @@
"node": ">= 10"
},
"dependencies": {
"@vercel/build-utils": "2.4.2",
"@vercel/go": "1.1.4",
"@vercel/next": "2.6.13",
"@vercel/node": "1.7.3",
"@vercel/build-utils": "2.4.3-canary.2",
"@vercel/go": "1.1.5-canary.0",
"@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.1",
"@vercel/ruby": "1.2.3",
"@vercel/static-build": "0.17.6",
"@vercel/static-build": "0.17.7-canary.1",
"update-notifier": "4.1.0"
},
"devDependencies": {
@@ -120,7 +121,7 @@
"chalk": "2.4.2",
"chokidar": "3.3.1",
"clipboardy": "2.1.0",
"codecov": "3.6.5",
"codecov": "3.7.1",
"cpy": "7.2.0",
"credit-card": "3.0.1",
"date-fns": "1.29.0",

View File

@@ -8,14 +8,15 @@ import Client from '../../util/client';
import { getLinkedProject } from '../../util/projects/link';
import { getFrameworks } from '../../util/get-frameworks';
import { isSettingValue } from '../../util/is-setting-value';
import { getCommandName } from '../../util/pkg-name';
import { ProjectSettings, ProjectEnvTarget } from '../../types';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
import { Env } from '@vercel/build-utils';
import setupAndLink from '../../util/link/setup-and-link';
type Options = {
'--debug'?: boolean;
'--listen'?: string;
'--confirm': boolean;
};
export default async function dev(
@@ -37,20 +38,33 @@ export default async function dev(
});
// retrieve dev command
const [link, frameworks] = await Promise.all([
let [link, frameworks] = await Promise.all([
getLinkedProject(output, client, cwd),
getFrameworks(client),
]);
if (link.status === 'error') {
return link.exitCode;
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
const autoConfirm = opts['--confirm'];
const forceDelete = false;
link = await setupAndLink(
ctx,
output,
cwd,
forceDelete,
autoConfirm,
'link',
'Set up and develop'
);
if (link.status === 'not_linked') {
// User aborted project linking questions
return 0;
}
}
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
output.error(
`Your codebase isnt linked to a project on Vercel. Run ${getCommandName()} to link it.`
);
return 1;
if (link.status === 'error') {
return link.exitCode;
}
let devCommand: string | undefined;

View File

@@ -32,6 +32,7 @@ const help = () => {
-d, --debug Debug mode [off]
-l, --listen [uri] Specify a URI endpoint on which to listen [0.0.0.0:3000]
-t, --token [token] Specify an Authorization Token
--confirm Skip questions and use defaults when setting up a new project
${chalk.dim('Examples:')}
@@ -56,6 +57,7 @@ export default async function main(ctx: NowContext) {
argv = getArgs(ctx.argv.slice(2), {
'--listen': String,
'-l': '--listen',
'--confirm': Boolean,
// Deprecated
'--port': Number,

View File

@@ -1,23 +1,24 @@
import chalk from 'chalk';
import psl from 'psl';
import { NowContext } from '../../types';
import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import addDomain from '../../util/domains/add-domain';
import Client from '../../util/client';
import cmd from '../../util/output/cmd';
import formatDnsTable from '../../util/format-dns-table';
import formatNSTable from '../../util/format-ns-table';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import param from '../../util/output/param';
import { getCommandName, getTitleName } from '../../util/pkg-name';
import { getCommandName } from '../../util/pkg-name';
import { getDomain } from '../../util/domains/get-domain';
import { getLinkedProject } from '../../util/projects/link';
import { isPublicSuffix } from '../../util/domains/is-public-suffix';
import { getDomainConfig } from '../../util/domains/get-domain-config';
import { addDomainToProject } from '../../util/projects/add-domain-to-project';
import { removeDomainFromProject } from '../../util/projects/remove-domain-from-project';
import code from '../../util/output/code';
type Options = {
'--cdn': boolean;
'--debug': boolean;
'--no-cdn': boolean;
'--force': boolean;
};
export default async function add(
@@ -33,6 +34,7 @@ export default async function add(
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const force = opts['--force'];
const client = new Client({ apiUrl, token, currentTeam, debug });
let contextName = null;
@@ -47,105 +49,116 @@ export default async function add(
throw err;
}
if (opts['--cdn'] !== undefined || opts['--no-cdn'] !== undefined) {
output.error(`Toggling CF from ${getTitleName()} CLI is deprecated.`);
return 1;
}
const project = await getLinkedProject(output, client).then(result => {
if (result.status === 'linked') {
return result.project;
}
if (args.length !== 1) {
return null;
});
if (project && args.length !== 1) {
output.error(
`${getCommandName('domains add <domain>')} expects one argument`
`${getCommandName('domains add <domain>')} expects one argument.`
);
return 1;
} else if (!project && args.length !== 2) {
output.error(
`${getCommandName(
'domains add <domain> <project>'
)} expects two arguments.`
);
return 1;
}
const domainName = String(args[0]);
const parsedDomain = psl.parse(domainName);
if (parsedDomain.error) {
output.error(`The provided domain name ${param(domainName)} is invalid`);
return 1;
}
const { domain, subdomain } = parsedDomain;
if (!domain) {
output.error(`The provided domain '${param(domainName)}' is not valid.`);
return 1;
}
if (subdomain) {
output.error(
`You are adding '${domainName}' as a domain name containing a subdomain part '${subdomain}'\n` +
` This feature is deprecated, please add just the root domain: ${chalk.cyan(
`${getCommandName(`domain add ${domain}`)}`
)}`
);
return 1;
}
const projectName = project ? project.name : String(args[1]);
const addStamp = stamp();
const addedDomain = await addDomain(client, domainName, contextName);
if (addedDomain instanceof ERRORS.InvalidDomain) {
output.error(
`The provided domain name "${addedDomain.meta.domain}" is invalid`
);
return 1;
}
let aliasTarget = await addDomainToProject(client, projectName, domainName);
if (addedDomain instanceof ERRORS.DomainAlreadyExists) {
output.error(
`The domain ${chalk.underline(
addedDomain.meta.domain
)} is already registered by a different account.\n` +
` If this seems like a mistake, please contact us at support@vercel.com`
);
return 1;
if (aliasTarget instanceof Error) {
if (
aliasTarget instanceof ERRORS.APIError &&
aliasTarget.code === 'ALIAS_DOMAIN_EXIST' &&
aliasTarget.project &&
aliasTarget.project.id
) {
if (force) {
const removeResponse = await removeDomainFromProject(
client,
aliasTarget.project.id,
domainName
);
if (removeResponse instanceof Error) {
output.prettyError(removeResponse);
return 1;
}
aliasTarget = await addDomainToProject(client, projectName, domainName);
}
}
if (aliasTarget instanceof Error) {
output.prettyError(aliasTarget);
return 1;
}
}
// We can cast the information because we've just added the domain and it should be there
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
addedDomain.name
)} added correctly. ${addStamp()}\n`
domainName
)} added to project ${chalk.bold(projectName)}. ${addStamp()}`
);
if (!addedDomain.verified) {
if (isPublicSuffix(domainName)) {
output.log(
`The domain will automatically get assigned to your latest production deployment.`
);
return 0;
}
const domainResponse = await getDomain(client, contextName, domainName);
if (domainResponse instanceof Error) {
output.prettyError(domainResponse);
return 1;
}
const domainConfig = await getDomainConfig(client, contextName, domainName);
if (domainConfig.misconfigured) {
output.warn(
`The domain was added but it is not verified. To verify it, you should either:`
`This domain is not configured properly. To configure it you should either:`
);
output.print(
` ${chalk.gray(
'a)'
)} Change your domain nameservers to the following intended set: ${chalk.gray(
'[recommended]'
)}\n`
` ${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`
);
output.print(
`\n${formatNSTable(
addedDomain.intendedNameservers,
addedDomain.nameservers,
domainResponse.intendedNameservers,
domainResponse.nameservers,
{ extraSpace: ' ' }
)}\n\n`
);
output.print(
` ${chalk.gray(
'b)'
)} Add a DNS TXT record with the name and value shown below.\n`
);
output.print(
`\n${formatDnsTable([['_now', 'TXT', addedDomain.verificationRecord]], {
extraSpace: ' ',
})}\n\n`
);
output.print(
` We will run a verification for you and you will receive an email upon completion.\n`
);
output.print(
` If you want to force running a verification, you can run ${cmd(
`${getCommandName('domains verify <domain>')}`
)}\n`
output.print(' Read more: https://vercel.link/domain-configuration\n\n');
} else {
output.log(
`The domain will automatically get assigned to your latest production deployment.`
);
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
}
return 0;

View File

@@ -13,7 +13,6 @@ import transferIn from './transfer-in';
import inspect from './inspect';
import ls from './ls';
import rm from './rm';
import verify from './verify';
import move from './move';
import { getPkgName } from '../../util/pkg-name';
@@ -25,17 +24,17 @@ const help = () => {
ls Show all domains in a list
inspect [name] Displays information related to a domain
add [name] Add a new domain that you already own
add [name] [project] Add a new domain that you already own
rm [name] Remove a domain
buy [name] Buy a domain that you don't yet own
move [name] [destination] Move a domain to another user or team.
transfer-in [name] Transfer in a domain to Vercel
verify [name] Run a verification for a domain
${chalk.dim('Options:')}
-h, --help Output usage information
-d, --debug Debug mode [off]
-f, --force Force a domain on a project and remove it from an existing one
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
@@ -82,7 +81,6 @@ const COMMAND_CONFIG = {
move: ['move'],
rm: ['rm', 'remove'],
transferIn: ['transfer-in'],
verify: ['verify'],
};
export default async function main(ctx: NowContext) {
@@ -90,10 +88,9 @@ export default async function main(ctx: NowContext) {
try {
argv = getArgs(ctx.argv.slice(2), {
'--cdn': Boolean,
'--code': String,
'--no-cdn': Boolean,
'--yes': Boolean,
'--force': Boolean,
'--next': Number,
'-N': '--next',
});
@@ -122,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

@@ -4,13 +4,16 @@ import { NowContext } from '../../types';
import { Output } from '../../util/output';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import dnsTable from '../../util/format-dns-table';
import formatDate from '../../util/format-date';
import formatNSTable from '../../util/format-ns-table';
import getDomainByName from '../../util/domains/get-domain-by-name';
import getScope from '../../util/get-scope';
import formatTable from '../../util/format-table';
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
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';
type Options = {
'--debug': boolean;
@@ -70,7 +73,7 @@ export default async function inspect(
.then(res => (res instanceof Error ? null : res.price))
.catch(() => null),
]);
if (domain instanceof DomainNotFound) {
if (!domain || domain instanceof DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
@@ -88,6 +91,15 @@ export default async function inspect(
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(
inspectStamp()
@@ -129,6 +141,7 @@ export default async function inspect(
domain.txtVerifiedAt
)}\n`
);
if (renewalPrice && domain.boughtAt) {
output.print(
` ${chalk.cyan('Renewal Price')}\t\t$${renewalPrice} USD\n`
@@ -145,37 +158,57 @@ export default async function inspect(
);
output.print('\n');
output.print(chalk.bold(' Verification Record\n\n'));
output.print(
`${dnsTable([['_now', 'TXT', domain.verificationRecord]], {
extraSpace: ' ',
})}\n`
);
output.print('\n');
if (!domain.verified) {
output.warn(`This domain is not verified. To verify it you should either:`);
output.print(
` ${chalk.gray(
'a)'
)} Change your domain nameservers to the intended set detailed above. ${chalk.gray(
'[recommended]'
)}\n`
if (domainConfig.misconfigured) {
output.warn(
`This domain is not configured properly. To configure it you should either:`
);
output.print(
` ${chalk.gray(
'b)'
)} Add a DNS TXT record with the name and value shown above.\n\n`
` ${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(
` If you want to force running a verification, you can run ${getCommandName(
`domains verify <domain>`
)}\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'));
const table = formatTable(
['Project', 'Domains'],
['l', 'l'],
[
{
rows: projects.map(project => {
const name = project.name;
const domains = (project.alias || [])
.map(target => target.domain)
.filter(alias => alias.endsWith(domainName));
const cols = domains.length ? domains.join(', ') : '-';
return [name, cols];
}),
},
]
);
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
output.print(
table
.split('\n')
.map(line => ` ${line}`)
.join('\n')
);
output.print('\n\n');
}
return null;

View File

@@ -1,22 +1,37 @@
import ms from 'ms';
import psl from 'psl';
import chalk from 'chalk';
import table from 'text-table';
import plural from 'pluralize';
import Client from '../../util/client';
import getDomains from '../../util/domains/get-domains';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import { Output } from '../../util/output';
import { Domain, NowContext } from '../../types';
import formatTable from '../../util/format-table';
import { formatDateWithoutTime } from '../../util/format-date';
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 { 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,
@@ -60,16 +75,31 @@ export default async function ls(
return 1;
}
const { domains, pagination } = await getDomains(
client,
contextName,
nextTimestamp
);
const [{ domains, pagination }, projects] = await Promise.all([
getDomains(client, contextName),
getProjectsWithDomains(client),
] as const);
if (projects instanceof Error) {
output.prettyError(projects);
return 1;
}
const domainsInfo = createDomainsInfo(domains, projects);
output.log(
`Domains found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}\n`
`${plural(
'project domain',
domainsInfo.length,
true
)} found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}`
);
if (domains.length > 0) {
console.log(`${formatDomainsTable(domains)}\n`);
if (domainsInfo.length > 0) {
output.print(
formatDomainsTable(domainsInfo).replace(/^(.*)/gm, `${' '.repeat(3)}$1`)
);
output.print('\n\n');
}
if (pagination && pagination.count === 20) {
@@ -84,28 +114,92 @@ export default async function ls(
return 0;
}
function formatDomainsTable(domains: Domain[]) {
const current = new Date();
return table(
[
[
'',
chalk.gray('domain'),
chalk.gray('serviceType'),
chalk.gray('verified'),
chalk.gray('cdn'),
chalk.gray('age'),
].map(s => chalk.dim(s)),
...domains.map(domain => {
const url = chalk.bold(domain.name);
const time = chalk.gray(ms(current.getTime() - domain.createdAt));
return ['', url, domain.serviceType, domain.verified, true, time];
}),
],
{
align: ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(4),
stringLength: strlen,
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[][] = domainsInfo.map(info => {
const expiration = formatDateWithoutTime(info.expiresAt);
const age = info.createdAt ? ms(current - info.createdAt) : '-';
return [
info.domain,
info.projectName || '-',
info.dns,
expiration,
info.configured.toString(),
chalk.gray(age),
];
});
const table = formatTable(
['domain', 'project', 'dns', 'expiration', 'configured', 'age'],
['l', 'l', 'l', 'l', 'l', 'l'],
[{ rows }]
);
return table;
}

View File

@@ -15,6 +15,7 @@ import * as ERRORS from '../../util/errors-ts';
import param from '../../util/output/param';
import promptBool from '../../util/input/prompt-bool';
import setCustomSuffix from '../../util/domains/set-custom-suffix';
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
import { getCommandName } from '../../util/pkg-name';
type Options = {
@@ -67,7 +68,7 @@ export default async function rm(
}
const domain = await getDomainByName(client, contextName, domainName);
if (domain instanceof DomainNotFound) {
if (domain instanceof DomainNotFound || domain.name !== domainName) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
@@ -85,6 +86,18 @@ export default async function rm(
return 1;
}
const projects = await findProjectsForDomain(client, domain.name);
if (Array.isArray(projects) && projects.length > 0) {
output.warn(
`The domain is currently used by ${plural(
'project',
projects.length,
true
)}.`
);
}
const skipConfirmation = opts['--yes'];
if (
!skipConfirmation &&

View File

@@ -1,147 +0,0 @@
import chalk from 'chalk';
import { NowContext } from '../../types';
import { Output } from '../../util/output';
import * as ERRORS from '../../util/errors-ts';
import Client from '../../util/client';
import formatDnsTable from '../../util/format-dns-table';
import formatNSTable from '../../util/format-ns-table';
import getDomainByName from '../../util/domains/get-domain-by-name';
import getScope from '../../util/get-scope';
import stamp from '../../util/output/stamp';
import verifyDomain from '../../util/domains/verify-domain';
import { getCommandName } from '../../util/pkg-name';
type Options = {
'--debug': boolean;
};
export default async function verify(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
) {
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--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;
}
const [domainName] = args;
if (!domainName) {
output.error(
`${getCommandName(`domains verify <domain>`)} expects one argument`
);
return 1;
}
if (args.length !== 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('domains verify <domain>')}`
)}`
);
return 1;
}
const domain = await getDomainByName(client, contextName, domainName);
if (domain instanceof ERRORS.DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
);
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
return 1;
}
if (domain instanceof ERRORS.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 verifyStamp = stamp();
const result = await verifyDomain(client, domain.name, contextName);
if (result instanceof ERRORS.DomainVerificationFailed) {
const { nsVerification, txtVerification } = result.meta;
output.error(
`The domain ${
domain.name
} could not be verified due to the following reasons: ${verifyStamp()}\n`
);
output.print(
` ${chalk.gray(
'a)'
)} Nameservers verification failed since we see a different set than the intended set:`
);
output.print(
`\n${formatNSTable(
nsVerification.intendedNameservers,
nsVerification.nameservers,
{ extraSpace: ' ' }
)}\n\n`
);
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`
);
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
return 1;
}
if (result.nsVerifiedAt) {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
domain.name
)} was verified using nameservers. ${verifyStamp()}`
);
return 0;
}
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
domain.name
)} was verified using DNS TXT record. ${verifyStamp()}`
);
output.print(
` You can verify with nameservers too. Run ${getCommandName(
`domains inspect ${domain.name}`
)} to find out the intended set.\n`
);
return 0;
}

View File

@@ -124,7 +124,9 @@ export default async function main(ctx: NowContext) {
return link.exitCode;
} else if (link.status === 'not_linked') {
output.error(
`Your codebase isnt linked to a project on Vercel. Run ${getCommandName()} to link it.`
`Your codebase isnt linked to a project on Vercel. Run ${getCommandName(
'link'
)} to begin.`
);
return 1;
} else {

View File

@@ -17,6 +17,7 @@ export default new Map([
['help', 'help'],
['init', 'init'],
['inspect', 'inspect'],
['link', 'link'],
['list', 'list'],
['ln', 'alias'],
['log', 'logs'],

View File

@@ -0,0 +1,98 @@
import chalk from 'chalk';
import { NowContext } from '../../types';
import createOutput from '../../util/output';
import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import setupAndLink from '../../util/link/setup-and-link';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} link`)} [options]
${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
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
--confirm Confirm default options and skip questions
${chalk.dim('Examples:')}
${chalk.gray('')} Link current directory to a Vercel Project
${chalk.cyan(`$ ${getPkgName()} link`)}
${chalk.gray(
''
)} Link current directory with default options and skip questions
${chalk.cyan(`$ ${getPkgName()} link --confirm`)}
${chalk.gray('')} Link a specific directory to a Vercel Project
${chalk.cyan(`$ ${getPkgName()} link /usr/src/project`)}
`);
};
const COMMAND_CONFIG = {
// No subcommands yet
};
export default async function main(ctx: NowContext) {
let argv;
try {
argv = getArgs(ctx.argv.slice(2), {
'--confirm': Boolean,
});
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help();
return 2;
}
const debug = argv['--debug'];
const output = createOutput({ debug });
const { args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG);
const path = args[0] || process.cwd();
const autoConfirm = argv['--confirm'];
const forceDelete = true;
const link = await setupAndLink(
ctx,
output,
path,
forceDelete,
autoConfirm,
'success',
'Set up'
);
if (link.status === 'error') {
return link.exitCode;
} else if (link.status === 'not_linked') {
// User aborted project linking questions
return 0;
} else if (link.status === 'linked') {
// Successfully linked
return 0;
} else {
const err: never = link;
throw new Error('Unknown link status: ' + err);
}
}

View File

@@ -83,6 +83,16 @@ export type Domain = {
};
};
export type DomainConfig = {
configuredBy: null | 'CNAME' | 'A' | 'http';
misconfigured: boolean;
serviceType: 'zeit.world' | 'external' | 'na';
nameservers: string[];
cnames: string[] & { traceString?: string };
aValues: string[] & { traceString?: string };
dnssecEnabled?: boolean;
};
export type Cert = {
uid: string;
autoRenew: boolean;
@@ -217,6 +227,16 @@ export type DNSRecordData =
| SRVRecordData
| MXRecordData;
export interface ProjectAliasTarget {
createdAt?: number;
domain: string;
redirect?: string | null;
target: 'PRODUCTION' | 'STAGING';
configuredBy?: null | 'CNAME' | 'A';
configuredChangedAt?: null | number;
configuredChangeAttempts?: [number, number];
}
export interface Secret {
uid: string;
name: string;
@@ -258,6 +278,10 @@ export interface Project extends ProjectSettings {
accountId: string;
updatedAt: number;
createdAt: number;
alias?: ProjectAliasTarget[];
devCommand?: string | null;
framework?: string | null;
rootDirectory?: string | null;
latestDeployments?: Partial<Deployment>[];
}
@@ -277,3 +301,8 @@ export interface PaginationOptions {
count: number;
next?: number;
}
export type ProjectLinkResult =
| { status: 'linked'; org: Org; project: Project }
| { status: 'not_linked'; org: null; project: null }
| { status: 'error'; exitCode: number };

View File

@@ -1512,9 +1512,9 @@ export default class DevServer {
const { dest, headers, uri_args } = routeResult;
// Set any headers defined in the matched `route` config
Object.entries(headers).forEach(([name, value]) => {
for (const [name, value] of Object.entries(headers)) {
res.setHeader(name, value);
});
}
if (statusCode) {
// Set the `statusCode` as read-only so that `http-proxy`
@@ -1537,6 +1537,13 @@ export default class DevServer {
if (this.devProcessPort) {
const upstream = `http://localhost:${this.devProcessPort}`;
debug(`Proxying to frontend dev server: ${upstream}`);
// Add the Vercel platform proxy request headers
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
for (const [name, value] of Object.entries(headers)) {
req.headers[name] = value;
}
this.setResponseHeaders(res, nowRequestId);
const origUrl = url.parse(req.url || '/', true);
delete origUrl.search;
@@ -1692,6 +1699,13 @@ export default class DevServer {
(!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda'))
) {
debug('Proxying to frontend dev server');
// Add the Vercel platform proxy request headers
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
for (const [name, value] of Object.entries(headers)) {
req.headers[name] = value;
}
this.setResponseHeaders(res, nowRequestId);
return proxyPass(
req,

View File

@@ -0,0 +1,29 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { DomainConfig } from '../../types';
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`
);
return config;
} catch (error) {
if (error.status < 500) {
return error;
}
throw error;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,33 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { Domain } from '../../types';
type Response = {
domain: Domain;
};
export async function getDomain(
client: Client,
contextName: string,
domainName: string
) {
const cancelWait = wait(
`Fetching domain ${domainName} under ${chalk.bold(contextName)}`
);
try {
const { domain } = await client.fetch<Response>(
`/v4/domains/${domainName}`
);
return domain;
} catch (error) {
if (error.status < 500) {
return error;
}
throw error;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,3 @@
export function isPublicSuffix(domainName: string) {
return domainName.endsWith('.vercel.app') || domainName.endsWith('.now.sh');
}

View File

@@ -18,3 +18,18 @@ export default function formatDate(dateStrOrNumber?: number | string | null) {
`[in ${ms(diff)}]`
)}`;
}
export function formatDateWithoutTime(
dateStrOrNumber?: number | string | null
) {
if (!dateStrOrNumber) {
return chalk.gray('-');
}
const date = new Date(dateStrOrNumber);
const diff = date.getTime() - Date.now();
return diff < 0
? `${format(date, 'MMM DD YYYY')} ${chalk.gray(`[${ms(-diff)} ago]`)}`
: `${format(date, 'MMM DD YYYY')} ${chalk.gray(`[in ${ms(diff)}]`)}`;
}

View File

@@ -20,7 +20,7 @@ import strlen from './strlen';
export default function formatTable(
header: string[],
align: Array<'l' | 'r' | 'c' | '.'>,
blocks: { name: string; rows: string[][] }[],
blocks: { name?: string; rows: string[][] }[],
hsep = ' '
) {
const nrCols = header.length;
@@ -50,8 +50,10 @@ export default function formatTable(
for (let j = 0; j < nrCols; j++) {
const col = `${row[j]}`;
const al = align[j] || 'l';
const spaces = Math.max(padding[j] * 8 - strlen(col), 0);
const pad = ' '.repeat(spaces);
const repeat = padding[j] > 1 ? padding[j] * 8 - strlen(col) : 0;
const pad = repeat > 0 ? ' '.repeat(repeat) : '';
rows[i][j] = al === 'l' ? col + pad : pad + col;
}
}

View File

@@ -13,6 +13,6 @@ export default async function getTeamById(
team = await client.fetch<Team>(`/teams/${teamId}`);
teamCache.set(teamId, team);
}
return team;
}

View File

@@ -4,18 +4,15 @@ import chalk from 'chalk';
import { Output } from '../output';
import { Framework } from '@vercel/frameworks';
import { isSettingValue } from '../is-setting-value';
import { ProjectSettings } from '../../types';
export interface ProjectSettings {
export interface PartialProjectSettings {
buildCommand: string | null;
outputDirectory: string | null;
devCommand: string | null;
}
export interface ProjectSettingsWithFramework extends ProjectSettings {
framework: string | null;
}
const fields: { name: string; value: keyof ProjectSettings }[] = [
const fields: { name: string; value: keyof PartialProjectSettings }[] = [
{ name: 'Build Command', value: 'buildCommand' },
{ name: 'Output Directory', value: 'outputDirectory' },
{ name: 'Development Command', value: 'devCommand' },
@@ -23,13 +20,15 @@ const fields: { name: string; value: keyof ProjectSettings }[] = [
export default async function editProjectSettings(
output: Output,
projectSettings: ProjectSettings | null,
framework: Framework | null
) {
projectSettings: PartialProjectSettings | null,
framework: Framework | null,
autoConfirm: boolean
): Promise<ProjectSettings> {
// create new settings object, missing values will be filled with `null`
const settings: Partial<ProjectSettingsWithFramework> = {
...projectSettings,
};
const settings: ProjectSettings = Object.assign(
{ framework: null },
projectSettings
);
for (let field of fields) {
settings[field.value] =
@@ -64,7 +63,10 @@ export default async function editProjectSettings(
);
}
if (!(await confirm(`Want to override the settings?`, false))) {
if (
autoConfirm ||
!(await confirm(`Want to override the settings?`, false))
) {
return settings;
}
@@ -75,7 +77,7 @@ export default async function editProjectSettings(
choices: fields,
});
for (let setting of settingFields as (keyof ProjectSettings)[]) {
for (let setting of settingFields as (keyof PartialProjectSettings)[]) {
const field = fields.find(f => f.value === setting);
const name = `${Date.now()}`;
const answers = await inquirer.prompt({

View File

@@ -15,10 +15,6 @@ export default async function inputProject(
detectedProjectName: string,
autoConfirm: boolean
): Promise<Project | string> {
if (autoConfirm) {
return detectedProjectName;
}
const slugifiedName = slugify(detectedProjectName);
// attempt to auto-detect a project to link
@@ -42,6 +38,10 @@ export default async function inputProject(
} catch (error) {}
existingProjectSpinner();
if (autoConfirm) {
return detectedProject || detectedProjectName;
}
let shouldLinkProject;
if (!detectedProject) {

View File

@@ -0,0 +1,229 @@
import { join, basename } from 'path';
import chalk from 'chalk';
import { remove } from 'fs-extra';
import { NowContext, ProjectLinkResult } from '../../types';
import { NowConfig } from '../dev/types';
import { Output } from '../output';
import {
getLinkedProject,
linkFolderToProject,
getVercelDirectory,
} from '../projects/link';
import createProject from '../projects/create-project';
import updateProject from '../projects/update-project';
import Client from '../client';
import handleError from '../handle-error';
import confirm from '../input/confirm';
import toHumanPath from '../humanize-path';
import { isDirectory } from '../config/global-path';
import selectOrg from '../input/select-org';
import inputProject from '../input/input-project';
import { validateRootDirectory } from '../validate-paths';
import { inputRootDirectory } from '../input/input-root-directory';
import editProjectSettings from '../input/edit-project-settings';
import stamp from '../output/stamp';
import { EmojiLabel } from '../emoji';
//@ts-expect-error
import createDeploy from '../deploy/create-deploy';
//@ts-expect-error
import Now from '../index';
export default async function setupAndLink(
ctx: NowContext,
output: Output,
path: string,
forceDelete: boolean,
autoConfirm: boolean,
successEmoji: EmojiLabel,
setupMsg: string
): Promise<ProjectLinkResult> {
const {
authConfig: { token },
config,
} = ctx;
const { apiUrl } = ctx;
const debug = output.isDebugEnabled();
const client = new Client({
apiUrl,
token,
currentTeam: config.currentTeam,
debug,
});
const isFile = !isDirectory(path);
if (isFile) {
output.error(`Expected directory but found file: ${path}`);
return { status: 'error', exitCode: 1 };
}
const link = await getLinkedProject(output, client, path);
const isTTY = process.stdout.isTTY;
const quiet = !isTTY;
let rootDirectory: string | null = null;
let newProjectName: string;
let org;
if (!forceDelete && link.status === 'linked') {
return link;
}
if (forceDelete) {
const vercelDir = getVercelDirectory(path);
remove(vercelDir);
}
const shouldStartSetup =
autoConfirm ||
(await confirm(
`${setupMsg} ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
if (!shouldStartSetup) {
output.print(`Aborted. Project not set up.\n`);
return { status: 'not_linked', org: null, project: null };
}
try {
org = await selectOrg(
output,
'Which scope should contain your project?',
client,
config.currentTeam,
autoConfirm
);
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.prettyError(err);
return { status: 'error', exitCode: 1 };
}
throw err;
}
const detectedProjectName = basename(path);
const projectOrNewProjectName = await inputProject(
output,
client,
org,
detectedProjectName,
autoConfirm
);
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
} else {
const project = projectOrNewProjectName;
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug,
successEmoji
);
return { status: 'linked', org, project };
}
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
if (
rootDirectory &&
!(await validateRootDirectory(output, path, sourcePath, ''))
) {
return { status: 'error', exitCode: 1 };
}
let localConfig: NowConfig = {};
if (ctx.localConfig && !(ctx.localConfig instanceof Error)) {
localConfig = ctx.localConfig;
}
client.currentTeam = org.type === 'team' ? org.id : undefined;
const now = new Now({
apiUrl,
token,
debug,
currentTeam: client.currentTeam,
});
let deployment = null;
try {
const createArgs: any = {
name: newProjectName,
env: {},
build: { env: {} },
forceNew: undefined,
withCache: undefined,
quiet,
wantsPublic: localConfig.public,
isFile,
type: null,
nowConfig: localConfig,
regions: undefined,
meta: {},
deployStamp: stamp(),
target: undefined,
skipAutoDetectionConfirmation: false,
};
deployment = await createDeploy(
output,
now,
client.currentTeam || 'current user',
[sourcePath],
createArgs,
org,
!isFile,
path
);
if (
!deployment ||
!('code' in deployment) ||
deployment.code !== 'missing_project_settings'
) {
output.error('Failed to detect project settings. Please try again.');
if (output.isDebugEnabled()) {
console.log(deployment);
}
return { status: 'error', exitCode: 1 };
}
const { projectSettings, framework } = deployment;
if (rootDirectory) {
projectSettings.rootDirectory = rootDirectory;
}
const settings = await editProjectSettings(
output,
projectSettings,
framework,
autoConfirm
);
const project = await createProject(client, newProjectName);
await updateProject(client, project.id, settings);
Object.assign(project, settings);
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug,
successEmoji
);
return { status: 'linked', org, project };
} catch (err) {
handleError(err);
return { status: 'error', exitCode: 1 };
}
}

View File

@@ -0,0 +1,46 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { ProjectAliasTarget } from '../../types';
export async function addDomainToProject(
client: Client,
projectNameOrId: string,
domain: string
) {
const cancelWait = wait(
`Adding domain ${domain} to project ${chalk.bold(projectNameOrId)}`
);
try {
const response = await client.fetch<ProjectAliasTarget[]>(
`/projects/${encodeURIComponent(projectNameOrId)}/alias`,
{
method: 'POST',
body: JSON.stringify({
target: 'PRODUCTION',
domain,
}),
}
);
const aliasTarget: ProjectAliasTarget | undefined = response.find(
aliasTarget => aliasTarget.domain === domain
);
if (!aliasTarget) {
throw new Error(
`Unexpected error when adding the domain "${domain}" to project "${projectNameOrId}".`
);
}
return aliasTarget;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,13 @@
import Client from '../client';
import { Project } from '../../types';
export default async function createProject(
client: Client,
projectName: string
) {
const project = await client.fetch<Project>('/v1/projects', {
method: 'POST',
body: JSON.stringify({ name: projectName }),
});
return project;
}

View File

@@ -0,0 +1,46 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { Project } from '../../types';
import { URLSearchParams } from 'url';
export async function findProjectsForDomain(
client: Client,
domainName: string
): Promise<Project[] | Error> {
const cancelWait = wait(
`Searching project for domain ${chalk.bold(domainName)}`
);
try {
const limit = 50;
let result: Project[] = [];
const query = new URLSearchParams({
hasProductionDomains: '1',
limit: limit.toString(),
domain: domainName,
});
for (let i = 0; i < 1000; i++) {
const response = await client.fetch<Project[]>(`/v2/projects/?${query}`);
result.push(...response);
if (response.length !== limit) {
break;
}
const [latest] = response.sort((a, b) => b.updatedAt - a.updatedAt);
query.append('from', latest.updatedAt.toString());
}
return result;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,39 @@
import Client from '../client';
import wait from '../output/wait';
import { Project } from '../../types';
import { URLSearchParams } from 'url';
export async function getProjectsWithDomains(
client: Client
): Promise<Project[] | Error> {
const cancelWait = wait(`Fetching projects with domains`);
try {
const limit = 50;
let result: Project[] = [];
const query = new URLSearchParams({
hasProductionDomains: '1',
limit: limit.toString(),
});
for (let i = 0; i < 1000; i++) {
const response = await client.fetch<Project[]>(`/v2/projects/?${query}`);
result.push(...response);
const [latest] = response.sort((a, b) => b.updatedAt - a.updatedAt);
query.append('from', latest.updatedAt.toString());
if (response.length !== limit) break;
}
return result;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -8,10 +8,10 @@ import { ProjectNotFound } from '../errors-ts';
import getUser from '../get-user';
import getTeamById from '../get-team-by-id';
import { Output } from '../output';
import { Project } from '../../types';
import { Project, ProjectLinkResult } from '../../types';
import { Org, ProjectLink } from '../../types';
import chalk from 'chalk';
import { prependEmoji, emoji } from '../emoji';
import { prependEmoji, emoji, EmojiLabel } from '../emoji';
import AJV from 'ajv';
import { isDirectory } from '../config/global-path';
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
@@ -112,11 +112,7 @@ export async function getLinkedProject(
output: Output,
client: Client,
path?: string
): Promise<
| { status: 'linked'; org: Org; project: Project }
| { status: 'not_linked'; org: null; project: null }
| { status: 'error'; exitCode: number }
> {
): Promise<ProjectLinkResult> {
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
const shouldUseEnv = Boolean(VERCEL_ORG_ID && VERCEL_PROJECT_ID);
@@ -154,7 +150,7 @@ export async function getLinkedProject(
spinner();
throw new NowBuildError({
message: `Could not retrieve Project Settings. To link your Project, remove the ${outputCode(
'.vercel'
VERCEL_DIR
)} directory and deploy again.`,
code: 'PROJECT_UNAUTHORIZED',
link: 'https://vercel.link/cannot-load-project-settings',
@@ -196,7 +192,8 @@ export async function linkFolderToProject(
path: string,
projectLink: ProjectLink,
projectName: string,
orgSlug: string
orgSlug: string,
successEmoji: EmojiLabel = 'link'
) {
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
@@ -266,7 +263,7 @@ export async function linkFolderToProject(
)} (created ${VERCEL_DIR}${
isGitIgnoreUpdated ? ' and added it to .gitignore' : ''
})`,
emoji('link')
emoji(successEmoji)
) + '\n'
);
}

View File

@@ -0,0 +1,34 @@
import chalk from 'chalk';
import Client from '../client';
import wait from '../output/wait';
import { ProjectAliasTarget } from '../../types';
export async function removeDomainFromProject(
client: Client,
projectNameOrId: string,
domain: string
) {
const cancelWait = wait(
`Removing domain ${domain} from project ${chalk.bold(projectNameOrId)}`
);
try {
const response = await client.fetch<ProjectAliasTarget[]>(
`/projects/${encodeURIComponent(
projectNameOrId
)}/alias?domain=${encodeURIComponent(domain)}`,
{
method: 'DELETE',
}
);
return response;
} catch (err) {
if (err.status < 500) {
return err;
}
throw err;
} finally {
cancelWait();
}
}

View File

@@ -0,0 +1,24 @@
import Client from '../client';
import { ProjectSettings } from '../../types';
interface ProjectSettingsResponse extends ProjectSettings {
id: string;
name: string;
updatedAt: number;
createdAt: number;
}
export default async function updateProject(
client: Client,
prjNameOrId: string,
settings: ProjectSettings
) {
const res = await client.fetch<ProjectSettingsResponse>(
`/v2/projects/${encodeURIComponent(prjNameOrId)}`,
{
method: 'PATCH',
body: JSON.stringify(settings),
}
);
return res;
}

View File

@@ -8,7 +8,7 @@ export default async function responseError(
) {
let bodyError;
if (res.status >= 400 && res.status < 500) {
if (!res.ok) {
let body;
try {

View File

@@ -3,7 +3,11 @@ import { devRouter } from '../src/util/dev/router';
test('[dev-router] 301 redirection', async t => {
const routesConfig = [
{ src: '/redirect', status: 301, headers: { Location: 'https://vercel.com' } },
{
src: '/redirect',
status: 301,
headers: { Location: 'https://vercel.com' },
},
];
const result = await devRouter('/redirect', 'GET', routesConfig);

View File

@@ -0,0 +1 @@
.vercel

View File

@@ -0,0 +1,10 @@
package handler
import (
"fmt"
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Req Path: %s", r.URL.Path)
}

View File

@@ -0,0 +1,10 @@
package another
import (
"fmt"
"net/http"
)
func Another(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is another page")
}

View File

@@ -0,0 +1,10 @@
package handler
import (
"fmt"
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the index page")
}

View File

@@ -2,6 +2,7 @@ import ms from 'ms';
import os from 'os';
import fs from 'fs-extra';
import test from 'ava';
import { isIP } from 'net';
import { join, resolve, delimiter } from 'path';
import _execa from 'execa';
import fetch from 'node-fetch';
@@ -1600,3 +1601,48 @@ test(
await testPath(200, '/index.css', 'This is index.css');
})
);
test(
'[vercel dev] Should support `*.go` API serverless functions',
testFixtureStdio('go', async testPath => {
await testPath(200, `/api`, 'This is the index page');
await testPath(200, `/api/index`, 'This is the index page');
await testPath(200, `/api/index.go`, 'This is the index page');
await testPath(200, `/api/another`, 'This is another page');
await testPath(200, '/api/another.go', 'This is another page');
await testPath(200, `/api/foo`, 'Req Path: /api/foo');
await testPath(200, `/api/bar`, 'Req Path: /api/bar');
})
);
test(
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
testFixtureStdio('node-ts-node-target', async testPath => {
await testPath(200, `/api/subclass`, '{"ok":true}');
await testPath(
200,
`/api/array`,
'{"months":[1,2,3,4,5,6,7,8,9,10,11,12]}'
);
await testPath(200, `/api/dump`, (t, body, res, isDev) => {
const { host } = new URL(res.url);
const { env, headers } = JSON.parse(body);
// Test that the API endpoint receives the Vercel proxy request headers
t.is(headers['x-forwarded-host'], host);
t.is(headers['x-vercel-deployment-url'], host);
t.truthy(isIP(headers['x-real-ip']));
t.truthy(isIP(headers['x-forwarded-for']));
t.truthy(isIP(headers['x-vercel-forwarded-for']));
// Test that the API endpoint has the Vercel platform env vars defined.
t.regex(env.NOW_REGION, /^[a-z]{3}\d$/);
if (isDev) {
// Only dev is tested because in production these are opt-in.
t.is(env.VERCEL_URL, host);
t.is(env.VERCEL_REGION, 'dev1');
}
});
})
);

View File

@@ -1,15 +1,19 @@
{
"version": 2,
"name": "nodejs",
"builds": [
{ "src": "*.js", "use": "@now/node" },
{ "src": "statics/*", "use": "@now/static" }
],
"routes": [
{ "src": "/api/(.*)", "dest": "/api.js?topic=$1" },
{ "src": "/help.js", "dest": "/index.js" },
{ "src": "/help", "dest": "/help.js" },
{ "src": "/proxy_pass", "dest": "https://vercel.com" },
{ "src": "/redirect", "status": 301, "headers": { "Location": "https://vercel.com" }}
]
"version": 2,
"name": "nodejs",
"builds": [
{ "src": "*.js", "use": "@now/node" },
{ "src": "statics/*", "use": "@now/static" }
],
"routes": [
{ "src": "/api/(.*)", "dest": "/api.js?topic=$1" },
{ "src": "/help.js", "dest": "/index.js" },
{ "src": "/help", "dest": "/help.js" },
{ "src": "/proxy_pass", "dest": "https://vercel.com" },
{
"src": "/redirect",
"status": 301,
"headers": { "Location": "https://vercel.com" }
}
]
}

View File

@@ -503,6 +503,18 @@ CMD ["node", "index.js"]`,
'project-link': {
'package.json': JSON.stringify({}),
},
'project-link-confirm': {
'package.json': JSON.stringify({}),
},
'project-link-dev': {
'package.json': JSON.stringify({}),
},
'dev-proxy-headers-and-env': {
'package.json': JSON.stringify({}),
'server.js': `require('http').createServer((req, res) => {
res.end(JSON.stringify({ headers: req.headers, env: process.env }));
}).listen(process.env.PORT);`,
},
'project-root-directory': {
'src/index.html': '<h1>I am a website.</h1>',
'src/now.json': JSON.stringify({

View File

@@ -29,8 +29,19 @@ function execa(file, args, options) {
return _execa(file, args, options);
}
function fixture(name) {
const directory = path.join(__dirname, 'fixtures', 'integration', name);
const config = path.join(directory, 'project.json');
// We need to remove it, otherwise we can't re-use fixtures
if (fs.existsSync(config)) {
fs.unlinkSync(config);
}
return directory;
}
const binaryPath = path.resolve(__dirname, `../scripts/start.js`);
const fixture = name => path.join(__dirname, 'fixtures', 'integration', name);
const example = name =>
path.join(__dirname, '..', '..', '..', 'examples', name);
const deployHelpMessage = `${logo} vercel [options] <command | path>`;
@@ -95,7 +106,19 @@ function fetchTokenInformation(token, retries = 3) {
}
function formatOutput({ stderr, stdout }) {
return `Received:\n"${stderr}"\n"${stdout}"`;
return `
-----
Stderr:
${stderr}
-----
Stdout:
${stdout}
-----
`;
}
// AVA's `t.context` can only be set before the tests,
@@ -279,10 +302,6 @@ test('login', async t => {
...defaultArgs,
]);
console.log(loginOutput.stderr);
console.log(loginOutput.stdout);
console.log(loginOutput.exitCode);
t.is(loginOutput.exitCode, 0, formatOutput(loginOutput));
t.regex(
loginOutput.stdout,
@@ -1026,16 +1045,35 @@ test('list the payment methods', async t => {
});
test('domains inspect', async t => {
const domainName = `inspect-${contextName}.org`;
const domainName = `inspect-${contextName}-${Math.random()
.toString()
.slice(2, 8)}.org`;
const addRes = await execa(
binaryPath,
[`domains`, `add`, domainName, ...defaultArgs],
{ reject: false }
);
t.is(addRes.exitCode, 0);
const directory = fixture('static-multiple-files');
const projectName = Math.random().toString().slice(2);
const { stderr, exitCode } = await execa(
const output = await execute([
directory,
`-V`,
`2`,
`--name=${projectName}`,
'--confirm',
'--public',
]);
t.is(output.exitCode, 0, formatOutput(output));
{
// Add a domain that can be inspected
const result = await execa(
binaryPath,
[`domains`, `add`, domainName, projectName, ...defaultArgs],
{ reject: false }
);
t.is(result.exitCode, 0, formatOutput(result));
}
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['domains', 'inspect', domainName, ...defaultArgs],
{
@@ -1043,18 +1081,30 @@ test('domains inspect', async t => {
}
);
const rmRes = await execa(
binaryPath,
[`domains`, `rm`, domainName, ...defaultArgs],
{ reject: false, input: 'y' }
);
t.is(rmRes.exitCode, 0);
t.is(exitCode, 0);
t.true(!stderr.includes(`Renewal Price`));
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
{
// Remove the domain again
const result = await execa(
binaryPath,
[`domains`, `rm`, domainName, ...defaultArgs],
{ reject: false, input: 'y' }
);
t.is(result.exitCode, 0, formatOutput(result));
}
});
test('try to purchase a domain', async t => {
if (process.env.VERCEL_TOKEN || process.env.NOW_TOKEN) {
console.log(
'Skipping test `try to purchase a domain` because a personal VERCEL_TOKEN was provided.'
);
t.pass();
return;
}
const { stderr, stdout, exitCode } = await execa(
binaryPath,
['domains', 'buy', `${session}-test.org`, ...defaultArgs],
@@ -1835,6 +1885,7 @@ test('deploying a file should not show prompts and display deprecation', async t
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], 'files');
console.log('static-single-file/first.png: stdout', stdout);
// Send a test request to the deployment
const response = await fetch(href);
@@ -2500,23 +2551,10 @@ test('deploy a Lambda with a specific runtime', async t => {
t.is(build.use, 'vercel-php@0.1.0', JSON.stringify(build, null, 2));
});
test('fail to deploy a Lambda with a specific runtime but without a locked version', async t => {
const directory = fixture('lambda-with-invalid-runtime');
const output = await execute([directory, '--confirm']);
test('fail to add a domain without a project', async t => {
const output = await execute(['domains', 'add', 'my-domain.now.sh']);
t.is(output.exitCode, 1, formatOutput(output));
t.regex(
output.stderr,
/Function Runtimes must have a valid version/gim,
formatOutput(output)
);
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, /expects two arguments/gm, formatOutput(output));
});
test('change user', async t => {
@@ -2546,6 +2584,68 @@ test('change user', async t => {
t.not(prevUser, nextUser, JSON.stringify({ prevUser, nextUser }));
});
test('assign a domain to a project', async t => {
const domain = `project-domain.${contextName}.now.sh`;
const directory = fixture('static-deployment');
const deploymentOutput = await execute([directory, '--public', '--confirm']);
t.is(deploymentOutput.exitCode, 0, formatOutput(deploymentOutput));
const host = deploymentOutput.stdout.trim().replace('https://', '');
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(typeof deployment.name, 'string', JSON.stringify(deployment, null, 2));
const project = deployment.name;
const output = await execute(['domains', 'add', domain, project, '--force']);
t.is(output.exitCode, 0, formatOutput(output));
const removeResponse = await execute(['rm', project, '-y']);
t.is(removeResponse.exitCode, 0, formatOutput(removeResponse));
});
test('list project domains', async t => {
const domain = `project-domain.${contextName}.now.sh`;
const directory = fixture('static-deployment');
const deploymentOutput = await execute([directory, '--public', '--confirm']);
t.is(deploymentOutput.exitCode, 0, formatOutput(deploymentOutput));
const host = deploymentOutput.stdout.trim().replace('https://', '');
const deployment = await apiFetch(
`/v10/now/deployments/unknown?url=${host}`
).then(resp => resp.json());
t.is(typeof deployment.name, 'string', JSON.stringify(deployment, null, 2));
const project = deployment.name;
const addOutput = await execute([
'domains',
'add',
domain,
project,
'--force',
]);
t.is(addOutput.exitCode, 0, formatOutput(addOutput));
const output = await execute(['domains', 'ls']);
t.is(output.exitCode, 0, formatOutput(output));
t.regex(output.stderr, new RegExp(domain), formatOutput(output));
t.regex(output.stderr, new RegExp(project), formatOutput(output));
const removeResponse = await execute(['rm', project, '-y']);
t.is(removeResponse.exitCode, 0, formatOutput(removeResponse));
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory, '--confirm']);
t.is(output.exitCode, 0, formatOutput(output));
});
test('should show prompts to set up project', async t => {
const directory = fixture('project-link');
const projectName = `project-link-${
@@ -2595,7 +2695,9 @@ test('should show prompts to set up project', async t => {
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Build Command?`)
);
now.stdin.write(`mkdir o && echo '<h1>custom hello</h1>' > o/index.html\n`);
now.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Output Directory?`)
@@ -3169,3 +3271,294 @@ test('reject deploying with wrong team .vercel config', async t => {
formatOutput({ stderr, stdout })
);
});
test('[vc link] should show prompts to set up project', async t => {
const dir = fixture('project-link');
const projectName = `project-link-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const vc = execa(binaryPath, ['link', ...defaultArgs], { cwd: dir });
await waitForPrompt(vc, chunk => /Set up [^?]+\?/.test(chunk));
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Which scope should contain your project?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
vc.stdin.write('no\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Whats your projects name?')
);
vc.stdin.write(`${projectName}\n`);
await waitForPrompt(vc, chunk =>
chunk.includes('In which directory is your code located?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Want to override the settings?')
);
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
vc.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Build Command?`)
);
vc.stdin.write(`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`);
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Output Directory?`)
);
vc.stdin.write(`o\n`);
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Development Command?`)
);
vc.stdin.write(`\n`);
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
const output = await vc;
// Ensure the exit code is right
t.is(output.exitCode, 0, formatOutput(output));
// Ensure .gitignore is created
t.is((await readFile(path.join(dir, '.gitignore'))).toString(), '.vercel');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc link --confirm] should not show prompts and autolink', async t => {
const dir = fixture('project-link-confirm');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--confirm', ...defaultArgs],
{ cwd: dir, reject: false }
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
t.is((await readFile(path.join(dir, '.gitignore'))).toString(), '.vercel');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
});
test('[vc dev] should show prompts to set up project', async t => {
const dir = fixture('project-link-dev');
const port = 58352;
const projectName = `project-link-dev-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
cwd: dir,
});
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Which scope should contain your project?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Link to existing project?')
);
dev.stdin.write('no\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Whats your projects name?')
);
dev.stdin.write(`${projectName}\n`);
await waitForPrompt(dev, chunk =>
chunk.includes('In which directory is your code located?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Want to override the settings?')
);
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
dev.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Build Command?`)
);
dev.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Output Directory?`)
);
dev.stdin.write(`o\n`);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Development Command?`)
);
dev.stdin.write(`\n`);
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
// Ensure .gitignore is created
t.is((await readFile(path.join(dir, '.gitignore'))).toString(), '.vercel');
// Ensure .vercel/project.json and .vercel/README.txt are created
t.is(
await exists(path.join(dir, '.vercel', 'project.json')),
true,
'project.json should be created'
);
t.is(
await exists(path.join(dir, '.vercel', 'README.txt')),
true,
'README.txt should be created'
);
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works
try {
const response = await fetch(`http://localhost:${port}/`);
const text = await response.text();
t.is(text.includes('<h1>custom hello</h1>'), true, text);
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});
test('[vc dev] should send the platform proxy request headers to frontend dev server ', async t => {
const dir = fixture('dev-proxy-headers-and-env');
const port = 58353;
const projectName = `dev-proxy-headers-and-env-${
Math.random().toString(36).split('.')[1]
}`;
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
cwd: dir,
});
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Which scope should contain your project?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Link to existing project?')
);
dev.stdin.write('no\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Whats your projects name?')
);
dev.stdin.write(`${projectName}\n`);
await waitForPrompt(dev, chunk =>
chunk.includes('In which directory is your code located?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Want to override the settings?')
);
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
dev.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Build Command?`)
);
dev.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Output Directory?`)
);
dev.stdin.write(`o\n`);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Development Command?`)
);
dev.stdin.write(`node server.js\n`);
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works
try {
const response = await fetch(`http://localhost:${port}/`);
const body = await response.json();
t.is(body.headers['x-vercel-deployment-url'], `localhost:${port}`);
t.is(body.env.NOW_REGION, 'dev1');
} finally {
process.kill(dev.pid, 'SIGTERM');
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "8.2.1",
"version": "8.2.2-canary.3",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -38,7 +38,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "2.4.2",
"@vercel/build-utils": "2.4.3-canary.2",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -112,6 +112,7 @@ export interface NowConfig extends LegacyNowConfig {
[fileNameSymbol]?: string;
name?: string;
version?: number;
public?: boolean;
env?: Dictionary<string>;
build?: {
env?: Dictionary<string>;

View File

@@ -1,4 +1,13 @@
#!/bin/bash
set -euo pipefail
# Start fresh
rm -rf dist
# Build with `ncc`
ncc build index.ts -e @vercel/build-utils -e @now/build-utils -o dist
ncc build install.ts -e @vercel/build-utils -e @now/build-utils -o dist/install
# Move `install.js` to dist
mv dist/install/index.js dist/install.js
rm -rf dist/install

View File

@@ -3,14 +3,10 @@ import execa from 'execa';
import fetch from 'node-fetch';
import { mkdirp, pathExists } from 'fs-extra';
import { dirname, join } from 'path';
import { homedir } from 'os';
import buildUtils from './build-utils';
import stringArgv from 'string-argv';
const { debug } = buildUtils;
const archMap = new Map([
['x64', 'amd64'],
['x86', '386'],
]);
const archMap = new Map([['x64', 'amd64'], ['x86', '386']]);
const platformMap = new Map([['win32', 'windows']]);
// Location where the `go` binary will be installed after `postinstall`
@@ -130,50 +126,35 @@ export async function downloadGo(
platform = process.platform,
arch = process.arch
) {
// Check default `Go` in user machine
const isUserGo = await pathExists(join(homedir(), 'go'));
// Check if `go` is already installed in user's `$PATH`
const { failed, stdout } = await execa('go', ['version'], { reject: false });
// If we found GOPATH in ENV, or default `Go` path exists
// asssume that user have `Go` installed
if (isUserGo || process.env.GOPATH !== undefined) {
const { stdout } = await execa('go', ['version']);
if (parseInt(stdout.split('.')[1]) >= 11) {
return createGo(dir, platform, arch);
}
throw new Error(
`Your current ${stdout} doesn't support Go Modules. Please update.`
);
} else {
// Check `Go` bin in builder CWD
const isGoExist = await pathExists(join(dir, 'bin'));
if (!isGoExist) {
debug(
'Installing `go` v%s to %o for %s %s',
version,
dir,
platform,
arch
);
const url = getGoUrl(version, platform, arch);
debug('Downloading `go` URL: %o', url);
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to download: ${url} (${res.status})`);
}
// TODO: use a zip extractor when `ext === "zip"`
await mkdirp(dir);
await new Promise((resolve, reject) => {
res.body
.on('error', reject)
.pipe(tar.extract({ cwd: dir, strip: 1 }))
.on('error', reject)
.on('finish', resolve);
});
}
if (!failed && parseInt(stdout.split('.')[1]) >= 11) {
debug('Using system installed version of `go`: %o', stdout.trim());
return createGo(dir, platform, arch);
}
// Check `go` bin in builder CWD
const isGoExist = await pathExists(join(dir, 'bin'));
if (!isGoExist) {
debug('Installing `go` v%s to %o for %s %s', version, dir, platform, arch);
const url = getGoUrl(version, platform, arch);
debug('Downloading `go` URL: %o', url);
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to download: ${url} (${res.status})`);
}
// TODO: use a zip extractor when `ext === "zip"`
await mkdirp(dir);
await new Promise((resolve, reject) => {
res.body
.on('error', reject)
.pipe(tar.extract({ cwd: dir, strip: 1 }))
.on('error', reject)
.on('finish', resolve);
});
}
return createGo(dir, platform, arch);
}

View File

@@ -1,8 +1,25 @@
import { join, sep, dirname, basename, normalize } from 'path';
import { readFile, writeFile, pathExists, move } from 'fs-extra';
import { homedir } from 'os';
import execa from 'execa';
import { BuildOptions, Meta, Files, shouldServe } from '@vercel/build-utils';
import retry from 'async-retry';
import { homedir, tmpdir } from 'os';
import { spawn } from 'child_process';
import { Readable } from 'stream';
import once from '@tootallnate/once';
import { join, dirname, basename, normalize, sep } from 'path';
import {
readFile,
writeFile,
pathExists,
mkdirp,
move,
remove,
} from 'fs-extra';
import {
BuildOptions,
Meta,
Files,
StartDevServerOptions,
StartDevServerResult,
} from '@vercel/build-utils';
import buildUtils from './build-utils';
const {
@@ -10,12 +27,17 @@ const {
download,
createLambda,
getWriteableDirectory,
shouldServe,
debug,
} = buildUtils;
const TMP = tmpdir();
import { createGo, getAnalyzedEntrypoint, OUT_EXTENSION } from './go-helpers';
const handlerFileName = `handler${OUT_EXTENSION}`;
export { shouldServe };
interface Analyzed {
found?: boolean;
packageName: string;
@@ -23,16 +45,22 @@ interface Analyzed {
watch: string[];
}
interface PortInfo {
port: number;
}
// Initialize private git repo for Go Modules
async function initPrivateGit(credentials: string) {
const gitCredentialsPath = join(homedir(), '.git-credentials');
await execa('git', [
'config',
'--global',
'credential.helper',
`store --file ${join(homedir(), '.git-credentials')}`,
`store --file ${gitCredentialsPath}`,
]);
await writeFile(join(homedir(), '.git-credentials'), credentials);
await writeFile(gitCredentialsPath, credentials);
}
/**
@@ -435,4 +463,160 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go
};
}
export { shouldServe };
function isPortInfo(v: any): v is PortInfo {
return v && typeof v.port === 'number';
}
function isReadable(v: any): v is Readable {
return v && v.readable === true;
}
async function copyEntrypoint(entrypoint: string, dest: string): Promise<void> {
const data = await readFile(entrypoint, 'utf8');
// Modify package to `package main`
const patched = data.replace(/\bpackage\W+\S+\b/, 'package main');
await writeFile(join(dest, 'entrypoint.go'), patched);
}
async function copyDevServer(
functionName: string,
dest: string
): Promise<void> {
const data = await readFile(join(__dirname, 'dev-server.go'), 'utf8');
// Populate the handler function name
const patched = data.replace('__HANDLER_FUNC_NAME', functionName);
await writeFile(join(dest, 'vercel-dev-server-main.go'), patched);
}
export async function startDevServer(
opts: StartDevServerOptions
): Promise<StartDevServerResult> {
const { entrypoint, workPath, meta = {} } = opts;
const { devCacheDir = join(workPath, '.vercel', 'cache') } = meta;
const entrypointDir = dirname(entrypoint);
// For some reason, if `entrypoint` is a path segment (filename contains `[]`
// brackets) then the `.go` suffix on the entrypoint is missing. Fix that here…
let entrypointWithExt = entrypoint;
if (!entrypoint.endsWith('.go')) {
entrypointWithExt += '.go';
}
const tmp = join(devCacheDir, 'go', Math.random().toString(32).substring(2));
const tmpPackage = join(tmp, entrypointDir);
await mkdirp(tmpPackage);
let goModAbsPathDir = '';
if (await pathExists(join(workPath, 'go.mod'))) {
goModAbsPathDir = workPath;
}
const analyzedRaw = await getAnalyzedEntrypoint(
entrypointWithExt,
goModAbsPathDir
);
if (!analyzedRaw) {
throw new Error(
`Could not find an exported function in "${entrypointWithExt}"
Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
);
}
const analyzed: Analyzed = JSON.parse(analyzedRaw);
await Promise.all([
copyEntrypoint(entrypointWithExt, tmpPackage),
copyDevServer(analyzed.functionName, tmpPackage),
]);
const portFile = join(
TMP,
`vercel-dev-port-${Math.random().toString(32).substring(2)}`
);
const env: typeof process.env = {
...process.env,
...meta.env,
VERCEL_DEV_PORT_FILE: portFile,
};
const tmpRelative = `.${sep}${entrypointDir}`;
const child = spawn('go', ['run', tmpRelative], {
cwd: tmp,
env,
stdio: ['ignore', 'inherit', 'inherit', 'pipe'],
});
child.once('exit', () => {
retry(() => remove(tmp)).catch((err: Error) => {
console.error('Could not delete tmp directory: %j: %s', tmp, err);
});
});
const portPipe = child.stdio[3];
if (!isReadable(portPipe)) {
throw new Error('File descriptor 3 is not readable');
}
// `dev-server.go` writes the ephemeral port number to FD 3 to be consumed here
const onPort = new Promise<PortInfo>(resolve => {
portPipe.setEncoding('utf8');
portPipe.once('data', d => {
resolve({ port: Number(d) });
});
});
const onPortFile = waitForPortFile(portFile);
const onExit = once.spread<[number, string | null]>(child, 'exit');
const result = await Promise.race([onPort, onPortFile, onExit]);
onExit.cancel();
onPortFile.cancel();
if (isPortInfo(result)) {
return {
port: result.port,
pid: child.pid,
};
} else if (Array.isArray(result)) {
// Got "exit" event from child process
throw new Error(
`Failed to start dev server for "${entrypointWithExt}" (code=${result[0]}, signal=${result[1]})`
);
} else {
throw new Error(`Unexpected result type: ${typeof result}`);
}
}
export interface CancelablePromise<T> extends Promise<T> {
cancel: () => void;
}
function waitForPortFile(portFile: string) {
const opts = { portFile, canceled: false };
const promise = waitForPortFile_(opts) as CancelablePromise<PortInfo | void>;
promise.cancel = () => {
opts.canceled = true;
};
return promise;
}
async function waitForPortFile_(opts: {
portFile: string;
canceled: boolean;
}): Promise<PortInfo | void> {
while (!opts.canceled) {
await new Promise(resolve => setTimeout(resolve, 100));
try {
const port = Number(await readFile(opts.portFile, 'ascii'));
retry(() => remove(opts.portFile)).catch((err: Error) => {
console.error('Could not delete port file: %j: %s', opts.portFile, err);
});
return { port };
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
}
}

View File

@@ -1,8 +1,8 @@
package main
import (
"net/http"
vc "github.com/vercel/go-bridge/go/bridge"
"net/http"
)
func main() {

View File

@@ -1,12 +1,12 @@
package main
import (
"net/http"
"__NOW_HANDLER_PACKAGE_NAME"
"__NOW_HANDLER_PACKAGE_NAME"
"net/http"
vc "github.com/vercel/go-bridge/go/bridge"
vc "github.com/vercel/go-bridge/go/bridge"
)
func main() {
vc.Start(http.HandlerFunc(__NOW_HANDLER_FUNC_NAME))
vc.Start(http.HandlerFunc(__NOW_HANDLER_FUNC_NAME))
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "1.1.4",
"version": "1.1.5-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
@@ -19,6 +19,8 @@
"dist"
],
"devDependencies": {
"@tootallnate/once": "1.1.2",
"@types/async-retry": "1.4.2",
"@types/execa": "^0.9.0",
"@types/fs-extra": "^5.0.5",
"@types/node-fetch": "^2.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "2.6.13",
"version": "2.6.14-canary.1",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",

View File

@@ -7,6 +7,7 @@ const {
getLambdaOptionsFromFunction,
getNodeVersion,
getSpawnOptions,
getScriptName,
glob,
runNpmInstall,
runPackageJsonScript,
@@ -35,13 +36,7 @@ import {
import { nodeFileTrace, NodeFileTraceReasons } from '@zeit/node-file-trace';
import { ChildProcess, fork } from 'child_process';
import escapeStringRegexp from 'escape-string-regexp';
import {
lstat,
pathExists,
readFile,
unlink as unlinkFile,
writeFile,
} from 'fs-extra';
import { lstat, pathExists, readFile, remove, writeFile } from 'fs-extra';
import os from 'os';
import path from 'path';
import resolveFrom from 'resolve-from';
@@ -108,13 +103,7 @@ const MAX_AGE_ONE_YEAR = 31536000;
/**
* Read package.json from files
*/
async function readPackageJson(
entryPath: string
): Promise<{
scripts?: { [key: string]: string };
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
}> {
async function readPackageJson(entryPath: string): Promise<PackageJson> {
const packagePath = path.join(entryPath, 'package.json');
try {
@@ -329,49 +318,38 @@ export const build = async ({
}
const isLegacy = nextVersionRange && isLegacyNext(nextVersionRange);
let shouldRunScript = 'now-build';
debug(`MODE: ${isLegacy ? 'legacy' : 'serverless'}`);
if (isLegacy) {
try {
await unlinkFile(path.join(entryPath, 'yarn.lock'));
} catch (err) {
debug('no yarn.lock removed');
}
try {
await unlinkFile(path.join(entryPath, 'package-lock.json'));
} catch (err) {
debug('no package-lock.json removed');
}
console.warn(
"WARNING: your application is being deployed in @vercel/next's legacy mode. http://err.sh/vercel/vercel/now-next-legacy-mode"
);
await Promise.all([
remove(path.join(entryPath, 'yarn.lock')),
remove(path.join(entryPath, 'package-lock.json')),
]);
debug('Normalizing package.json');
const packageJson = normalizePackageJson(pkg);
debug('Normalized package.json result: ', packageJson);
await writePackageJson(entryPath, packageJson);
} else if (pkg.scripts && pkg.scripts['now-build']) {
debug('Found user `now-build` script');
shouldRunScript = 'now-build';
} else if (pkg.scripts && pkg.scripts['build']) {
debug('Found user `build` script');
shouldRunScript = 'build';
} else if (!pkg.scripts || !pkg.scripts['now-build']) {
debug(
}
const buildScriptName = getScriptName(pkg, [
'vercel-build',
'now-build',
'build',
]);
let { buildCommand } = config;
if (!buildScriptName && !buildCommand) {
console.log(
'Your application is being built using `next build`. ' +
'If you need to define a different build step, please create a `now-build` script in your `package.json` ' +
'(e.g. `{ "scripts": { "now-build": "npm run prepare && next build" } }`).'
'If you need to define a different build step, please create a `vercel-build` script in your `package.json` ' +
'(e.g. `{ "scripts": { "vercel-build": "npm run prepare && next build" } }`).'
);
pkg.scripts = {
'now-build': 'next build',
...(pkg.scripts || {}),
};
shouldRunScript = 'now-build';
await writePackageJson(entryPath, pkg);
buildCommand = 'next build';
}
if (process.env.NPM_AUTH_TOKEN) {
@@ -398,12 +376,11 @@ export const build = async ({
await createServerlessConfig(workPath, entryPath, nextVersion);
}
debug('Running user script...');
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
const env: { [key: string]: string | undefined } = { ...spawnOpts.env };
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
if (config.buildCommand) {
if (buildCommand) {
// Add `node_modules/.bin` to PATH
const nodeBinPath = await getNodeBinPath({ cwd: entryPath });
env.PATH = `${nodeBinPath}${path.delimiter}${env.PATH}`;
@@ -412,14 +389,14 @@ export const build = async ({
`Added "${nodeBinPath}" to PATH env because a build command was used.`
);
console.log(`Running "${config.buildCommand}"`);
await execCommand(config.buildCommand, {
console.log(`Running "${buildCommand}"`);
await execCommand(buildCommand, {
...spawnOpts,
cwd: entryPath,
env,
});
} else {
await runPackageJsonScript(entryPath, shouldRunScript, {
} else if (buildScriptName) {
await runPackageJsonScript(entryPath, buildScriptName, {
...spawnOpts,
env,
});
@@ -667,7 +644,7 @@ export const build = async ({
}
if (process.env.NPM_AUTH_TOKEN) {
await unlinkFile(path.join(entryPath, '.npmrc'));
await remove(path.join(entryPath, '.npmrc'));
}
const pageLambdaRoutes: Route[] = [];
@@ -838,7 +815,7 @@ export const build = async ({
// > 1 because _error is a lambda but isn't used if a static 404 is available
const pageKeys = Object.keys(pages);
const hasLambdas = !static404Page || pageKeys.length > 1;
let hasLambdas = !static404Page || pageKeys.length > 1;
if (pageKeys.length === 0) {
const nextConfig = await getNextConfig(workPath, entryPath);
@@ -878,14 +855,38 @@ export const build = async ({
[filePath: string]: FileFsRef;
};
let canUsePreviewMode = false;
const isApiPage = (page: string) =>
page.replace(/\\/g, '/').match(/serverless\/pages\/api/);
const canUsePreviewMode = Object.keys(pages).some(page =>
isApiPage(pages[page].fsPath)
);
let pseudoLayerBytes = 0;
let apiPseudoLayerBytes = 0;
const pseudoLayers: PseudoLayer[] = [];
const apiPseudoLayers: PseudoLayer[] = [];
const nonLambdaSsgPages = new Set<string>();
const isApiPage = (page: string) =>
page.replace(/\\/g, '/').match(/serverless\/pages\/api/);
const onPrerenderRouteInitial = (routeKey: string) => {
// Get the route file as it'd be mounted in the builder output
const pr = prerenderManifest.staticRoutes[routeKey];
const { initialRevalidate, srcRoute } = pr;
const route = srcRoute || routeKey;
if (
initialRevalidate === false &&
!canUsePreviewMode &&
!prerenderManifest.fallbackRoutes[route] &&
!prerenderManifest.legacyBlockingRoutes[route]
) {
nonLambdaSsgPages.add(route);
}
};
Object.keys(prerenderManifest.staticRoutes).forEach(route =>
onPrerenderRouteInitial(route)
);
const tracedFiles: {
[filePath: string]: FileFsRef;
@@ -895,22 +896,31 @@ export const build = async ({
} = {};
if (requiresTracing) {
const tracingLabel =
'Traced Next.js serverless functions for external files in';
console.time(tracingLabel);
const apiPages: string[] = [];
const nonApiPages: string[] = [];
const allPagePaths = Object.keys(pages).map(page => pages[page].fsPath);
const pageKeys = Object.keys(pages);
for (const page of allPagePaths) {
if (isApiPage(page)) {
apiPages.push(page);
canUsePreviewMode = true;
} else {
nonApiPages.push(page);
for (const page of pageKeys) {
const pagePath = pages[page].fsPath;
const route = `/${page.replace(/\.js$/, '')}`;
if (route === '/_error' && static404Page) continue;
if (isApiPage(pagePath)) {
apiPages.push(pagePath);
} else if (!nonLambdaSsgPages.has(route)) {
nonApiPages.push(pagePath);
}
}
hasLambdas =
!static404Page || apiPages.length > 0 || nonApiPages.length > 0;
const tracingLabel =
'Traced Next.js serverless functions for external files in';
if (hasLambdas) {
console.time(tracingLabel);
}
const {
fileList: apiFileList,
@@ -960,10 +970,16 @@ export const build = async ({
await Promise.all(
apiFileList.map(collectTracedFiles(apiReasons, apiTracedFiles))
);
console.timeEnd(tracingLabel);
if (hasLambdas) {
console.timeEnd(tracingLabel);
}
const zippingLabel = 'Compressed shared serverless function files';
console.time(zippingLabel);
if (hasLambdas) {
console.time(zippingLabel);
}
let pseudoLayer;
let apiPseudoLayer;
@@ -978,7 +994,9 @@ export const build = async ({
pseudoLayers.push(pseudoLayer);
apiPseudoLayers.push(apiPseudoLayer);
console.timeEnd(zippingLabel);
if (hasLambdas) {
console.timeEnd(zippingLabel);
}
} else {
// An optional assets folder that is placed alongside every page
// entrypoint.
@@ -1063,6 +1081,11 @@ export const build = async ({
if (routeIsDynamic) {
dynamicPages.push(normalizePage(pathname));
}
if (nonLambdaSsgPages.has(`/${pathname}`)) {
continue;
}
const outputName = path.join('/', entryDirectory, pathname);
const lambdaGroups = routeIsApi ? apiLambdaGroups : pageLambdaGroups;
@@ -1472,13 +1495,6 @@ export const build = async ({
lambda = lambdas[outputSrcPathPage];
}
if (lambda == null) {
throw new NowBuildError({
code: 'NEXT_MISSING_LAMBDA',
message: `Unable to find lambda for route: ${routeFileNoExt}`,
});
}
if (initialRevalidate === false) {
if (htmlFsRef == null || jsonFsRef == null) {
throw new NowBuildError({
@@ -1495,6 +1511,13 @@ export const build = async ({
}
if (prerenders[outputPathPage] == null) {
if (lambda == null) {
throw new NowBuildError({
code: 'NEXT_MISSING_LAMBDA',
message: `Unable to find lambda for route: ${routeFileNoExt}`,
});
}
prerenders[outputPathPage] = new Prerender({
expiration: initialRevalidate,
lambda,

View File

@@ -1,6 +1,6 @@
{
"scripts": {
"build": "next build && next export"
"now-build": "next build && next export"
},
"dependencies": {
"next": "canary",

View File

@@ -1,6 +1,6 @@
{
"scripts": {
"build": "next build && next export"
"vercel-build": "next build && next export"
},
"dependencies": {
"next": "canary",

View File

@@ -0,0 +1,5 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
};

View File

@@ -0,0 +1,108 @@
{
"version": 2,
"uploadNowJson": true,
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
"probes": [
{
"path": "/forever",
"status": 200,
"mustContain": "hello"
},
{
"path": "/blog/post-1",
"status": 200,
"mustContain": "post-1"
},
{
"path": "/blog/post-2",
"status": 200,
"mustContain": "post-2"
},
{
"path": "/_next/data/testing-build-id/blog/post-1.json",
"status": 200,
"mustContain": "post-1"
},
{
"path": "/_next/data/testing-build-id/blog/post-2.json",
"status": 200,
"mustContain": "post-2"
},
{
"path": "/blog/post-1/comment-1",
"status": 200,
"mustContain": "comment-1"
},
{
"path": "/blog/post-2/comment-2",
"status": 200,
"mustContain": "comment-2"
},
{
"path": "/_next/data/testing-build-id/blog/post-1/comment-1.json",
"status": 200,
"mustContain": "comment-1"
},
{
"path": "/_next/data/testing-build-id/blog/post-2/comment-2.json",
"status": 200,
"mustContain": "comment-2"
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"mustContain": "world"
},
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"mustContain": "world"
},
{
"path": "/nofallback/one",
"status": 200,
"mustContain": "one"
},
{
"path": "/nofallback/two",
"status": 200,
"mustContain": "two"
},
{
"path": "/nofallback/nope",
"status": 404,
"mustContain": "This page could not be found"
},
{
"path": "/_next/data/testing-build-id/nofallback/one.json",
"status": 200,
"mustContain": "one"
},
{
"path": "/_next/data/testing-build-id/nofallback/two.json",
"status": 200,
"mustContain": "two"
},
{
"path": "/_next/data/testing-build-id/nofallback/nope.json",
"status": 404
},
{
"logMustNotContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
},
{
"logMustNotContain": "WARNING: Your application is being opted out of \"@vercel/next\" optimized lambdas mode due to `functions` config"
},
{
"logMustNotContain": "Traced Next.js serverless functions for external files in"
},
{
"logMustNotContain": "All serverless functions created in"
},
{
"logMustNotContain": "Compressed shared serverless function files"
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

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