mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-27 11:49:14 +00:00
Compare commits
67 Commits
@vercel/ne
...
@vercel/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c821a6fb5 | ||
|
|
5c4fb319af | ||
|
|
fbe9ea0750 | ||
|
|
fa0f1b90b4 | ||
|
|
2daa0e28d3 | ||
|
|
48358b4986 | ||
|
|
c59f44a63b | ||
|
|
ca27864201 | ||
|
|
6c81a87338 | ||
|
|
a7bef9387b | ||
|
|
65d6c5e1f4 | ||
|
|
c09355fdb3 | ||
|
|
d9ac4c45e1 | ||
|
|
c2ff95714f | ||
|
|
a51feb7a62 | ||
|
|
ff10918230 | ||
|
|
04bea1e3cd | ||
|
|
bc5e5e8a9c | ||
|
|
2e647175f5 | ||
|
|
1a4e1d2fdd | ||
|
|
49e2274d81 | ||
|
|
9af3938544 | ||
|
|
7b5bf061c2 | ||
|
|
ffb98781f1 | ||
|
|
3411fcbb68 | ||
|
|
55cfd33338 | ||
|
|
93a9e5bed3 | ||
|
|
b454021234 | ||
|
|
bb705cd091 | ||
|
|
0986f4bcb6 | ||
|
|
83f77223aa | ||
|
|
effda1fa6c | ||
|
|
2ab6a7ef0c | ||
|
|
200bf5e996 | ||
|
|
94ab2512e9 | ||
|
|
da3207278e | ||
|
|
8e10a82b43 | ||
|
|
d7731d191b | ||
|
|
0f5f99e667 | ||
|
|
93a7831943 | ||
|
|
0eacbeae11 | ||
|
|
e50417dc47 | ||
|
|
4840a25d30 | ||
|
|
785e91979d | ||
|
|
7a776c54b8 | ||
|
|
997031c53b | ||
|
|
fdee03a599 | ||
|
|
c5a93ecdad | ||
|
|
5a9391b7ce | ||
|
|
99e49473b8 | ||
|
|
de1cc6f9a7 | ||
|
|
08eedd8f34 | ||
|
|
5021a71a8e | ||
|
|
56671d7c2f | ||
|
|
5035fa537f | ||
|
|
63cc9009c8 | ||
|
|
ccf6e3c432 | ||
|
|
8d015e3138 | ||
|
|
42f2fa1a20 | ||
|
|
8397aac0e3 | ||
|
|
7bcdc144eb | ||
|
|
624da9170d | ||
|
|
fb5b013a03 | ||
|
|
0a4bb53a58 | ||
|
|
2fbd9c78e3 | ||
|
|
1ed2b7a57d | ||
|
|
88cd9ca3c3 |
@@ -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
|
||||
|
||||
|
||||
28
.github/CODEOWNERS
vendored
28
.github/CODEOWNERS
vendored
@@ -1,27 +1,27 @@
|
||||
# Documentation
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
* @tootallnate
|
||||
* @TooTallNate
|
||||
/.github/workflows @AndyBitz @styfle
|
||||
/packages/frameworks @AndyBitz
|
||||
/packages/now-cli/src/commands/dev/ @tootallnate @styfle @AndyBitz
|
||||
/packages/now-cli/src/util/dev/ @tootallnate @styfle @AndyBitz
|
||||
/packages/now-cli/src/commands/domains/ @javivelasco @mglagola @anatrajkovska
|
||||
/packages/now-cli/src/commands/certs/ @javivelasco @mglagola @anatrajkovska
|
||||
/packages/now-cli/src/commands/dev @TooTallNate @styfle @AndyBitz
|
||||
/packages/now-cli/src/util/dev @TooTallNate @styfle @AndyBitz
|
||||
/packages/now-cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
|
||||
/packages/now-cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
|
||||
/packages/now-cli/src/commands/env @styfle @lucleray
|
||||
/packages/now-client @rdev
|
||||
/packages/now-build-utils @styfle @AndyBitz
|
||||
/packages/now-node @styfle @tootallnate @lucleray
|
||||
/packages/now-node-bridge @styfle @tootallnate @lucleray
|
||||
/packages/now-client @rdev @styfle @TooTallNate
|
||||
/packages/now-build-utils @styfle @AndyBitz @TooTallNate
|
||||
/packages/now-node @styfle @TooTallNate @lucleray
|
||||
/packages/now-node-bridge @styfle @TooTallNate @lucleray
|
||||
/packages/now-next @Timer @ijjk
|
||||
/packages/now-go @styfle @sophearak
|
||||
/packages/now-python @styfle @sophearak
|
||||
/packages/now-ruby @styfle @coetry @nathancahill
|
||||
/packages/now-go @styfle @TooTallNate
|
||||
/packages/now-python @styfle @TooTallNate
|
||||
/packages/now-ruby @styfle @coetry @TooTallNate
|
||||
/packages/now-static-build @styfle @AndyBitz
|
||||
/packages/now-routing-utils @styfle @dav-is @ijjk
|
||||
/examples @mcsdevv @timothyis
|
||||
/examples/create-react-app @Timer
|
||||
/examples/nextjs @timneutkens
|
||||
/examples/nextjs @timneutkens @Timer
|
||||
/examples/hugo @mcsdevv @timothyis @styfle
|
||||
/examples/jekyll @mcsdevv @timothyis @sarupbanskota
|
||||
/examples/jekyll @mcsdevv @timothyis @styfle
|
||||
/examples/zola @mcsdevv @timothyis @styfle
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
# Runtime Developer Reference
|
||||
|
||||
The following page is a reference for how to create a Runtime using the available Runtime API.
|
||||
The following page is a reference for how to create a Runtime by implementing
|
||||
the Runtime API interface.
|
||||
|
||||
A Runtime is an npm module that exposes a `build` function and optionally an `analyze` function and `prepareCache` function.
|
||||
Official Runtimes are published to [npmjs.com](https://npmjs.com) as a package and referenced in the `use` property of the `vercel.json` configuration file.
|
||||
However, the `use` property will work with any [npm install argument](https://docs.npmjs.com/cli/install) such as a git repo url which is useful for testing your Runtime.
|
||||
A Runtime is an npm module that implements the following interface:
|
||||
|
||||
```typescript
|
||||
interface Runtime {
|
||||
version: number;
|
||||
build: (options: BuildOptions) => Promise<BuildResult>;
|
||||
analyze?: (options: AnalyzeOptions) => Promise<string>;
|
||||
prepareCache?: (options: PrepareCacheOptions) => Promise<CacheOutputs>;
|
||||
shouldServe?: (options: ShouldServeOptions) => Promise<boolean>;
|
||||
startDevServer?: (
|
||||
options: StartDevServerOptions
|
||||
) => Promise<StartDevServerResult>;
|
||||
}
|
||||
```
|
||||
|
||||
The `version` property and the `build()` function are the only _required_ fields.
|
||||
The rest are optional extensions that a Runtime _may_ implement in order to
|
||||
enhance functionality. These functions are documented in more detail below.
|
||||
|
||||
Official Runtimes are published to [the npm registry](https://npmjs.com) as a package and referenced in the `use` property of the `vercel.json` configuration file.
|
||||
|
||||
> **Note:** The `use` property in the `builds` array will work with any [npm
|
||||
> install argument](https://docs.npmjs.com/cli/install) such as a git repo URL,
|
||||
> which is useful for testing your Runtime. Alternatively, the `functions` property
|
||||
> requires that you specify a specifc tag published to npm, for stability purposes.
|
||||
|
||||
See the [Runtimes Documentation](https://vercel.com/docs/runtimes) to view example usage.
|
||||
|
||||
@@ -16,146 +39,170 @@ A **required** exported constant that decides which version of the Runtime API t
|
||||
|
||||
The latest and suggested version is `3`.
|
||||
|
||||
### `analyze`
|
||||
**Example:**
|
||||
|
||||
An **optional** exported function that returns a unique fingerprint used for the purpose of [build de-duplication](https://vercel.com/docs/v2/platform/deployments#deduplication). If the `analyze` function is not supplied, a random fingerprint is assigned to each build.
|
||||
|
||||
```js
|
||||
export analyze({
|
||||
files: Files,
|
||||
entrypoint: String,
|
||||
workPath: String,
|
||||
config: Object
|
||||
}) : String fingerprint
|
||||
```typescript
|
||||
export const version = 3;
|
||||
```
|
||||
|
||||
If you are using TypeScript, you should use the following types:
|
||||
### `build()`
|
||||
|
||||
```ts
|
||||
import { AnalyzeOptions } from '@vercel/build-utils'
|
||||
A **required** exported function that returns a Serverless Function.
|
||||
|
||||
export analyze(options: AnalyzeOptions) {
|
||||
return 'fingerprint goes here'
|
||||
> What's a Serverless Function? Read about [Serverless Functions](https://vercel.com/docs/v2/serverless-functions/introduction) to learn more.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
import { BuildOptions, createLambda } from '@vercel/build-utils';
|
||||
|
||||
export async function build(options: BuildOptions) {
|
||||
// Build the code here…
|
||||
|
||||
const lambda = createLambda(/* … */);
|
||||
return {
|
||||
output: lambda,
|
||||
watch: [
|
||||
// Dependent files to trigger a rebuild in `vercel dev` go here…
|
||||
],
|
||||
routes: [
|
||||
// If your Runtime needs to define additional routing, define it here…
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `build`
|
||||
### `analyze()`
|
||||
|
||||
A **required** exported function that returns a [Serverless Function](#serverless-function).
|
||||
An **optional** exported function that returns a unique fingerprint used for the
|
||||
purpose of [build
|
||||
de-duplication](https://vercel.com/docs/v2/platform/deployments#deduplication).
|
||||
If the `analyze()` function is not supplied, then a random fingerprint is
|
||||
assigned to each build.
|
||||
|
||||
What's a Serverless Function? Read about [Serverless Functions](https://vercel.com/docs/v2/serverless-functions/introduction) to learn more.
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
build({
|
||||
files: Files,
|
||||
entrypoint: String,
|
||||
workPath: String,
|
||||
config: Object,
|
||||
meta?: {
|
||||
isDev?: Boolean,
|
||||
requestPath?: String,
|
||||
filesChanged?: Array<String>,
|
||||
filesRemoved?: Array<String>
|
||||
}
|
||||
}) : {
|
||||
watch?: Array<String>,
|
||||
output: Lambda,
|
||||
routes?: Object
|
||||
```typescript
|
||||
import { AnalyzeOptions } from '@vercel/build-utils';
|
||||
|
||||
export async function analyze(options: AnalyzeOptions) {
|
||||
// Do calculations to generate a fingerprint based off the source code here…
|
||||
|
||||
return 'fingerprint goes here';
|
||||
}
|
||||
```
|
||||
|
||||
If you are using TypeScript, you should use the following types:
|
||||
### `prepareCache()`
|
||||
|
||||
```ts
|
||||
import { BuildOptions } from '@vercel/build-utils'
|
||||
An **optional** exported function that is executed after [`build()`](#build) is
|
||||
completed. The implementation should return an object of `File`s that will be
|
||||
pre-populated in the working directory for the next build run in the user's
|
||||
project. An example use-case is that `@vercel/node` uses this function to cache
|
||||
the `node_modules` directory, making it faster to install npm dependencies for
|
||||
future builds.
|
||||
|
||||
export build(options: BuildOptions) {
|
||||
// Build the code here
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
import { PrepareCacheOptions } from '@vercel/build-utils';
|
||||
|
||||
export async function prepareCache(options: PrepareCacheOptions) {
|
||||
// Create a mapping of file names and `File` object instances to cache here…
|
||||
|
||||
return {
|
||||
output: {
|
||||
'path-to-file': File,
|
||||
'path-to-lambda': Lambda
|
||||
},
|
||||
watch: [],
|
||||
routes: {}
|
||||
}
|
||||
'path-to-file': File,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `prepareCache`
|
||||
### `shouldServe()`
|
||||
|
||||
An **optional** exported function that is equivalent to [`build`](#build), but it executes the instructions necessary to prepare a cache for the next run.
|
||||
An **optional** exported function that is only used by `vercel dev` in [Vercel
|
||||
CLI](https://vercel.com/download) and indicates whether a
|
||||
[Runtime](https://vercel.com/docs/runtimes) wants to be responsible for responding
|
||||
to a certain request path.
|
||||
|
||||
```js
|
||||
prepareCache({
|
||||
files: Files,
|
||||
entrypoint: String,
|
||||
workPath: String,
|
||||
cachePath: String,
|
||||
config: Object
|
||||
}) : Files cacheOutput
|
||||
```
|
||||
**Example:**
|
||||
|
||||
If you are using TypeScript, you can import the types for each of these functions by using the following:
|
||||
```typescript
|
||||
import { ShouldServeOptions } from '@vercel/build-utils';
|
||||
|
||||
```ts
|
||||
import { PrepareCacheOptions } from '@vercel/build-utils'
|
||||
export async function shouldServe(options: ShouldServeOptions) {
|
||||
// Determine whether or not the Runtime should respond to the request path here…
|
||||
|
||||
export prepareCache(options: PrepareCacheOptions) {
|
||||
return { 'path-to-file': File }
|
||||
return options.requestPath === options.entrypoint;
|
||||
}
|
||||
```
|
||||
|
||||
### `shouldServe`
|
||||
If this function is not defined, Vercel CLI will use the [default implementation](https://github.com/vercel/vercel/blob/52994bfe26c5f4f179bdb49783ee57ce19334631/packages/now-build-utils/src/should-serve.ts).
|
||||
|
||||
An **optional** exported function that is only used by `vercel dev` in [Vercel CLI](https:///download) and indicates whether a [Runtime](https://vercel.com/docs/runtimes) wants to be responsible for building a certain request path.
|
||||
### `startDevServer()`
|
||||
|
||||
```js
|
||||
shouldServe({
|
||||
entrypoint: String,
|
||||
files: Files,
|
||||
config: Object,
|
||||
requestPath: String,
|
||||
workPath: String
|
||||
}) : Boolean
|
||||
```
|
||||
An **optional** exported function that is only used by `vercel dev` in [Vercel
|
||||
CLI](https://vercel.com/download). If this function is defined, Vercel CLI will
|
||||
**not** invoke the `build()` function, and instead invoke this function for every
|
||||
HTTP request. It is an opportunity to provide an optimized development experience
|
||||
rather than going through the entire `build()` process that is used in production.
|
||||
|
||||
If you are using TypeScript, you can import the types for each of these functions by using the following:
|
||||
This function is invoked _once per HTTP request_ and is expected to spawn a child
|
||||
process which creates an HTTP server that will execute the entrypoint code when
|
||||
an HTTP request is received. This child process is _single-serve_ (only used for
|
||||
a single HTTP request). After the HTTP response is complete, `vercel dev` sends
|
||||
a shut down signal to the child process.
|
||||
|
||||
```ts
|
||||
import { ShouldServeOptions } from '@vercel/build-utils'
|
||||
The `startDevServer()` function returns an object with the `port` number that the
|
||||
child process' HTTP server is listening on (which should be an [ephemeral
|
||||
port](https://stackoverflow.com/a/28050404/376773)) as well as the child process'
|
||||
Process ID, which `vercel dev` uses to send the shut down signal to.
|
||||
|
||||
export shouldServe(options: ShouldServeOptions) {
|
||||
return Boolean
|
||||
> **Hint:** To determine which ephemeral port the child process is listening on,
|
||||
> some form of [IPC](https://en.wikipedia.org/wiki/Inter-process_communication) is
|
||||
> required. For example, in `@vercel/go` the child process writes the port number
|
||||
> to [_file descriptor 3_](https://en.wikipedia.org/wiki/File_descriptor), which is read by the `startDevServer()` function
|
||||
> implementation.
|
||||
|
||||
It may also return `null` to opt-out of this behavior for a particular request
|
||||
path or entrypoint.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
import { spawn } from 'child_process';
|
||||
import { StartDevServerOptions } from '@vercel/build-utils';
|
||||
|
||||
export async function startDevServer(options: StartDevServerOptions) {
|
||||
// Create a child process which will create an HTTP server.
|
||||
//
|
||||
// Note: `my-runtime-dev-server` is an example dev server program name.
|
||||
// Your implementation will spawn a different program specific to your runtime.
|
||||
const child = spawn('my-runtime-dev-server', [options.entrypoint], {
|
||||
stdio: ['ignore', 'inherit', 'inherit', 'pipe'],
|
||||
});
|
||||
|
||||
// In this example, the child process will write the port number to FD 3…
|
||||
const portPipe = child.stdio[3];
|
||||
const childPort = await new Promise(resolve => {
|
||||
portPipe.setEncoding('utf8');
|
||||
portPipe.once('data', data => {
|
||||
resolve(Number(data));
|
||||
});
|
||||
});
|
||||
|
||||
return { pid: child.pid, port: childPort };
|
||||
}
|
||||
```
|
||||
|
||||
If this method is not defined, Vercel CLI will default to [this function](https://github.com/vercel/vercel/blob/52994bfe26c5f4f179bdb49783ee57ce19334631/packages/now-build-utils/src/should-serve.ts).
|
||||
|
||||
### Runtime Options
|
||||
|
||||
The exported functions [`analyze`](#analyze), [`build`](#build), and [`prepareCache`](#preparecache) receive one argument with the following properties.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `files`: All source files of the project as a [Files](#files) data structure.
|
||||
- `entrypoint`: Name of entrypoint file for this particular build job. Value `files[entrypoint]` is guaranteed to exist and be a valid [File](#files) reference. `entrypoint` is always a discrete file and never a glob, since globs are expanded into separate builds at deployment time.
|
||||
- `workPath`: A writable temporary directory where you are encouraged to perform your build process. This directory will be populated with the restored cache from the previous run (if any) for [`analyze`](#analyze) and [`build`](#build).
|
||||
- `cachePath`: A writable temporary directory where you can build a cache for the next run. This is only passed to `prepareCache`.
|
||||
- `config`: An arbitrary object passed from by the user in the [Build definition](#defining-the-build-step) in `vercel.json`.
|
||||
|
||||
## Examples
|
||||
|
||||
Check out our [Node.js Runtime](https://github.com/vercel/vercel/tree/master/packages/now-node), [Go Runtime](https://github.com/vercel/vercel/tree/master/packages/now-go), [Python Runtime](https://github.com/vercel/vercel/tree/master/packages/now-python) or [Ruby Runtime](https://github.com/vercel/vercel/tree/master/packages/now-ruby) for examples of how to build one.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Execution Context
|
||||
|
||||
A [Serverless Function](https://vercel.com/docs/v2/serverless-functions/introduction) is created where the Runtime logic is executed. The lambda is run using the Node.js 8 runtime. A brand new sandbox is created for each deployment, for security reasons. The sandbox is cleaned up between executions to ensure no lingering temporary files are shared from build to build.
|
||||
- Runtimes are executed in a Linux container that closely matches the Servereless Function runtime environment.
|
||||
- The Runtime code is executed using Node.js version **12.x**.
|
||||
- A brand new sandbox is created for each deployment, for security reasons.
|
||||
- The sandbox is cleaned up between executions to ensure no lingering temporary files are shared from build to build.
|
||||
|
||||
All the APIs you export ([`analyze`](#analyze), [`build`](#build) and [`prepareCache`](#preparecache)) are not guaranteed to be run in the same process, but the filesystem we expose (e.g.: `workPath` and the results of calling [`getWriteableDirectory`](#getWriteableDirectory) ) is retained.
|
||||
All the APIs you export ([`analyze()`](#analyze), [`build()`](#build),
|
||||
[`prepareCache()`](#preparecache), etc.) are not guaranteed to be run in the
|
||||
same process, but the filesystem we expose (e.g.: `workPath` and the results
|
||||
of calling [`getWritableDirectory`](#getWritableDirectory) ) is retained.
|
||||
|
||||
If you need to share state between those steps, use the filesystem.
|
||||
|
||||
@@ -173,11 +220,11 @@ The env and secrets specified by the user as `build.env` are passed to the Runti
|
||||
|
||||
When you publish your Runtime to npm, make sure to not specify `@vercel/build-utils` (as seen below in the API definitions) as a dependency, but rather as part of `peerDependencies`.
|
||||
|
||||
## Types
|
||||
## `@vercel/build-utils` Types
|
||||
|
||||
### `Files`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { File } from '@vercel/build-utils';
|
||||
type Files = { [filePath: string]: File };
|
||||
```
|
||||
@@ -188,7 +235,7 @@ When used as an input, the `Files` object will only contain `FileRefs`. When `Fi
|
||||
|
||||
An example of a valid output `Files` object is:
|
||||
|
||||
```json
|
||||
```javascript
|
||||
{
|
||||
"index.html": FileRef,
|
||||
"api/index.js": Lambda
|
||||
@@ -199,7 +246,7 @@ An example of a valid output `Files` object is:
|
||||
|
||||
This is an abstract type that can be imported if you are using TypeScript.
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { File } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
@@ -211,71 +258,71 @@ Valid `File` types include:
|
||||
|
||||
### `FileRef`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { FileRef } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract file instance stored in our platform, based on the file identifier string (its checksum). When a `Files` object is passed as an input to `analyze` or `build`, all its values will be instances of `FileRef`.
|
||||
This is a [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract file instance stored in our platform, based on the file identifier string (its checksum). When a `Files` object is passed as an input to `analyze` or `build`, all its values will be instances of `FileRef`.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `mode : Number` file mode
|
||||
- `digest : String` a checksum that represents the file
|
||||
- `mode: Number` file mode
|
||||
- `digest: String` a checksum that represents the file
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `toStream() : Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
|
||||
- `toStream(): Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
|
||||
|
||||
### `FileFsRef`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { FileFsRef } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract instance of a file present in the filesystem that the build process is executing in.
|
||||
This is a [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract instance of a file present in the filesystem that the build process is executing in.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `mode : Number` file mode
|
||||
- `fsPath : String` the absolute path of the file in file system
|
||||
- `mode: Number` file mode
|
||||
- `fsPath: String` the absolute path of the file in file system
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `static async fromStream({ mode : Number, stream : Stream, fsPath : String }) : FileFsRef` creates an instance of a [FileFsRef](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) from `Stream`, placing file at `fsPath` with `mode`
|
||||
- `toStream() : Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
|
||||
- `static async fromStream({ mode: Number, stream: Stream, fsPath: String }): FileFsRef` creates an instance of a [FileFsRef](#FileFsRef) from `Stream`, placing file at `fsPath` with `mode`
|
||||
- `toStream(): Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
|
||||
|
||||
### `FileBlob`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { FileBlob } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract instance of a file present in memory.
|
||||
This is a [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract instance of a file present in memory.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `mode : Number` file mode
|
||||
- `data : String | Buffer` the body of the file
|
||||
- `mode: Number` file mode
|
||||
- `data: String | Buffer` the body of the file
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `static async fromStream({ mode : Number, stream : Stream }) :FileBlob` creates an instance of a [FileBlob](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) from [`Stream`](https://nodejs.org/api/stream.html) with `mode`
|
||||
- `toStream() : Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
|
||||
- `static async fromStream({ mode: Number, stream: Stream }): FileBlob` creates an instance of a [FileBlob](#FileBlob) from [`Stream`](https://nodejs.org/api/stream.html) with `mode`
|
||||
- `toStream(): Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
|
||||
|
||||
### `Lambda`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { Lambda } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), called a Serverless Function, that can be created by supplying `files`, `handler`, `runtime`, and `environment` as an object to the [`createLambda`](#createlambda) helper. The instances of this class should not be created directly. Instead, invoke the [`createLambda`](#createlambda) helper function.
|
||||
This is a [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents a Serverless Function. An instance can be created by supplying `files`, `handler`, `runtime`, and `environment` as an object to the [`createLambda`](#createlambda) helper. The instances of this class should not be created directly. Instead, invoke the [`createLambda`](#createlambda) helper function.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `files : Files` the internal filesystem of the lambda
|
||||
- `handler : String` path to handler file and (optionally) a function name it exports
|
||||
- `runtime : LambdaRuntime` the name of the lambda runtime
|
||||
- `environment : Object` key-value map of handler-related (aside of those passed by user) environment variables
|
||||
- `files: Files` the internal filesystem of the lambda
|
||||
- `handler: String` path to handler file and (optionally) a function name it exports
|
||||
- `runtime: LambdaRuntime` the name of the lambda runtime
|
||||
- `environment: Object` key-value map of handler-related (aside of those passed by user) environment variables
|
||||
|
||||
### `LambdaRuntime`
|
||||
|
||||
@@ -291,15 +338,15 @@ This is an abstract enumeration type that is implemented by one of the following
|
||||
- `ruby2.5`
|
||||
- `provided`
|
||||
|
||||
## JavaScript API
|
||||
## `@vercel/build-utils` Helper Functions
|
||||
|
||||
The following is exposed by `@vercel/build-utils` to simplify the process of writing Runtimes, manipulating the file system, using the above types, etc.
|
||||
|
||||
### `createLambda`
|
||||
### `createLambda()`
|
||||
|
||||
Signature: `createLambda(Object spec) : Lambda`
|
||||
Signature: `createLambda(Object spec): Lambda`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { createLambda } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
@@ -316,29 +363,33 @@ await createLambda({
|
||||
});
|
||||
```
|
||||
|
||||
### `download`
|
||||
### `download()`
|
||||
|
||||
Signature: `download() : Files`
|
||||
Signature: `download(): Files`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { download } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
This utility allows you to download the contents of a [`Files`](#files) data structure, therefore creating the filesystem represented in it.
|
||||
This utility allows you to download the contents of a [`Files`](#files) data
|
||||
structure, therefore creating the filesystem represented in it.
|
||||
|
||||
Since `Files` is an abstract way of representing files, you can think of `download` as a way of making that virtual filesystem _real_.
|
||||
Since `Files` is an abstract way of representing files, you can think of
|
||||
`download()` as a way of making that virtual filesystem _real_.
|
||||
|
||||
If the **optional** `meta` property is passed (the argument for [build](#build)), only the files that have changed are downloaded. This is decided using `filesRemoved` and `filesChanged` inside that object.
|
||||
If the **optional** `meta` property is passed (the argument for
|
||||
[`build()`](#build)), only the files that have changed are downloaded.
|
||||
This is decided using `filesRemoved` and `filesChanged` inside that object.
|
||||
|
||||
```js
|
||||
await download(files, workPath, meta);
|
||||
```
|
||||
|
||||
### `glob`
|
||||
### `glob()`
|
||||
|
||||
Signature: `glob() : Files`
|
||||
Signature: `glob(): Files`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { glob } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
@@ -355,21 +406,21 @@ exports.build = ({ files, workPath }) => {
|
||||
}
|
||||
```
|
||||
|
||||
### `getWriteableDirectory`
|
||||
### `getWritableDirectory()`
|
||||
|
||||
Signature: `getWriteableDirectory() : String`
|
||||
Signature: `getWritableDirectory(): String`
|
||||
|
||||
```ts
|
||||
import { getWriteableDirectory } from '@vercel/build-utils';
|
||||
```typescript
|
||||
import { getWritableDirectory } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
In some occasions, you might want to write to a temporary directory.
|
||||
|
||||
### `rename`
|
||||
### `rename()`
|
||||
|
||||
Signature: `rename(Files) : Files`
|
||||
Signature: `rename(Files, Function): Files`
|
||||
|
||||
```ts
|
||||
```typescript
|
||||
import { rename } from '@vercel/build-utils';
|
||||
```
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@zeit/ncc": "0.20.4",
|
||||
"async-retry": "1.2.3",
|
||||
"buffer-replace": "1.0.0",
|
||||
"cheerio": "1.0.0-rc.3",
|
||||
"eslint": "6.2.2",
|
||||
"eslint-config-prettier": "6.1.0",
|
||||
"eslint-plugin-jest": "23.8.2",
|
||||
|
||||
@@ -640,7 +640,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"buildCommand": {
|
||||
"placeholder": "`npm run build` or `nuxt build`"
|
||||
"placeholder": "`npm run build` or `nuxt generate`"
|
||||
},
|
||||
"devCommand": {
|
||||
"value": "nuxt"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/frameworks",
|
||||
"version": "0.0.15-canary.4",
|
||||
"version": "0.0.16-canary.0",
|
||||
"main": "frameworks.json",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
@@ -9,7 +9,7 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "24.0.22",
|
||||
"@types/node": "12.0.4",
|
||||
"ajv": "6.10.2",
|
||||
"ajv": "6.12.2",
|
||||
"jest": "24.9.0",
|
||||
"ts-jest": "24.1.0",
|
||||
"typescript": "3.9.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.3.2-canary.5",
|
||||
"version": "2.4.1-canary.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -38,3 +38,78 @@ interface Props {
|
||||
*/
|
||||
action?: string;
|
||||
}
|
||||
|
||||
export function getPrettyError(obj: {
|
||||
dataPath?: string;
|
||||
message?: string;
|
||||
params: any;
|
||||
}): NowBuildError {
|
||||
const docsUrl = 'https://vercel.com/docs/configuration';
|
||||
try {
|
||||
const { dataPath, params, message: ajvMessage } = obj;
|
||||
const prop = getTopLevelPropertyName(dataPath);
|
||||
let message =
|
||||
dataPath && dataPath.startsWith('.') ? `\`${dataPath.slice(1)}\` ` : '';
|
||||
|
||||
if (params && typeof params.additionalProperty === 'string') {
|
||||
const suggestion = getSuggestion(prop, params.additionalProperty);
|
||||
message += `should NOT have additional property \`${params.additionalProperty}\`. ${suggestion}`;
|
||||
} else if (params && typeof params.missingProperty === 'string') {
|
||||
message += `missing required property \`${params.missingProperty}\`.`;
|
||||
} else {
|
||||
message += `${ajvMessage}.`;
|
||||
}
|
||||
|
||||
return new NowBuildError({
|
||||
code: 'DEV_VALIDATE_CONFIG',
|
||||
message: message,
|
||||
link: prop ? `${docsUrl}#project/${prop.toLowerCase()}` : docsUrl,
|
||||
action: 'View Documentation',
|
||||
});
|
||||
} catch (e) {
|
||||
return new NowBuildError({
|
||||
code: 'DEV_VALIDATE_CONFIG',
|
||||
message: `Failed to validate configuration.`,
|
||||
link: docsUrl,
|
||||
action: 'View Documentation',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top level property from the dataPath.
|
||||
* `.cleanUrls` => `cleanUrls`
|
||||
* `.headers[0].source` => `headers`
|
||||
* `.headers[0].headers[0]` => `headers`
|
||||
* `` => ``
|
||||
*/
|
||||
function getTopLevelPropertyName(dataPath?: string): string {
|
||||
if (dataPath && dataPath.startsWith('.')) {
|
||||
const lastIndex = dataPath.indexOf('[');
|
||||
return lastIndex > -1 ? dataPath.slice(1, lastIndex) : dataPath.slice(1);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const mapTypoToSuggestion: { [key: string]: { [key: string]: string } } = {
|
||||
'': {
|
||||
builder: 'builds',
|
||||
'build.env': '{ "build": { "env": {"name": "value"} } }',
|
||||
'builds.env': '{ "build": { "env": {"name": "value"} } }',
|
||||
},
|
||||
rewrites: { src: 'source', dest: 'destination' },
|
||||
redirects: { src: 'source', dest: 'destination', status: 'statusCode' },
|
||||
headers: { src: 'source', header: 'headers' },
|
||||
routes: {
|
||||
source: 'src',
|
||||
destination: 'dest',
|
||||
header: 'headers',
|
||||
method: 'methods',
|
||||
},
|
||||
};
|
||||
|
||||
function getSuggestion(topLevelProp: string, additionalProperty: string) {
|
||||
const choices = mapTypoToSuggestion[topLevelProp];
|
||||
const choice = choices ? choices[additionalProperty] : undefined;
|
||||
return choice ? `Did you mean \`${choice}\`?` : 'Please remove it.';
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getLatestNodeVersion,
|
||||
getDiscontinuedNodeVersions,
|
||||
} from './fs/node-version';
|
||||
import { NowBuildError } from './errors';
|
||||
import streamToBuffer from './fs/stream-to-buffer';
|
||||
import shouldServe from './should-serve';
|
||||
import debug from './debug';
|
||||
@@ -111,9 +112,11 @@ export const getPlatformEnv = (name: string): string | undefined => {
|
||||
const n = process.env[nName];
|
||||
if (typeof v === 'string') {
|
||||
if (typeof n === 'string') {
|
||||
throw new Error(
|
||||
`Both "${vName}" and "${nName}" env vars are defined. Please only define the "${vName}" env var`
|
||||
);
|
||||
throw new NowBuildError({
|
||||
code: 'CONFLICTING_ENV_VAR_NAMES',
|
||||
message: `Both "${vName}" and "${nName}" env vars are defined. Please only define the "${vName}" env var.`,
|
||||
link: 'https://vercel.link/combining-old-and-new-config',
|
||||
});
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ describe('Test `detectBuilders`', () => {
|
||||
});
|
||||
|
||||
it('use a custom runtime', async () => {
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
const { builders, errors } = await detectBuilders(files, null, {
|
||||
functions,
|
||||
@@ -501,11 +501,11 @@ describe('Test `detectBuilders`', () => {
|
||||
|
||||
expect(errors).toBe(null);
|
||||
expect(builders!.length).toBe(1);
|
||||
expect(builders![0].use).toBe('now-php@0.0.8');
|
||||
expect(builders![0].use).toBe('vercel-php@0.1.0');
|
||||
});
|
||||
|
||||
it('use a custom runtime but without a source', async () => {
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/team.js'];
|
||||
const { errors } = await detectBuilders(files, null, {
|
||||
functions,
|
||||
@@ -1538,7 +1538,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
});
|
||||
|
||||
it('use a custom runtime', async () => {
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
const { builders, errors } = await detectBuilders(files, null, {
|
||||
functions,
|
||||
@@ -1547,11 +1547,11 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
|
||||
expect(errors).toBe(null);
|
||||
expect(builders!.length).toBe(1);
|
||||
expect(builders![0].use).toBe('now-php@0.0.8');
|
||||
expect(builders![0].use).toBe('vercel-php@0.1.0');
|
||||
});
|
||||
|
||||
it('use a custom runtime but without a source', async () => {
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/team.js'];
|
||||
const { errors } = await detectBuilders(files, null, {
|
||||
functions,
|
||||
@@ -2081,7 +2081,7 @@ it('Test `detectRoutes`', async () => {
|
||||
|
||||
{
|
||||
// use a custom runtime
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
|
||||
const { defaultRoutes } = await detectBuilders(files, null, { functions });
|
||||
@@ -2393,7 +2393,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
|
||||
{
|
||||
// use a custom runtime
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
|
||||
const { defaultRoutes, rewriteRoutes } = await detectBuilders(files, null, {
|
||||
@@ -2692,7 +2692,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
|
||||
{
|
||||
// use a custom runtime
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
|
||||
const {
|
||||
@@ -2941,7 +2941,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
|
||||
{
|
||||
// use a custom runtime
|
||||
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
|
||||
const {
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('Test `getPlatformEnv()`', () => {
|
||||
assert(err);
|
||||
assert.equal(
|
||||
err!.message,
|
||||
'Both "VERCEL_FOO" and "NOW_FOO" env vars are defined. Please only define the "VERCEL_FOO" env var'
|
||||
'Both "VERCEL_FOO" and "NOW_FOO" env vars are defined. Please only define the "VERCEL_FOO" env var.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/cgi",
|
||||
"version": "1.0.6-canary.0",
|
||||
"version": "1.0.6",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "19.0.2-canary.15",
|
||||
"version": "19.1.2-canary.14",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Now",
|
||||
"description": "The command-line interface for Vercel",
|
||||
"homepage": "https://vercel.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js --serial --fail-fast --verbose",
|
||||
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js test/dev-validate.unit.js --serial --fail-fast --verbose",
|
||||
"test-integration-cli": "ava test/integration.js --serial --fail-fast --verbose",
|
||||
"test-integration-v1": "ava test/integration-v1.js --serial --fail-fast",
|
||||
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
|
||||
@@ -62,13 +62,13 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.3.2-canary.5",
|
||||
"@vercel/go": "1.1.2-canary.2",
|
||||
"@vercel/next": "2.6.3-canary.6",
|
||||
"@vercel/node": "1.6.2-canary.5",
|
||||
"@vercel/python": "1.2.2-canary.2",
|
||||
"@vercel/ruby": "1.2.2-canary.1",
|
||||
"@vercel/static-build": "0.17.2-canary.1"
|
||||
"@vercel/build-utils": "2.4.1-canary.1",
|
||||
"@vercel/go": "1.1.3-canary.1",
|
||||
"@vercel/next": "2.6.8-canary.4",
|
||||
"@vercel/node": "1.7.2-canary.1",
|
||||
"@vercel/python": "1.2.2",
|
||||
"@vercel/ruby": "1.2.3-canary.0",
|
||||
"@vercel/static-build": "0.17.4-canary.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/node": "5.5.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"@zeit/fun": "0.11.2",
|
||||
"@zeit/ncc": "0.18.5",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
"ajv": "6.10.2",
|
||||
"ajv": "6.12.2",
|
||||
"alpha-sort": "2.0.1",
|
||||
"ansi-escapes": "3.0.0",
|
||||
"ansi-regex": "3.0.0",
|
||||
@@ -136,6 +136,7 @@
|
||||
"escape-html": "1.0.3",
|
||||
"esm": "3.1.4",
|
||||
"execa": "3.2.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "7.0.1",
|
||||
"get-port": "5.1.1",
|
||||
"glob": "7.1.2",
|
||||
|
||||
@@ -3,8 +3,8 @@ import bytes from 'bytes';
|
||||
import { join } from 'path';
|
||||
import { write as copy } from 'clipboardy';
|
||||
import chalk from 'chalk';
|
||||
import title from 'title';
|
||||
import { fileNameSymbol } from '@vercel/client';
|
||||
import { getPrettyError } from '@vercel/build-utils';
|
||||
import Client from '../../util/client';
|
||||
import { handleError } from '../../util/error';
|
||||
import getArgs from '../../util/get-args';
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
} from '../../util/errors-ts';
|
||||
import { SchemaValidationFailed } from '../../util/errors';
|
||||
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
|
||||
import isWildcardAlias from '../../util/alias/is-wildcard-alias';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import editProjectSettings from '../../util/input/edit-project-settings';
|
||||
import {
|
||||
@@ -56,6 +55,7 @@ import validatePaths, {
|
||||
} from '../../util/validate-paths';
|
||||
import { readLocalConfig } from '../../util/config/files';
|
||||
import { getCommandName } from '../../util/pkg-name.ts';
|
||||
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url.ts';
|
||||
|
||||
const addProcessEnv = async (log, env) => {
|
||||
let val;
|
||||
@@ -87,6 +87,7 @@ const addProcessEnv = async (log, env) => {
|
||||
|
||||
const printDeploymentStatus = async (
|
||||
output,
|
||||
client,
|
||||
{
|
||||
readyState,
|
||||
alias: aliasList,
|
||||
@@ -119,18 +120,14 @@ const printDeploymentStatus = async (
|
||||
let previewUrl;
|
||||
let isWildcard;
|
||||
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
// search for a non now.sh/non wildcard domain
|
||||
// but fallback to the first alias in the list
|
||||
const mainAlias =
|
||||
aliasList.find(
|
||||
alias =>
|
||||
!alias.endsWith('.now.sh') &&
|
||||
!alias.endsWith('.vercel.app') &&
|
||||
!isWildcardAlias(alias)
|
||||
) || aliasList[0];
|
||||
|
||||
isWildcard = isWildcardAlias(mainAlias);
|
||||
previewUrl = isWildcard ? mainAlias : `https://${mainAlias}`;
|
||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||
if (previewUrlInfo) {
|
||||
isWildcard = previewUrlInfo.isWildcard;
|
||||
previewUrl = previewUrlInfo.previewUrl;
|
||||
} else {
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
} else {
|
||||
// fallback to deployment url
|
||||
isWildcard = false;
|
||||
@@ -706,6 +703,12 @@ export default async function main(
|
||||
|
||||
return printDeploymentStatus(
|
||||
output,
|
||||
new Client({
|
||||
apiUrl: ctx.apiUrl,
|
||||
token: ctx.authConfig.token,
|
||||
currentTeam: org.type === 'team' ? org.id : null,
|
||||
debug: debugEnabled,
|
||||
}),
|
||||
deployment,
|
||||
deployStamp,
|
||||
!argv['--no-clipboard'],
|
||||
@@ -735,53 +738,10 @@ function handleCreateDeployError(output, error, localConfig) {
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof SchemaValidationFailed) {
|
||||
const { message, params, keyword, dataPath } = error.meta;
|
||||
|
||||
if (params && params.additionalProperty) {
|
||||
const prop = params.additionalProperty;
|
||||
|
||||
output.error(
|
||||
`The property ${code(prop)} is not allowed in ${highlight(
|
||||
localConfig[fileNameSymbol]
|
||||
)} – please remove it.`
|
||||
);
|
||||
|
||||
if (prop === 'build.env' || prop === 'builds.env') {
|
||||
output.note(
|
||||
`Do you mean ${code('build')} (object) with a property ${code(
|
||||
'env'
|
||||
)} (object) instead of ${code(prop)}?`
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (dataPath === '.name') {
|
||||
output.error(message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (keyword === 'type') {
|
||||
const prop = dataPath.substr(1, dataPath.length);
|
||||
|
||||
output.error(
|
||||
`The property ${code(prop)} in ${highlight(
|
||||
localConfig[fileNameSymbol]
|
||||
)} can only be of type ${code(title(params.type))}.`
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const link = 'https://vercel.com/docs/configuration';
|
||||
|
||||
output.error(
|
||||
`Failed to validate ${highlight(
|
||||
localConfig[fileNameSymbol]
|
||||
)}: ${message}\nDocumentation: ${link}`
|
||||
);
|
||||
|
||||
const niceError = getPrettyError(error.meta);
|
||||
const fileName = localConfig[fileNameSymbol] || 'vercel.json';
|
||||
niceError.message = `Invalid ${fileName} - ${niceError.message}`;
|
||||
output.prettyError(niceError);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof TooManyRequests) {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 } from '../../types';
|
||||
|
||||
type Options = {
|
||||
'--debug'?: boolean;
|
||||
@@ -50,21 +51,26 @@ export default async function dev(
|
||||
return 1;
|
||||
}
|
||||
|
||||
let devCommand: undefined | string;
|
||||
let frameworkSlug: null | string = null;
|
||||
let devCommand: string | undefined;
|
||||
let frameworkSlug: string | undefined;
|
||||
let projectSettings: ProjectSettings | undefined;
|
||||
if (link.status === 'linked') {
|
||||
const { project, org } = link;
|
||||
client.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
|
||||
projectSettings = project;
|
||||
|
||||
if (project.devCommand) {
|
||||
devCommand = project.devCommand;
|
||||
} else if (project.framework) {
|
||||
const framework = frameworks.find(f => f.slug === project.framework);
|
||||
|
||||
if (framework) {
|
||||
frameworkSlug = framework.slug;
|
||||
const defaults = framework.settings.devCommand;
|
||||
if (framework.slug) {
|
||||
frameworkSlug = framework.slug;
|
||||
}
|
||||
|
||||
const defaults = framework.settings.devCommand;
|
||||
if (isSettingValue(defaults)) {
|
||||
devCommand = defaults.value;
|
||||
}
|
||||
@@ -81,6 +87,7 @@ export default async function dev(
|
||||
debug,
|
||||
devCommand,
|
||||
frameworkSlug,
|
||||
projectSettings,
|
||||
});
|
||||
|
||||
process.once('SIGINT', () => devServer.stop());
|
||||
|
||||
@@ -10,6 +10,7 @@ import handleError from '../../util/handle-error';
|
||||
import createOutput from '../../util/output/create-output';
|
||||
import logo from '../../util/output/logo';
|
||||
import cmd from '../../util/output/cmd';
|
||||
import highlight from '../../util/output/highlight';
|
||||
import dev from './dev';
|
||||
import readPackage from '../../util/read-package';
|
||||
import readConfig from '../../util/config/read-config';
|
||||
@@ -119,6 +120,21 @@ export default async function main(ctx: NowContext) {
|
||||
try {
|
||||
return await dev(ctx, argv, args, output);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
// Error message will look like the following:
|
||||
// "request to https://api.vercel.com/www/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
|
||||
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
|
||||
if (matches && matches[1]) {
|
||||
const hostname = matches[1];
|
||||
output.error(
|
||||
`The hostname ${highlight(
|
||||
hostname
|
||||
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
output.debug(err.stack);
|
||||
return 1;
|
||||
}
|
||||
output.prettyError(err);
|
||||
output.debug(stringifyError(err));
|
||||
return 1;
|
||||
|
||||
37
packages/now-cli/src/commands/env/pull.ts
vendored
37
packages/now-cli/src/commands/env/pull.ts
vendored
@@ -9,16 +9,39 @@ import getDecryptedSecret from '../../util/env/get-decrypted-secret';
|
||||
import param from '../../util/output/param';
|
||||
import withSpinner from '../../util/with-spinner';
|
||||
import { join } from 'path';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promises, openSync, closeSync, readSync } from 'fs';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
const { writeFile } = promises;
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
'--yes': boolean;
|
||||
};
|
||||
|
||||
function readHeadSync(path: string, length: number) {
|
||||
const buffer = Buffer.alloc(length);
|
||||
const fd = openSync(path, 'r');
|
||||
try {
|
||||
readSync(fd, buffer, 0, buffer.length, null);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
function tryReadHeadSync(path: string, length: number) {
|
||||
try {
|
||||
return readHeadSync(path, length);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async function pull(
|
||||
client: Client,
|
||||
project: Project,
|
||||
@@ -35,10 +58,14 @@ export default async function pull(
|
||||
|
||||
const [filename = '.env'] = args;
|
||||
const fullPath = join(process.cwd(), filename);
|
||||
const exists = existsSync(fullPath);
|
||||
const skipConfirmation = opts['--yes'];
|
||||
|
||||
if (
|
||||
const head = tryReadHeadSync(fullPath, Buffer.byteLength(CONTENTS_PREFIX));
|
||||
const exists = typeof head !== 'undefined';
|
||||
|
||||
if (head === CONTENTS_PREFIX) {
|
||||
output.print(`Overwriting existing ${chalk.bold(filename)} file\n`);
|
||||
} else if (
|
||||
exists &&
|
||||
!skipConfirmation &&
|
||||
!(await promptBool(
|
||||
@@ -83,6 +110,7 @@ export default async function pull(
|
||||
});
|
||||
|
||||
const contents =
|
||||
CONTENTS_PREFIX +
|
||||
records
|
||||
.filter(obj => {
|
||||
if (!obj.found) {
|
||||
@@ -95,7 +123,8 @@ export default async function pull(
|
||||
return true;
|
||||
})
|
||||
.map(({ key, value }) => `${key}="${escapeValue(value)}"`)
|
||||
.join('\n') + '\n';
|
||||
.join('\n') +
|
||||
'\n';
|
||||
|
||||
await writeFile(fullPath, contents, 'utf8');
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import checkForUpdate from 'update-check';
|
||||
import ms from 'ms';
|
||||
import { URL } from 'url';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { NowBuildError } from '@vercel/build-utils';
|
||||
import getGlobalPathConfig from './util/config/global-path';
|
||||
import {
|
||||
getDefaultConfig,
|
||||
@@ -114,10 +115,10 @@ const main = async argv_ => {
|
||||
}
|
||||
|
||||
if (
|
||||
localConfig instanceof NowError &&
|
||||
(localConfig instanceof NowError || localConfig instanceof NowBuildError) &&
|
||||
!(localConfig instanceof ERRORS.CantFindConfig)
|
||||
) {
|
||||
output.error(`Failed to load local config file: ${localConfig.message}`);
|
||||
output.prettyError(localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -313,21 +314,6 @@ const main = async argv_ => {
|
||||
// need to migrate.
|
||||
if (authConfig.credentials) {
|
||||
authConfigExists = false;
|
||||
} else if (
|
||||
!authConfig.token &&
|
||||
!subcommandsWithoutToken.includes(targetOrSubcommand) &&
|
||||
!argv['--help'] &&
|
||||
!argv['--token']
|
||||
) {
|
||||
console.error(
|
||||
error(
|
||||
`The content of "${hp(VERCEL_AUTH_CONFIG_PATH)}" is invalid. ` +
|
||||
`No \`token\` property found inside. Run ${getCommandName(
|
||||
'login'
|
||||
)} to authorize.`
|
||||
)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
const results = await getDefaultAuthConfig(authConfig);
|
||||
@@ -637,34 +623,19 @@ const main = async argv_ => {
|
||||
.send();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOTFOUND' && err.hostname === 'api.vercel.com') {
|
||||
output.error(
|
||||
`The hostname ${highlight(
|
||||
'api.vercel.com'
|
||||
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
output.debug(err.stack);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
await reportError(Sentry, err, apiUrl, configFiles);
|
||||
|
||||
// If there is a code we should not consider the error unexpected
|
||||
// but instead show the message. Any error that is handled by this should
|
||||
// actually be handled in the sub command instead. Please make sure
|
||||
// that happens for anything that lands here. It should NOT bubble up to here.
|
||||
if (err.code) {
|
||||
output.debug(err.stack);
|
||||
output.error(err.message);
|
||||
|
||||
if (shouldCollectMetrics) {
|
||||
metric
|
||||
.event(eventCategory, '1', pkg.version)
|
||||
.exception(err.message)
|
||||
.send();
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
// Error message will look like the following:
|
||||
// "request to https://api.vercel.com/www/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
|
||||
const matches = /getaddrinfo ENOTFOUND (.*)$/.exec(err.message || '');
|
||||
if (matches && matches[1]) {
|
||||
const hostname = matches[1];
|
||||
output.error(
|
||||
`The hostname ${highlight(
|
||||
hostname
|
||||
)} could not be resolved. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
|
||||
output.debug(err.stack);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -675,9 +646,23 @@ const main = async argv_ => {
|
||||
.send();
|
||||
}
|
||||
|
||||
// Otherwise it is an unexpected error and we should show the trace
|
||||
// and an unexpected error message
|
||||
output.error(`An unexpected error occurred in ${subcommand}: ${err.stack}`);
|
||||
// If there is a code we should not consider the error unexpected
|
||||
// but instead show the message. Any error that is handled by this should
|
||||
// actually be handled in the sub command instead. Please make sure
|
||||
// that happens for anything that lands here. It should NOT bubble up to here.
|
||||
if (err.code) {
|
||||
output.debug(err.stack);
|
||||
output.prettyError(err);
|
||||
} else {
|
||||
await reportError(Sentry, err, apiUrl, configFiles);
|
||||
|
||||
// Otherwise it is an unexpected error and we should show the trace
|
||||
// and an unexpected error message
|
||||
output.error(
|
||||
`An unexpected error occurred in ${subcommand}: ${err.stack}`
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -688,8 +673,6 @@ const main = async argv_ => {
|
||||
return exitCode;
|
||||
};
|
||||
|
||||
debug('start');
|
||||
|
||||
const handleRejection = async err => {
|
||||
debug('handling rejection');
|
||||
|
||||
|
||||
@@ -244,15 +244,20 @@ export interface ProjectEnvVariable {
|
||||
system?: boolean;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
export interface ProjectSettings {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
}
|
||||
|
||||
export interface Project extends ProjectSettings {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
updatedAt: number;
|
||||
createdAt: number;
|
||||
devCommand?: string | null;
|
||||
framework?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
latestDeployments?: Partial<Deployment>[];
|
||||
}
|
||||
|
||||
|
||||
17
packages/now-cli/src/util/certs/get-certs-for-cn.ts
Normal file
17
packages/now-cli/src/util/certs/get-certs-for-cn.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { stringify } from 'querystring';
|
||||
import { Cert } from '../../types';
|
||||
import Client from '../client';
|
||||
|
||||
/**
|
||||
* Returns certs that contain @param cn.
|
||||
*/
|
||||
export async function getCertsForCn(
|
||||
client: Client,
|
||||
cn: string,
|
||||
{ limit }: { limit?: number } = {}
|
||||
) {
|
||||
const { certs } = await client.fetch<{
|
||||
certs: Cert[];
|
||||
}>(`/v4/now/certs?${stringify({ cn, ...(limit ? { limit } : {}) })}`);
|
||||
return certs;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export default class Client extends EventEmitter {
|
||||
_withCache: boolean;
|
||||
_output: Output;
|
||||
_token: string;
|
||||
currentTeam?: string;
|
||||
currentTeam?: string | null;
|
||||
|
||||
constructor({
|
||||
apiUrl,
|
||||
@@ -36,7 +36,7 @@ export default class Client extends EventEmitter {
|
||||
}: {
|
||||
apiUrl: string;
|
||||
token: string;
|
||||
currentTeam?: string;
|
||||
currentTeam?: string | null;
|
||||
forceNew?: boolean;
|
||||
withCache?: boolean;
|
||||
debug?: boolean;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import isWildcardAlias from '../alias/is-wildcard-alias';
|
||||
import { getCertsForCn } from '../certs/get-certs-for-cn';
|
||||
import Client from '../client';
|
||||
|
||||
/**
|
||||
* Tries to find the "best" alias url.
|
||||
* @param aliasList
|
||||
*/
|
||||
export async function getPreferredPreviewURL(
|
||||
client: Client,
|
||||
aliasList: string[]
|
||||
) {
|
||||
if (aliasList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* First checks for non public aliases and non wildcard domains.
|
||||
*/
|
||||
const preferredAliases = aliasList.filter(
|
||||
alias =>
|
||||
!alias.endsWith('.now.sh') &&
|
||||
!alias.endsWith('.vercel.app') &&
|
||||
!isWildcardAlias(alias)
|
||||
);
|
||||
for (const alias of preferredAliases) {
|
||||
const certs = await getCertsForCn(client, alias, { limit: 1 }).catch(() => {
|
||||
return null;
|
||||
});
|
||||
if (certs && certs.length > 0) {
|
||||
return { previewUrl: `https://${alias}`, isWildcard: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback to first alias
|
||||
*/
|
||||
const [firstAlias] = aliasList;
|
||||
if (isWildcardAlias(firstAlias)) {
|
||||
return { previewUrl: firstAlias, isWildcard: true };
|
||||
}
|
||||
|
||||
if (firstAlias.endsWith('.vercel.app') || firstAlias.endsWith('.now.sh')) {
|
||||
return { previewUrl: `https://${firstAlias}`, isWildcard: false };
|
||||
}
|
||||
|
||||
return { previewUrl: `http://${firstAlias}`, isWildcard: false };
|
||||
}
|
||||
@@ -264,7 +264,7 @@ async function npmInstall(
|
||||
? '`npm` is not installed'
|
||||
: 'Failed to install `vercel dev` dependencies',
|
||||
code: 'NPM_INSTALL_ERROR',
|
||||
link: 'https://vercel.link/npm-install-error',
|
||||
link: 'https://vercel.link/npm-install-failed-dev',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -56,6 +56,8 @@ async function createBuildProcess(
|
||||
workPath: string,
|
||||
output: Output
|
||||
): Promise<ChildProcess> {
|
||||
output.debug(`Creating build process for "${match.entrypoint}"`);
|
||||
|
||||
const builderWorkerPath = join(__dirname, 'builder-worker.js');
|
||||
|
||||
// Ensure that `node` is in the builder's `PATH`
|
||||
@@ -78,7 +80,7 @@ async function createBuildProcess(
|
||||
|
||||
buildProcess.on('exit', (code, signal) => {
|
||||
output.debug(
|
||||
`Build process for ${match.src} exited with ${signal || code}`
|
||||
`Build process for "${match.entrypoint}" exited with ${signal || code}`
|
||||
);
|
||||
match.buildProcess = undefined;
|
||||
});
|
||||
@@ -128,7 +130,6 @@ export async function executeBuild(
|
||||
|
||||
let { buildProcess } = match;
|
||||
if (!runInProcess && !buildProcess) {
|
||||
devServer.output.debug(`Creating build process for ${entrypoint}`);
|
||||
buildProcess = await createBuildProcess(
|
||||
match,
|
||||
envConfigs,
|
||||
@@ -431,7 +432,7 @@ export async function getBuildMatches(
|
||||
}
|
||||
|
||||
const files = fileList
|
||||
.filter(name => name === src || minimatch(name, src))
|
||||
.filter(name => name === src || minimatch(name, src, { dot: true }))
|
||||
.map(name => join(cwd, name));
|
||||
|
||||
if (files.length === 0) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import directoryTemplate from 'serve-handler/src/directory';
|
||||
import getPort from 'get-port';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import which from 'which';
|
||||
|
||||
import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
|
||||
@@ -49,17 +50,7 @@ import { MissingDotenvVarsError } from '../errors-ts';
|
||||
import cliPkg from '../pkg';
|
||||
import { getVercelDirectory } from '../projects/link';
|
||||
import { staticFiles as getFiles, getAllProjectFiles } from '../get-files';
|
||||
import {
|
||||
validateNowConfigBuilds,
|
||||
validateNowConfigRoutes,
|
||||
validateNowConfigCleanUrls,
|
||||
validateNowConfigHeaders,
|
||||
validateNowConfigRedirects,
|
||||
validateNowConfigRewrites,
|
||||
validateNowConfigTrailingSlash,
|
||||
validateNowConfigFunctions,
|
||||
} from './validate';
|
||||
|
||||
import { validateConfig } from './validate';
|
||||
import { devRouter, getRoutesTypes } from './router';
|
||||
import getMimeType from './mime-type';
|
||||
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
|
||||
@@ -92,6 +83,7 @@ import {
|
||||
HttpHeadersConfig,
|
||||
EnvConfigs,
|
||||
} from './types';
|
||||
import { ProjectSettings } from '../../types';
|
||||
|
||||
interface FSEvent {
|
||||
type: string;
|
||||
@@ -116,7 +108,7 @@ export default class DevServer {
|
||||
public output: Output;
|
||||
public proxy: httpProxy;
|
||||
public envConfigs: EnvConfigs;
|
||||
public frameworkSlug: string | null;
|
||||
public frameworkSlug?: string;
|
||||
public files: BuilderInputs;
|
||||
public address: string;
|
||||
public devCacheDir: string;
|
||||
@@ -139,6 +131,7 @@ export default class DevServer {
|
||||
private devProcess?: ChildProcess;
|
||||
private devProcessPort?: number;
|
||||
private devServerPids: Set<number>;
|
||||
private projectSettings?: ProjectSettings;
|
||||
|
||||
private getNowConfigPromise: Promise<NowConfig> | null;
|
||||
private blockingBuildsPromise: Promise<void> | null;
|
||||
@@ -153,6 +146,7 @@ export default class DevServer {
|
||||
this.files = {};
|
||||
this.address = '';
|
||||
this.devCommand = options.devCommand;
|
||||
this.projectSettings = options.projectSettings;
|
||||
this.frameworkSlug = options.frameworkSlug;
|
||||
this.cachedNowConfig = null;
|
||||
this.caseSensitive = false;
|
||||
@@ -234,8 +228,18 @@ export default class DevServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the build matches in case an entrypoint was created or deleted
|
||||
const nowConfig = await this.getNowConfig(false);
|
||||
|
||||
// Update the env vars configuration
|
||||
const nowConfigBuild = nowConfig.build || {};
|
||||
const [runEnv, buildEnv] = await Promise.all([
|
||||
this.getLocalEnv('.env', nowConfig.env),
|
||||
this.getLocalEnv('.env.build', nowConfigBuild.env),
|
||||
]);
|
||||
const allEnv = { ...buildEnv, ...runEnv };
|
||||
this.envConfigs = { buildEnv, runEnv, allEnv };
|
||||
|
||||
// Update the build matches in case an entrypoint was created or deleted
|
||||
await this.updateBuildMatches(nowConfig);
|
||||
|
||||
const filesChangedArray = [...filesChanged];
|
||||
@@ -377,9 +381,7 @@ export default class DevServer {
|
||||
const sources = matches.map(m => m.src);
|
||||
|
||||
if (isInitial && fileList.length === 0) {
|
||||
this.output.warn(
|
||||
'There are no files (or only files starting with a dot) inside your deployment.'
|
||||
);
|
||||
this.output.warn('There are no files inside your deployment.');
|
||||
}
|
||||
|
||||
// Delete build matches that no longer exists
|
||||
@@ -400,7 +402,7 @@ export default class DevServer {
|
||||
const blockingBuilds: Promise<void>[] = [];
|
||||
for (const match of matches) {
|
||||
const currentMatch = this.buildMatches.get(match.src);
|
||||
if (!currentMatch || currentMatch.use !== match.use) {
|
||||
if (!buildMatchEquals(currentMatch, match)) {
|
||||
this.output.debug(
|
||||
`Adding build match for "${match.src}" with "${match.use}"`
|
||||
);
|
||||
@@ -435,6 +437,9 @@ export default class DevServer {
|
||||
`Cleaning up "blockingBuildsPromise" after error: ${err}`
|
||||
);
|
||||
this.blockingBuildsPromise = null;
|
||||
if (err) {
|
||||
this.output.prettyError(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -522,23 +527,16 @@ export default class DevServer {
|
||||
// The default empty `vercel.json` is used to serve all files as static
|
||||
// when no `vercel.json` is present
|
||||
let configPath = 'vercel.json';
|
||||
let config: NowConfig = this.cachedNowConfig || {
|
||||
let config: NowConfig = {
|
||||
version: 2,
|
||||
[fileNameSymbol]: configPath,
|
||||
};
|
||||
|
||||
// We need to delete these properties for zero config to work
|
||||
// with file changes
|
||||
if (this.cachedNowConfig) {
|
||||
delete this.cachedNowConfig.builds;
|
||||
delete this.cachedNowConfig.routes;
|
||||
}
|
||||
|
||||
try {
|
||||
configPath = getNowConfigPath(this.cwd);
|
||||
this.output.debug(`Reading ${configPath}`);
|
||||
config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
||||
config[fileNameSymbol] = configPath;
|
||||
config[fileNameSymbol] = basename(configPath);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
this.output.debug(err.toString());
|
||||
@@ -585,7 +583,7 @@ export default class DevServer {
|
||||
} = await detectBuilders(files, pkg, {
|
||||
tag: getDistTag(cliPkg.version) === 'canary' ? 'canary' : 'latest',
|
||||
functions: config.functions,
|
||||
...(projectSettings ? { projectSettings } : {}),
|
||||
projectSettings: projectSettings || this.projectSettings,
|
||||
featHandleMiss,
|
||||
cleanUrls,
|
||||
trailingSlash,
|
||||
@@ -689,14 +687,12 @@ export default class DevServer {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.tryValidateOrExit(config, validateNowConfigBuilds);
|
||||
await this.tryValidateOrExit(config, validateNowConfigRoutes);
|
||||
await this.tryValidateOrExit(config, validateNowConfigCleanUrls);
|
||||
await this.tryValidateOrExit(config, validateNowConfigHeaders);
|
||||
await this.tryValidateOrExit(config, validateNowConfigRedirects);
|
||||
await this.tryValidateOrExit(config, validateNowConfigRewrites);
|
||||
await this.tryValidateOrExit(config, validateNowConfigTrailingSlash);
|
||||
await this.tryValidateOrExit(config, validateNowConfigFunctions);
|
||||
const error = validateConfig(config);
|
||||
|
||||
if (error) {
|
||||
this.output.prettyError(error);
|
||||
await this.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
validateEnvConfig(type: string, env: Env = {}, localEnv: Env = {}): Env {
|
||||
@@ -773,7 +769,6 @@ export default class DevServer {
|
||||
this.getLocalEnv('.env.build', nowConfigBuild.env),
|
||||
]);
|
||||
const allEnv = { ...buildEnv, ...runEnv };
|
||||
Object.assign(process.env, allEnv);
|
||||
this.envConfigs = { buildEnv, runEnv, allEnv };
|
||||
|
||||
const opts = { output: this.output, isBuilds: true };
|
||||
@@ -1500,16 +1495,15 @@ export default class DevServer {
|
||||
if (!match) {
|
||||
// If the dev command is started, then proxy to it
|
||||
if (this.devProcessPort) {
|
||||
debug('Proxying to frontend dev server');
|
||||
const upstream = `http://localhost:${this.devProcessPort}`;
|
||||
debug(`Proxying to frontend dev server: ${upstream}`);
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
return proxyPass(
|
||||
req,
|
||||
res,
|
||||
`http://localhost:${this.devProcessPort}`,
|
||||
this,
|
||||
nowRequestId,
|
||||
false
|
||||
);
|
||||
const origUrl = url.parse(req.url || '/', true);
|
||||
delete origUrl.search;
|
||||
origUrl.pathname = dest;
|
||||
Object.assign(origUrl.query, uri_args);
|
||||
req.url = url.format(origUrl);
|
||||
return proxyPass(req, res, upstream, this, nowRequestId, false);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -2200,7 +2194,9 @@ function isIndex(path: string): boolean {
|
||||
}
|
||||
|
||||
function minimatches(files: string[], pattern: string): boolean {
|
||||
return files.some(file => file === pattern || minimatch(file, pattern));
|
||||
return files.some(
|
||||
file => file === pattern || minimatch(file, pattern, { dot: true })
|
||||
);
|
||||
}
|
||||
|
||||
function fileChanged(
|
||||
@@ -2261,3 +2257,11 @@ function hasNewRoutingProperties(nowConfig: NowConfig) {
|
||||
typeof nowConfig.trailingSlash !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function buildMatchEquals(a?: BuildMatch, b?: BuildMatch): boolean {
|
||||
if (!a || !b) return false;
|
||||
if (a.src !== b.src) return false;
|
||||
if (a.use !== b.use) return false;
|
||||
if (!deepEqual(a.config || {}, b.config || {})) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@ import {
|
||||
import { NowConfig } from '@vercel/client';
|
||||
import { HandleValue, Route } from '@vercel/routing-utils';
|
||||
import { Output } from '../output';
|
||||
import { ProjectSettings } from '../../types';
|
||||
|
||||
export { NowConfig };
|
||||
|
||||
export interface DevServerOptions {
|
||||
output: Output;
|
||||
debug: boolean;
|
||||
devCommand: string | undefined;
|
||||
frameworkSlug: string | null;
|
||||
devCommand?: string;
|
||||
frameworkSlug?: string;
|
||||
projectSettings?: ProjectSettings;
|
||||
}
|
||||
|
||||
export interface EnvConfigs {
|
||||
|
||||
@@ -8,71 +8,44 @@ import {
|
||||
trailingSlashSchema,
|
||||
} from '@vercel/routing-utils';
|
||||
import { NowConfig } from './types';
|
||||
import { functionsSchema, buildsSchema } from '@vercel/build-utils';
|
||||
import {
|
||||
functionsSchema,
|
||||
buildsSchema,
|
||||
NowBuildError,
|
||||
getPrettyError,
|
||||
} from '@vercel/build-utils';
|
||||
import { fileNameSymbol } from '@vercel/client';
|
||||
|
||||
const vercelConfigSchema = {
|
||||
type: 'object',
|
||||
// These are not all possibilities because `vc dev`
|
||||
// doesn't need to know about `regions`, `public`, etc.
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
builds: buildsSchema,
|
||||
routes: routesSchema,
|
||||
cleanUrls: cleanUrlsSchema,
|
||||
headers: headersSchema,
|
||||
redirects: redirectsSchema,
|
||||
rewrites: rewritesSchema,
|
||||
trailingSlash: trailingSlashSchema,
|
||||
functions: functionsSchema,
|
||||
},
|
||||
};
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
const validateBuilds = ajv.compile(buildsSchema);
|
||||
const validateRoutes = ajv.compile(routesSchema);
|
||||
const validateCleanUrls = ajv.compile(cleanUrlsSchema);
|
||||
const validateHeaders = ajv.compile(headersSchema);
|
||||
const validateRedirects = ajv.compile(redirectsSchema);
|
||||
const validateRewrites = ajv.compile(rewritesSchema);
|
||||
const validateTrailingSlash = ajv.compile(trailingSlashSchema);
|
||||
const validateFunctions = ajv.compile(functionsSchema);
|
||||
export function validateConfig(config: NowConfig): NowBuildError | null {
|
||||
const validate = ajv.compile(vercelConfigSchema);
|
||||
|
||||
export function validateNowConfigBuilds(config: NowConfig) {
|
||||
return validateKey(config, 'builds', validateBuilds);
|
||||
}
|
||||
|
||||
export function validateNowConfigRoutes(config: NowConfig) {
|
||||
return validateKey(config, 'routes', validateRoutes);
|
||||
}
|
||||
|
||||
export function validateNowConfigCleanUrls(config: NowConfig) {
|
||||
return validateKey(config, 'cleanUrls', validateCleanUrls);
|
||||
}
|
||||
|
||||
export function validateNowConfigHeaders(config: NowConfig) {
|
||||
return validateKey(config, 'headers', validateHeaders);
|
||||
}
|
||||
|
||||
export function validateNowConfigRedirects(config: NowConfig) {
|
||||
return validateKey(config, 'redirects', validateRedirects);
|
||||
}
|
||||
|
||||
export function validateNowConfigRewrites(config: NowConfig) {
|
||||
return validateKey(config, 'rewrites', validateRewrites);
|
||||
}
|
||||
|
||||
export function validateNowConfigTrailingSlash(config: NowConfig) {
|
||||
return validateKey(config, 'trailingSlash', validateTrailingSlash);
|
||||
}
|
||||
|
||||
export function validateNowConfigFunctions(config: NowConfig) {
|
||||
return validateKey(config, 'functions', validateFunctions);
|
||||
}
|
||||
|
||||
function validateKey(
|
||||
config: NowConfig,
|
||||
key: keyof NowConfig,
|
||||
validate: Ajv.ValidateFunction
|
||||
) {
|
||||
const value = config[key];
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!validate(value)) {
|
||||
if (!validate.errors) {
|
||||
return null;
|
||||
if (!validate(config)) {
|
||||
if (validate.errors && validate.errors[0]) {
|
||||
const error = validate.errors[0];
|
||||
const fileName = config[fileNameSymbol] || 'vercel.json';
|
||||
const niceError = getPrettyError(error);
|
||||
niceError.message = `Invalid ${fileName} - ${niceError.message}`;
|
||||
return niceError;
|
||||
}
|
||||
|
||||
const error = validate.errors[0];
|
||||
|
||||
return `Invalid \`${String(key)}\` property: ${error.dataPath} ${
|
||||
error.message
|
||||
}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import bytes from 'bytes';
|
||||
import { Response } from 'node-fetch';
|
||||
import { NowBuildError } from '@vercel/build-utils';
|
||||
import { NowError } from './now-error';
|
||||
import code from './output/code';
|
||||
import { getCommandName } from './pkg-name';
|
||||
@@ -771,17 +772,17 @@ export class CantParseJSONFile extends NowError<
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictingConfigFiles extends NowError<
|
||||
'CONFLICTING_CONFIG_FILES',
|
||||
{ files: string[] }
|
||||
> {
|
||||
export class ConflictingConfigFiles extends NowBuildError {
|
||||
files: string[];
|
||||
|
||||
constructor(files: string[]) {
|
||||
super({
|
||||
code: 'CONFLICTING_CONFIG_FILES',
|
||||
meta: { files },
|
||||
message:
|
||||
'Cannot use both a `vercel.json` and `now.json` file. Please delete the `now.json` file.',
|
||||
link: 'https://vercel.link/combining-old-and-new-config',
|
||||
});
|
||||
this.files = files;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ export default class Now extends EventEmitter {
|
||||
regions,
|
||||
target: target || undefined,
|
||||
projectSettings,
|
||||
source: 'cli',
|
||||
};
|
||||
|
||||
// Ignore specific items from vercel.json
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { metrics, shouldCollectMetrics } from '../metrics';
|
||||
import { APIError } from '../errors-ts';
|
||||
import renderLink from './link';
|
||||
|
||||
const metric = metrics();
|
||||
|
||||
@@ -9,10 +10,9 @@ export default function error(...input: string[] | [APIError]) {
|
||||
if (typeof input[0] === 'object') {
|
||||
const { slug, message, link } = input[0];
|
||||
messages = [message];
|
||||
if (slug) {
|
||||
messages.push(`> More details: https://err.sh/now/${slug}`);
|
||||
} else if (link) {
|
||||
messages.push(`> More details: ${link}`);
|
||||
const details = slug ? `https://err.sh/now/${slug}` : link;
|
||||
if (details) {
|
||||
messages.push(`${chalk.bold('More details')}: ${renderLink(details)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import chalk from 'chalk';
|
||||
import { prependEmoji, emoji } from '../emoji';
|
||||
import AJV from 'ajv';
|
||||
import { isDirectory } from '../config/global-path';
|
||||
import { getPlatformEnv } from '@vercel/build-utils';
|
||||
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
@@ -49,9 +49,12 @@ export function getVercelDirectory(cwd: string = process.cwd()): string {
|
||||
const possibleDirs = [join(cwd, VERCEL_DIR), join(cwd, VERCEL_DIR_FALLBACK)];
|
||||
const existingDirs = possibleDirs.filter(d => isDirectory(d));
|
||||
if (existingDirs.length > 1) {
|
||||
throw new Error(
|
||||
'Both `.vercel` and `.now` directories exist. Please remove the `.now` directory.'
|
||||
);
|
||||
throw new NowBuildError({
|
||||
code: 'CONFLICTING_CONFIG_DIRECTORIES',
|
||||
message:
|
||||
'Both `.vercel` and `.now` directories exist. Please remove the `.now` directory.',
|
||||
link: 'https://vercel.link/combining-old-and-new-config',
|
||||
});
|
||||
}
|
||||
return existingDirs[0] || possibleDirs[0];
|
||||
}
|
||||
|
||||
262
packages/now-cli/test/dev-validate.unit.js
vendored
Normal file
262
packages/now-cli/test/dev-validate.unit.js
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
import test from 'ava';
|
||||
import { validateConfig } from '../src/util/dev/validate';
|
||||
|
||||
test('[dev-validate] should not error with empty config', async t => {
|
||||
const config = {};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(error, null);
|
||||
});
|
||||
|
||||
test('[dev-validate] should not error with complete config', async t => {
|
||||
const config = {
|
||||
version: 2,
|
||||
public: true,
|
||||
regions: ['sfo1', 'iad1'],
|
||||
builds: [{ src: 'package.json', use: '@vercel/next' }],
|
||||
cleanUrls: true,
|
||||
headers: [{ source: '/', headers: [{ key: 'x-id', value: '123' }] }],
|
||||
rewrites: [{ source: '/help', destination: '/support' }],
|
||||
redirects: [{ source: '/kb', destination: 'https://example.com' }],
|
||||
trailingSlash: false,
|
||||
functions: { 'api/user.go': { memory: 128, maxDuration: 5 } },
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(error, null);
|
||||
});
|
||||
|
||||
test('[dev-validate] should not error with builds and routes', async t => {
|
||||
const config = {
|
||||
builds: [{ src: 'api/index.js', use: '@vercel/node' }],
|
||||
routes: [{ src: '/(.*)', dest: '/api/index.js' }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(error, null);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid rewrites due to additional property and offer suggestion', async t => {
|
||||
const config = {
|
||||
rewrites: [{ src: '/(.*)', dest: '/api/index.js' }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `rewrites[0]` should NOT have additional property `src`. Did you mean `source`?'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/rewrites'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid routes due to additional property and offer suggestion', async t => {
|
||||
const config = {
|
||||
routes: [{ source: '/(.*)', destination: '/api/index.js' }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `routes[0]` should NOT have additional property `source`. Did you mean `src`?'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/routes'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid routes array type', async t => {
|
||||
const config = {
|
||||
routes: { src: '/(.*)', dest: '/api/index.js' },
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(error.message, 'Invalid vercel.json - `routes` should be array.');
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/routes'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid redirects array object', async t => {
|
||||
const config = {
|
||||
redirects: [
|
||||
{
|
||||
/* intentionally empty */
|
||||
},
|
||||
],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `redirects[0]` missing required property `source`.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/redirects'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid redirects.permanent poperty', async t => {
|
||||
const config = {
|
||||
redirects: [{ source: '/', destination: '/go', permanent: 'yes' }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `redirects[0].permanent` should be boolean.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/redirects'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid cleanUrls type', async t => {
|
||||
const config = {
|
||||
cleanUrls: 'true',
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `cleanUrls` should be boolean.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/cleanurls'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid trailingSlash type', async t => {
|
||||
const config = {
|
||||
trailingSlash: [true],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `trailingSlash` should be boolean.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/trailingslash'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid headers property', async t => {
|
||||
const config = {
|
||||
headers: [{ 'Content-Type': 'text/html' }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `headers[0]` should NOT have additional property `Content-Type`. Please remove it.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/headers'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid headers.source type', async t => {
|
||||
const config = {
|
||||
headers: [{ source: [{ 'Content-Type': 'text/html' }] }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `headers[0].source` should be string.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/headers'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid headers additional property', async t => {
|
||||
const config = {
|
||||
headers: [{ source: '/', stuff: [{ 'Content-Type': 'text/html' }] }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `headers[0]` should NOT have additional property `stuff`. Please remove it.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/headers'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid headers wrong nested headers type', async t => {
|
||||
const config = {
|
||||
headers: [{ source: '/', headers: [{ 'Content-Type': 'text/html' }] }],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `headers[0].headers[0]` should NOT have additional property `Content-Type`. Please remove it.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/headers'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with invalid headers wrong nested headers additional property', async t => {
|
||||
const config = {
|
||||
headers: [
|
||||
{ source: '/', headers: [{ key: 'Content-Type', val: 'text/html' }] },
|
||||
],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `headers[0].headers[0]` should NOT have additional property `val`. Please remove it.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/headers'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with too many redirects', async t => {
|
||||
const config = {
|
||||
redirects: Array.from({ length: 5000 }).map((_, i) => ({
|
||||
source: `/${i}`,
|
||||
destination: `/v/${i}`,
|
||||
})),
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `redirects` should NOT have more than 1024 items.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/redirects'
|
||||
);
|
||||
});
|
||||
|
||||
test('[dev-validate] should error with too many nested headers', async t => {
|
||||
const config = {
|
||||
headers: [
|
||||
{
|
||||
source: '/',
|
||||
headers: [{ key: `x-id`, value: `123` }],
|
||||
},
|
||||
{
|
||||
source: '/too-many',
|
||||
headers: Array.from({ length: 5000 }).map((_, i) => ({
|
||||
key: `${i}`,
|
||||
value: `${i}`,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
const error = validateConfig(config);
|
||||
t.deepEqual(
|
||||
error.message,
|
||||
'Invalid vercel.json - `headers[1].headers` should NOT have more than 1024 items.'
|
||||
);
|
||||
t.deepEqual(
|
||||
error.link,
|
||||
'https://vercel.com/docs/configuration#project/headers'
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
proof goes here
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "gridsome build",
|
||||
"develop": "gridsome develop",
|
||||
"dev": "gridsome develop -p $PORT",
|
||||
"explore": "gridsome explore"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<h1>Not Found</h1>
|
||||
<p>This is a Custom Gridsome 404.</p>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
metaInfo: {
|
||||
title: 'Not Found'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Layout>
|
||||
|
||||
|
||||
<!-- Learn how to use images here: https://gridsome.org/docs/images -->
|
||||
<g-image alt="Example image" src="~/favicon.png" width="135" />
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
|
||||
<h1>Gridsome on Vercel</h1>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Pariatur excepturi labore tempore expedita, et iste tenetur suscipit explicabo! Dolores, aperiam non officia eos quod asperiores
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"redirects": [{ "source": "/support", "destination": "/about?ref=support" }]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<h1>Contact Us</h1>
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rewrites": [{ "source": "/support", "destination": "/contact.html" }]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Contact() {
|
||||
return <h1>Contact Page</h1>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rewrites": [{ "source": "/support", "destination": "/contact" }]
|
||||
}
|
||||
1
packages/now-cli/test/dev/fixtures/force-module-commonjs/.gitignore
vendored
Normal file
1
packages/now-cli/test/dev/fixtures/force-module-commonjs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vercel
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function(req, res) {
|
||||
res.end('Force "module: commonjs" JavaScript with ES Modules API endpoint');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
export default function(req: IncomingMessage, res: ServerResponse) {
|
||||
res.end('Force "module: commonjs" TypeScript API endpoint');
|
||||
}
|
||||
2
packages/now-cli/test/dev/fixtures/force-module-commonjs/next-env.d.ts
vendored
Normal file
2
packages/now-cli/test/dev/fixtures/force-module-commonjs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "force-module-commonjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^9.3.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "14.0.9",
|
||||
"@types/react": "^16.9.32",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function () {
|
||||
return <div>Force "module: commonjs" test page</div>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"paths": {
|
||||
"@components/*": ["components/*"],
|
||||
"@lib/*": ["lib/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "api"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
5633
packages/now-cli/test/dev/fixtures/force-module-commonjs/yarn.lock
Normal file
5633
packages/now-cli/test/dev/fixtures/force-module-commonjs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/now-cli/test/dev/fixtures/go/.gitignore
vendored
Normal file
1
packages/now-cli/test/dev/fixtures/go/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vercel
|
||||
10
packages/now-cli/test/dev/fixtures/go/api/[segment].go
Normal file
10
packages/now-cli/test/dev/fixtures/go/api/[segment].go
Normal 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)
|
||||
}
|
||||
10
packages/now-cli/test/dev/fixtures/go/api/another.go
Normal file
10
packages/now-cli/test/dev/fixtures/go/api/another.go
Normal 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")
|
||||
}
|
||||
10
packages/now-cli/test/dev/fixtures/go/api/index.go
Normal file
10
packages/now-cli/test/dev/fixtures/go/api/index.go
Normal 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")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default (req, res) => {
|
||||
res.send(process.env);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{ "env": { "FOO": "bar" } }
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
assetPrefix: '/blog',
|
||||
env: {
|
||||
ASSET_PREFIX: '/blog',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "with-zones-blog",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"next": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest"
|
||||
},
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Blog() {
|
||||
return (
|
||||
<div>
|
||||
<h3>This is our blog</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/blog/post/[id]" as="/blog/post/1">
|
||||
<a>Post 1</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/blog/post/[id]" as="/blog/post/2">
|
||||
<a>Post 2</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/">Home</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Post() {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div>
|
||||
<h3>Post #{router.query.id}</h3>
|
||||
<p>Lorem ipsum</p>
|
||||
<Link href="/blog">
|
||||
<a>Back to blog</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const Header = () => (
|
||||
<div>
|
||||
<h2>The Company</h2>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "with-zones-home",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"next": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest"
|
||||
},
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
const About = ({ id }) => {
|
||||
return (
|
||||
<div>
|
||||
<p>This is the {id} page with static props.</p>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<a>Go Back</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps = ({ params }) => {
|
||||
return { props: { id: params.id } };
|
||||
};
|
||||
export const getStaticPaths = () => ({ paths: ['/1/dynamic'], fallback: true });
|
||||
|
||||
export default About;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const About = () => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
return (
|
||||
<div>
|
||||
<p>This is the {id} page without static props.</p>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<a>Go Back</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const About = () => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
return (
|
||||
<div>
|
||||
<p>This is the about static page.</p>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<a>Go Back</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -0,0 +1,36 @@
|
||||
import Link from 'next/link';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Header = dynamic(import('../components/Header'));
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<p>This is our homepage</p>
|
||||
<div>
|
||||
<a href="/blog">Blog</a>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/about">
|
||||
<a>About us</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/foo">
|
||||
<a>foo</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/1/dynamic">
|
||||
<a>1/dynamic</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/1/foo">
|
||||
<a>1/foo</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "with-zones-example",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"home",
|
||||
"blog"
|
||||
],
|
||||
"devDependencies": {
|
||||
"vercel": "canary"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{ "src": "blog/package.json", "use": "@vercel/next@canary" },
|
||||
{ "src": "home/package.json", "use": "@vercel/next@canary" }
|
||||
],
|
||||
"routes": [
|
||||
{ "src": "/blog/_next(.*)", "dest": "blog/_next$1" },
|
||||
{ "src": "/blog(.*)", "dest": "blog/blog$1" },
|
||||
{ "src": "(.*)", "dest": "home$1" }
|
||||
]
|
||||
}
|
||||
5682
packages/now-cli/test/dev/fixtures/monorepo-dynamic-paths/yarn.lock
Normal file
5682
packages/now-cli/test/dev/fixtures/monorepo-dynamic-paths/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/now-cli/test/dev/fixtures/node-helpers/index.js
Normal file
10
packages/now-cli/test/dev/fixtures/node-helpers/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default (req, res) => {
|
||||
const hasHelpers = typeof req.query !== 'undefined';
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
hasHelpers,
|
||||
query: req.query,
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{ "builds": [{ "src": "index.js", "use": "@vercel/node@canary" }] }
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = (req, res) => {
|
||||
const months = [...Array(12).keys()].map(month => month + 1);
|
||||
res.json({ months });
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
const { PCRE } = require('pcre-to-regexp');
|
||||
|
||||
// `ts-node` default "target" is "es5" which transpiles `class` statements and
|
||||
// is incompatible with dependencies that use ES6 native `class`. Setting the
|
||||
// "target" to "es2018" or newer prevents the `class` transpilation.
|
||||
//
|
||||
// See: https://github.com/vercel/vercel/discussions/4724
|
||||
// See: https://github.com/TypeStrong/ts-node/issues/903
|
||||
class P extends PCRE {}
|
||||
|
||||
export default (req, res) => {
|
||||
const p = new P('hi'); // This line should not throw an error
|
||||
console.log(p);
|
||||
res.send({ ok: true });
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "node-ts-node-target",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pcre-to-regexp": "1.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
pcre-to-regexp@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/pcre-to-regexp/-/pcre-to-regexp-1.1.0.tgz#1c48373d194b982e1416031b41470839fab3ad6c"
|
||||
integrity sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g==
|
||||
@@ -0,0 +1 @@
|
||||
We come in peace
|
||||
@@ -1,4 +1,5 @@
|
||||
import ms from 'ms';
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import test from 'ava';
|
||||
import { join, resolve, delimiter } from 'path';
|
||||
@@ -161,7 +162,7 @@ async function testPath(
|
||||
Object.entries(headers).forEach(([key, expectedValue]) => {
|
||||
let actualValue = res.headers.get(key);
|
||||
if (key.toLowerCase() === 'location' && actualValue === '//') {
|
||||
// HACK: `node-fetch` has strang behavior for location header so fix it
|
||||
// HACK: `node-fetch` has strange behavior for location header so fix it
|
||||
// with `manual-dont-change` opt and convert double slash to single.
|
||||
// See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352
|
||||
actualValue = '/';
|
||||
@@ -187,20 +188,29 @@ async function testFixture(directory, opts = {}, args = []) {
|
||||
}
|
||||
);
|
||||
|
||||
const stdoutList = [];
|
||||
const stderrList = [];
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const readyResolver = createResolver();
|
||||
const exitResolver = createResolver();
|
||||
|
||||
dev.stderr.on('data', data => stderrList.push(Buffer.from(data)));
|
||||
dev.stdout.on('data', data => stdoutList.push(Buffer.from(data)));
|
||||
dev.stdout.setEncoding('utf8');
|
||||
dev.stderr.setEncoding('utf8');
|
||||
|
||||
dev.stdout.on('data', data => {
|
||||
stdout += data;
|
||||
});
|
||||
dev.stderr.on('data', data => {
|
||||
stderr += data;
|
||||
|
||||
if (stderr.includes('Ready! Available at')) {
|
||||
readyResolver.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
let printedOutput = false;
|
||||
|
||||
dev.on('exit', () => {
|
||||
if (!printedOutput) {
|
||||
const stdout = Buffer.concat(stdoutList).toString();
|
||||
const stderr = Buffer.concat(stderrList).toString();
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
@@ -209,8 +219,6 @@ async function testFixture(directory, opts = {}, args = []) {
|
||||
|
||||
dev.on('error', () => {
|
||||
if (!printedOutput) {
|
||||
const stdout = Buffer.concat(stdoutList).toString();
|
||||
const stderr = Buffer.concat(stderrList).toString();
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
@@ -226,6 +234,7 @@ async function testFixture(directory, opts = {}, args = []) {
|
||||
return {
|
||||
dev,
|
||||
port,
|
||||
readyResolver,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,14 +276,12 @@ function testFixtureStdio(
|
||||
|
||||
await runNpmInstall(cwd);
|
||||
|
||||
const stdoutList = [];
|
||||
const stderrList = [];
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const readyResolver = createResolver();
|
||||
const exitResolver = createResolver();
|
||||
|
||||
try {
|
||||
let stderr = '';
|
||||
let printedOutput = false;
|
||||
|
||||
const env = skipDeploy
|
||||
@@ -285,17 +292,19 @@ function testFixtureStdio(
|
||||
env,
|
||||
});
|
||||
|
||||
dev.stdout.setEncoding('utf8');
|
||||
dev.stderr.setEncoding('utf8');
|
||||
|
||||
dev.stdout.pipe(process.stdout);
|
||||
dev.stderr.pipe(process.stderr);
|
||||
|
||||
dev.stdout.on('data', data => {
|
||||
stdoutList.push(data);
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
dev.stderr.on('data', data => {
|
||||
stderrList.push(data);
|
||||
stderr += data;
|
||||
|
||||
stderr += data.toString();
|
||||
if (stderr.includes('Ready! Available at')) {
|
||||
readyResolver.resolve();
|
||||
}
|
||||
@@ -315,8 +324,6 @@ function testFixtureStdio(
|
||||
|
||||
dev.on('exit', () => {
|
||||
if (!printedOutput) {
|
||||
const stdout = Buffer.concat(stdoutList).toString();
|
||||
const stderr = Buffer.concat(stderrList).toString();
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
@@ -325,8 +332,6 @@ function testFixtureStdio(
|
||||
|
||||
dev.on('error', () => {
|
||||
if (!printedOutput) {
|
||||
const stdout = Buffer.concat(stdoutList).toString();
|
||||
const stderr = Buffer.concat(stderrList).toString();
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
@@ -382,7 +387,173 @@ test('[vercel dev] prints `npm install` errors', async t => {
|
||||
t.truthy(
|
||||
result.stderr.includes('Failed to install `vercel dev` dependencies')
|
||||
);
|
||||
t.truthy(result.stderr.includes('https://vercel.link/npm-install-error'));
|
||||
t.truthy(
|
||||
result.stderr.includes('https://vercel.link/npm-install-failed-dev')
|
||||
);
|
||||
});
|
||||
|
||||
test('[vercel dev] `vercel.json` should be invalidated if deleted', async t => {
|
||||
const dir = fixture('invalidate-vercel-config');
|
||||
const configPath = join(dir, 'vercel.json');
|
||||
const originalConfig = await fs.readJSON(configPath);
|
||||
const { dev, port, readyResolver } = await testFixture(dir);
|
||||
|
||||
try {
|
||||
await readyResolver;
|
||||
|
||||
{
|
||||
// Env var should be set from `vercel.json`
|
||||
const res = await fetch(`http://localhost:${port}/api`);
|
||||
const body = await res.json();
|
||||
t.is(body.FOO, 'bar');
|
||||
}
|
||||
|
||||
{
|
||||
// Env var should not be set after `vercel.json` is deleted
|
||||
await fs.remove(configPath);
|
||||
await sleep(1000);
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/api`);
|
||||
const body = await res.json();
|
||||
t.is(body.FOO, undefined);
|
||||
}
|
||||
} finally {
|
||||
await dev.kill('SIGTERM');
|
||||
await fs.writeJSON(configPath, originalConfig);
|
||||
}
|
||||
});
|
||||
|
||||
test('[vercel dev] reflects changes to config and env without restart', async t => {
|
||||
const dir = fixture('node-helpers');
|
||||
const configPath = join(dir, 'vercel.json');
|
||||
const originalConfig = await fs.readJSON(configPath);
|
||||
const { dev, port, readyResolver } = await testFixture(dir);
|
||||
|
||||
try {
|
||||
await readyResolver;
|
||||
|
||||
{
|
||||
// Node.js helpers should be available by default
|
||||
const res = await fetch(`http://localhost:${port}/?foo=bar`);
|
||||
const body = await res.json();
|
||||
t.is(body.hasHelpers, true);
|
||||
t.is(body.query.foo, 'bar');
|
||||
}
|
||||
|
||||
{
|
||||
// Disable the helpers via `config.helpers = false`
|
||||
const config = {
|
||||
...originalConfig,
|
||||
builds: [
|
||||
{
|
||||
...originalConfig.builds[0],
|
||||
config: {
|
||||
helpers: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await fs.writeJSON(configPath, config);
|
||||
await sleep(1000);
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/?foo=bar`);
|
||||
const body = await res.json();
|
||||
t.is(body.hasHelpers, false);
|
||||
t.is(body.query, undefined);
|
||||
}
|
||||
|
||||
{
|
||||
// Enable the helpers via `config.helpers = true`
|
||||
const config = {
|
||||
...originalConfig,
|
||||
builds: [
|
||||
{
|
||||
...originalConfig.builds[0],
|
||||
config: {
|
||||
helpers: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await fs.writeJSON(configPath, config);
|
||||
await sleep(1000);
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/?foo=baz`);
|
||||
const body = await res.json();
|
||||
t.is(body.hasHelpers, true);
|
||||
t.is(body.query.foo, 'baz');
|
||||
}
|
||||
|
||||
{
|
||||
// Disable the helpers via `NODEJS_HELPERS = '0'`
|
||||
const config = {
|
||||
...originalConfig,
|
||||
build: {
|
||||
env: {
|
||||
NODEJS_HELPERS: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
await fs.writeJSON(configPath, config);
|
||||
await sleep(1000);
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/?foo=baz`);
|
||||
const body = await res.json();
|
||||
t.is(body.hasHelpers, false);
|
||||
t.is(body.query, undefined);
|
||||
}
|
||||
|
||||
{
|
||||
// Enable the helpers via `NODEJS_HELPERS = '1'`
|
||||
const config = {
|
||||
...originalConfig,
|
||||
build: {
|
||||
env: {
|
||||
NODEJS_HELPERS: '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
await fs.writeJSON(configPath, config);
|
||||
await sleep(1000);
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/?foo=boo`);
|
||||
const body = await res.json();
|
||||
t.is(body.hasHelpers, true);
|
||||
t.is(body.query.foo, 'boo');
|
||||
}
|
||||
} finally {
|
||||
await dev.kill('SIGTERM');
|
||||
await fs.writeJSON(configPath, originalConfig);
|
||||
}
|
||||
});
|
||||
|
||||
test('[vercel dev] `@vercel/node` TypeScript should be resolved by default', async t => {
|
||||
// The purpose of this test is to test that `@vercel/node` can properly
|
||||
// resolve the default "typescript" module when the project doesn't include
|
||||
// its own version. To properly test for this, a fixture needs to be created
|
||||
// *outside* of the `vercel` repo, since otherwise the root-level
|
||||
// "node_modules/typescript" is resolved as relative to the project, and
|
||||
// not relative to `@vercel/node` which is what we are testing for here.
|
||||
const dir = join(os.tmpdir(), 'vercel-node-typescript-resolve-test');
|
||||
const apiDir = join(dir, 'api');
|
||||
await fs.mkdirp(apiDir);
|
||||
await fs.writeFile(
|
||||
join(apiDir, 'hello.js'),
|
||||
'export default (req, res) => { res.end("world"); }'
|
||||
);
|
||||
|
||||
const { dev, port, readyResolver } = await testFixture(dir);
|
||||
|
||||
try {
|
||||
await readyResolver;
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/api/hello`);
|
||||
const body = await res.text();
|
||||
t.is(body, 'world');
|
||||
} finally {
|
||||
await dev.kill('SIGTERM');
|
||||
await fs.remove(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -495,6 +666,7 @@ test(
|
||||
testFixtureStdio('public-and-api', async testPath => {
|
||||
await testPath(200, '/', 'This is the home page');
|
||||
await testPath(200, '/about.html', 'This is the about page');
|
||||
await testPath(200, '/.well-known/humans.txt', 'We come in peace');
|
||||
await testPath(200, '/api/date', /current date/);
|
||||
await testPath(200, '/api/rand', /random number/);
|
||||
await testPath(200, '/api/rand.js', /random number/);
|
||||
@@ -523,7 +695,7 @@ test('[vercel dev] validate builds', async t => {
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid `builds` property: \[0\]\.src should be string/m
|
||||
/Invalid vercel\.json - `builds\[0\].src` should be string/m
|
||||
);
|
||||
});
|
||||
|
||||
@@ -534,7 +706,7 @@ test('[vercel dev] validate routes', async t => {
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid `routes` property: \[0\]\.src should be string/m
|
||||
/Invalid vercel\.json - `routes\[0\].src` should be string/m
|
||||
);
|
||||
});
|
||||
|
||||
@@ -543,7 +715,10 @@ test('[vercel dev] validate cleanUrls', async t => {
|
||||
const output = await exec(directory);
|
||||
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(output.stderr, /Invalid `cleanUrls` property:\s+should be boolean/m);
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid vercel\.json - `cleanUrls` should be boolean/m
|
||||
);
|
||||
});
|
||||
|
||||
test('[vercel dev] validate trailingSlash', async t => {
|
||||
@@ -553,7 +728,7 @@ test('[vercel dev] validate trailingSlash', async t => {
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid `trailingSlash` property:\s+should be boolean/m
|
||||
/Invalid vercel\.json - `trailingSlash` should be boolean/m
|
||||
);
|
||||
});
|
||||
|
||||
@@ -564,7 +739,7 @@ test('[vercel dev] validate rewrites', async t => {
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid `rewrites` property: \[0\]\.destination should be string/m
|
||||
/Invalid vercel\.json - `rewrites\[0\].destination` should be string/m
|
||||
);
|
||||
});
|
||||
|
||||
@@ -575,7 +750,7 @@ test('[vercel dev] validate redirects', async t => {
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid `redirects` property: \[0\]\.statusCode should be integer/m
|
||||
/Invalid vercel\.json - `redirects\[0\].statusCode` should be integer/m
|
||||
);
|
||||
});
|
||||
|
||||
@@ -586,7 +761,7 @@ test('[vercel dev] validate headers', async t => {
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(
|
||||
output.stderr,
|
||||
/Invalid `headers` property: \[0\]\.headers\[0\]\.value should be string/m
|
||||
/Invalid vercel\.json - `headers\[0\].headers\[0\].value` should be string/m
|
||||
);
|
||||
});
|
||||
|
||||
@@ -836,11 +1011,26 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] support dynamic next.js routes in monorepos',
|
||||
testFixtureStdio('monorepo-dynamic-paths', async testPath => {
|
||||
await testPath(200, '/', /This is our homepage/m);
|
||||
await testPath(200, '/about', /This is the about static page./m);
|
||||
await testPath(
|
||||
200,
|
||||
'/1/dynamic',
|
||||
/This is the (.*)dynamic(.*) page with static props./m
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] 00-list-directory',
|
||||
testFixtureStdio('00-list-directory', async testPath => {
|
||||
await testPath(200, '/', /Files within/m);
|
||||
await testPath(200, '/', /test[0-3]\.txt/m);
|
||||
await testPath(200, '/', /\.well-known/m);
|
||||
await testPath(200, '/.well-known/keybase.txt', 'proof goes here');
|
||||
})
|
||||
);
|
||||
|
||||
@@ -918,6 +1108,13 @@ test(
|
||||
'[vercel dev] 06-gridsome',
|
||||
testFixtureStdio('06-gridsome', async testPath => {
|
||||
await testPath(200, '/');
|
||||
await testPath(200, '/about');
|
||||
await testPath(308, '/support', 'Redirecting to /about?ref=support (308)', {
|
||||
Location: '/about?ref=support',
|
||||
});
|
||||
// Bug with gridsome's dev server: https://github.com/gridsome/gridsome/issues/831
|
||||
// Works in prod only so leave out for now
|
||||
// await testPath(404, '/nothing');
|
||||
})
|
||||
);
|
||||
|
||||
@@ -925,6 +1122,9 @@ test(
|
||||
'[vercel dev] 07-hexo-node',
|
||||
testFixtureStdio('07-hexo-node', async testPath => {
|
||||
await testPath(200, '/', /Hexo \+ Node.js API/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
await testPath(200, '/contact.html', /Contact Us/m);
|
||||
await testPath(200, '/support', /Contact Us/m);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -949,6 +1149,8 @@ test(
|
||||
testFixtureStdio('10-nextjs-node', async testPath => {
|
||||
await testPath(200, '/', /Next.js \+ Node.js API/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
await testPath(200, '/contact', /Contact Page/);
|
||||
await testPath(200, '/support', /Contact Page/);
|
||||
await testPath(404, '/nothing', /Custom Next 404/);
|
||||
})
|
||||
);
|
||||
@@ -959,6 +1161,7 @@ test(
|
||||
'12-polymer-node',
|
||||
async testPath => {
|
||||
await testPath(200, '/', /Polymer \+ Node.js API/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
@@ -970,6 +1173,7 @@ test(
|
||||
'13-preact-node',
|
||||
async testPath => {
|
||||
await testPath(200, '/', /Preact/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
@@ -981,6 +1185,7 @@ test(
|
||||
'14-svelte-node',
|
||||
async testPath => {
|
||||
await testPath(200, '/', /Svelte/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
@@ -992,6 +1197,7 @@ test(
|
||||
'16-vue-node',
|
||||
async testPath => {
|
||||
await testPath(200, '/', /Vue.js \+ Node.js API/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
@@ -1003,6 +1209,7 @@ test(
|
||||
'17-vuepress-node',
|
||||
async testPath => {
|
||||
await testPath(200, '/', /VuePress \+ Node.js API/m);
|
||||
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
@@ -1256,13 +1463,10 @@ test('[vercel dev] render warning for empty cwd dir', async t => {
|
||||
|
||||
// Monitor `stderr` for the warning
|
||||
dev.stderr.setEncoding('utf8');
|
||||
const msg = 'There are no files inside your deployment.';
|
||||
await new Promise(resolve => {
|
||||
dev.stderr.on('data', str => {
|
||||
if (
|
||||
str.includes(
|
||||
'There are no files (or only files starting with a dot) inside your deployment'
|
||||
)
|
||||
) {
|
||||
if (str.includes(msg)) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
@@ -1401,6 +1605,23 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] Should force `tsc` option "module: commonjs" for `startDevServer()`',
|
||||
testFixtureStdio('force-module-commonjs', async testPath => {
|
||||
await testPath(200, `/`, /Force "module: commonjs" test page/);
|
||||
await testPath(
|
||||
200,
|
||||
`/api`,
|
||||
'Force "module: commonjs" JavaScript with ES Modules API endpoint'
|
||||
);
|
||||
await testPath(
|
||||
200,
|
||||
`/api/ts`,
|
||||
'Force "module: commonjs" TypeScript API endpoint'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] should prioritize index.html over other file named index.*',
|
||||
testFixtureStdio('index-html-priority', async testPath => {
|
||||
@@ -1408,3 +1629,28 @@ 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]}'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -122,6 +122,7 @@ module.exports = async session => {
|
||||
'single-dotfile': {
|
||||
'.testing': 'i am a dotfile',
|
||||
},
|
||||
'empty-directory': {},
|
||||
'config-scope-property-email': {
|
||||
'now.json': `{ "scope": "${session}@zeit.pub", "builds": [ { "src": "*.html", "use": "@now/static" } ], "version": 2 }`,
|
||||
'index.html': '<span>test scope email</span',
|
||||
@@ -138,6 +139,10 @@ module.exports = async session => {
|
||||
'vercel.json': '{"fake": 1}',
|
||||
'index.html': '<h1>Fake</h1>',
|
||||
},
|
||||
'builds-wrong-build-env': {
|
||||
'vercel.json': '{ "build.env": { "key": "value" } }',
|
||||
'index.html': '<h1>Should fail</h1>',
|
||||
},
|
||||
'builds-no-list': {
|
||||
'now.json': `{
|
||||
"version": 2,
|
||||
@@ -467,7 +472,7 @@ CMD ["node", "index.js"]`,
|
||||
'now.json': JSON.stringify({
|
||||
functions: {
|
||||
'api/**/*.php': {
|
||||
runtime: 'now-php@0.0.8',
|
||||
runtime: 'vercel-php@0.1.0',
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -478,7 +483,7 @@ CMD ["node", "index.js"]`,
|
||||
functions: {
|
||||
'api/**/*.php': {
|
||||
memory: 128,
|
||||
runtime: 'now-php@canary',
|
||||
runtime: 'vercel-php@canary',
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -496,12 +501,7 @@ CMD ["node", "index.js"]`,
|
||||
}),
|
||||
},
|
||||
'project-link': {
|
||||
'pages/index.js': 'export default () => <div><h1>Now CLI test</h1></div>',
|
||||
'package.json': JSON.stringify({
|
||||
dependencies: {
|
||||
gatsby: 'latest',
|
||||
},
|
||||
}),
|
||||
'package.json': JSON.stringify({}),
|
||||
},
|
||||
'project-root-directory': {
|
||||
'src/index.html': '<h1>I am a website.</h1>',
|
||||
|
||||
179
packages/now-cli/test/integration.js
vendored
179
packages/now-cli/test/integration.js
vendored
@@ -31,6 +31,8 @@ function execa(file, args, options) {
|
||||
|
||||
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>`;
|
||||
let session = 'temp-session';
|
||||
|
||||
@@ -243,8 +245,10 @@ test.after.always(async () => {
|
||||
loginApiServer.close();
|
||||
}
|
||||
|
||||
// Make sure the token gets revoked
|
||||
await execa(binaryPath, ['logout', ...defaultArgs]);
|
||||
// Make sure the token gets revoked unless it's passed in via environment
|
||||
if (!process.env.VERCEL_TOKEN) {
|
||||
await execa(binaryPath, ['logout', ...defaultArgs]);
|
||||
}
|
||||
|
||||
if (tmpDir) {
|
||||
// Remove config directory entirely
|
||||
@@ -252,6 +256,20 @@ test.after.always(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('default command should prompt login with empty auth.json', async t => {
|
||||
await fs.writeFile(getConfigAuthPath(), JSON.stringify({}));
|
||||
try {
|
||||
await execa(binaryPath, [...defaultArgs]);
|
||||
t.fail();
|
||||
} catch (err) {
|
||||
t.true(
|
||||
err.stderr.includes(
|
||||
'Error! No existing credentials found. Please run `vercel login` or pass "--token"'
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('login', async t => {
|
||||
t.timeout(ms('1m'));
|
||||
|
||||
@@ -511,12 +529,46 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
t.regex(stderr, /Created .env file/gm);
|
||||
|
||||
const contents = fs.readFileSync(path.join(target, '.env'), 'utf8');
|
||||
t.true(contents.startsWith('# Created by Vercel CLI\n'));
|
||||
|
||||
const lines = new Set(contents.split('\n'));
|
||||
t.true(lines.has('MY_ENV_VAR="MY_VALUE"'));
|
||||
t.true(lines.has('MY_STDIN_VAR="{"expect":"quotes"}"'));
|
||||
t.true(lines.has('VERCEL_URL=""'));
|
||||
}
|
||||
|
||||
async function nowEnvPullOverwrite() {
|
||||
const { exitCode, stderr, stdout } = await execa(
|
||||
binaryPath,
|
||||
['env', 'pull', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
cwd: target,
|
||||
}
|
||||
);
|
||||
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
t.regex(stderr, /Overwriting existing .env file/gm);
|
||||
t.regex(stderr, /Updated .env file/gm);
|
||||
}
|
||||
|
||||
async function nowEnvPullConfirm() {
|
||||
fs.writeFileSync(path.join(target, '.env'), 'hahaha');
|
||||
|
||||
const vc = execa(binaryPath, ['env', 'pull', ...defaultArgs], {
|
||||
reject: false,
|
||||
cwd: target,
|
||||
});
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes('Found existing file ".env". Do you want to overwrite?')
|
||||
);
|
||||
vc.stdin.end('y\n');
|
||||
|
||||
const { exitCode, stderr, stdout } = await vc;
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
}
|
||||
|
||||
async function nowDeployWithVar() {
|
||||
const { exitCode, stderr, stdout } = await execa(
|
||||
binaryPath,
|
||||
@@ -609,6 +661,8 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
await nowEnvAddSystemEnv();
|
||||
await nowEnvLsIncludesVar();
|
||||
await nowEnvPull();
|
||||
await nowEnvPullOverwrite();
|
||||
await nowEnvPullConfirm();
|
||||
await nowDeployWithVar();
|
||||
await nowEnvRemove();
|
||||
await nowEnvRemoveWithArgs();
|
||||
@@ -1304,7 +1358,7 @@ test('set platform version using `--platform-version` to `2`', async t => {
|
||||
});
|
||||
|
||||
test('ensure we render a warning for deployments with no files', async t => {
|
||||
const directory = fixture('single-dotfile');
|
||||
const directory = fixture('empty-directory');
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
@@ -1327,11 +1381,7 @@ test('ensure we render a warning for deployments with no files', async t => {
|
||||
console.log(exitCode);
|
||||
|
||||
// Ensure the warning is printed
|
||||
t.true(
|
||||
stderr.includes(
|
||||
'There are no files (or only files starting with a dot) inside your deployment.'
|
||||
)
|
||||
);
|
||||
t.regex(stderr, /There are no files inside your deployment/);
|
||||
|
||||
// Test if the output is really a URL
|
||||
const { href, host } = new URL(stdout);
|
||||
@@ -1341,10 +1391,8 @@ test('ensure we render a warning for deployments with no files', async t => {
|
||||
t.is(exitCode, 0);
|
||||
|
||||
// Send a test request to the deployment
|
||||
const response = await fetch(href);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
t.is(contentType, 'text/plain; charset=utf-8');
|
||||
const res = await fetch(href);
|
||||
t.is(res.status, 404);
|
||||
});
|
||||
|
||||
test('ensure we render a prompt when deploying home directory', async t => {
|
||||
@@ -1473,9 +1521,10 @@ test('try to create a builds deployments with wrong now.json', async t => {
|
||||
t.is(exitCode, 1);
|
||||
t.true(
|
||||
stderr.includes(
|
||||
'Error! The property `builder` is not allowed in now.json – please remove it.'
|
||||
'Error! Invalid now.json - should NOT have additional property `builder`. Did you mean `builds`?'
|
||||
)
|
||||
);
|
||||
t.true(stderr.includes('https://vercel.com/docs/configuration'));
|
||||
});
|
||||
|
||||
test('try to create a builds deployments with wrong vercel.json', async t => {
|
||||
@@ -1496,9 +1545,35 @@ test('try to create a builds deployments with wrong vercel.json', async t => {
|
||||
t.is(exitCode, 1);
|
||||
t.true(
|
||||
stderr.includes(
|
||||
'Error! The property `fake` is not allowed in vercel.json – please remove it.'
|
||||
'Error! Invalid vercel.json - should NOT have additional property `fake`. Please remove it.'
|
||||
)
|
||||
);
|
||||
t.true(stderr.includes('https://vercel.com/docs/configuration'));
|
||||
});
|
||||
|
||||
test('try to create a builds deployments with wrong `build.env` property', async t => {
|
||||
const directory = fixture('builds-wrong-build-env');
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
['--public', ...defaultArgs, '--confirm'],
|
||||
{
|
||||
cwd: directory,
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
t.is(exitCode, 1, formatOutput({ stdout, stderr }));
|
||||
t.true(
|
||||
stderr.includes(
|
||||
'Error! Invalid vercel.json - should NOT have additional property `build.env`. Did you mean `{ "build": { "env": {"name": "value"} } }`?'
|
||||
),
|
||||
formatOutput({ stdout, stderr })
|
||||
);
|
||||
t.true(
|
||||
stderr.includes('https://vercel.com/docs/configuration'),
|
||||
formatOutput({ stdout, stderr })
|
||||
);
|
||||
});
|
||||
|
||||
test('create a builds deployments with no actual builds', async t => {
|
||||
@@ -1589,7 +1664,7 @@ test('create a staging deployment', async t => {
|
||||
/Setting target to staging/gm,
|
||||
formatOutput(targetCall)
|
||||
);
|
||||
|
||||
t.regex(targetCall.stdout, /https:\/\//gm);
|
||||
t.is(targetCall.exitCode, 0, formatOutput(targetCall));
|
||||
|
||||
const { host } = new URL(targetCall.stdout);
|
||||
@@ -1625,6 +1700,7 @@ test('create a production deployment', async t => {
|
||||
/Setting target to production/gm,
|
||||
formatOutput(targetCall)
|
||||
);
|
||||
t.regex(targetCall.stdout, /https:\/\//gm);
|
||||
|
||||
const { host: targetHost } = new URL(targetCall.stdout);
|
||||
const targetDeployment = await apiFetch(
|
||||
@@ -1648,6 +1724,7 @@ test('create a production deployment', async t => {
|
||||
/Setting target to production/gm,
|
||||
formatOutput(targetCall)
|
||||
);
|
||||
t.regex(call.stdout, /https:\/\//gm);
|
||||
|
||||
const { host } = new URL(call.stdout);
|
||||
const deployment = await apiFetch(
|
||||
@@ -2341,7 +2418,7 @@ test('deploy a Lambda with a specific runtime', async t => {
|
||||
const { host: url } = new URL(output.stdout);
|
||||
|
||||
const [build] = await getDeploymentBuildsByUrl(url);
|
||||
t.is(build.use, 'now-php@0.0.8', JSON.stringify(build, null, 2));
|
||||
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 => {
|
||||
@@ -2451,7 +2528,7 @@ test('should show prompts to set up project', async t => {
|
||||
await waitForPrompt(now, chunk =>
|
||||
chunk.includes(`What's your Development Command?`)
|
||||
);
|
||||
now.stdin.write(`yarn dev\n`);
|
||||
now.stdin.write(`\n`);
|
||||
|
||||
await waitForPrompt(now, chunk => chunk.includes('Linked to'));
|
||||
|
||||
@@ -2482,6 +2559,41 @@ test('should show prompts to set up project', async t => {
|
||||
const response = await fetch(new URL(output.stdout).href);
|
||||
const text = await response.text();
|
||||
t.is(text.includes('<h1>custom hello</h1>'), true, text);
|
||||
|
||||
// Ensure that `vc dev` also uses the configured build command
|
||||
// and output directory
|
||||
let stderr = '';
|
||||
const port = 58351;
|
||||
const dev = execa(binaryPath, [
|
||||
'dev',
|
||||
'--listen',
|
||||
port,
|
||||
directory,
|
||||
...defaultArgs,
|
||||
]);
|
||||
dev.stderr.setEncoding('utf8');
|
||||
|
||||
try {
|
||||
dev.stdout.pipe(process.stdout);
|
||||
dev.stderr.pipe(process.stderr);
|
||||
await new Promise((resolve, reject) => {
|
||||
dev.once('exit', (code, signal) => {
|
||||
reject(`"vc dev" failed with ${signal || code}`);
|
||||
});
|
||||
dev.stderr.on('data', data => {
|
||||
stderr += data;
|
||||
if (stderr.includes('Ready! Available at')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const res2 = await fetch(`http://localhost:${port}/`);
|
||||
const text2 = await res2.text();
|
||||
t.is(text2.includes('<h1>custom hello</h1>'), true, text2);
|
||||
} finally {
|
||||
process.kill(dev.pid, 'SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
test('should prefill "project name" prompt with folder name', async t => {
|
||||
@@ -2932,3 +3044,36 @@ test('`vc --debug project ls` should output the projects listing', async t => {
|
||||
formatOutput({ stderr, stdout })
|
||||
);
|
||||
});
|
||||
|
||||
test('deploy gatsby twice and print cached directories', async t => {
|
||||
const directory = example('gatsby');
|
||||
const packageJsonPath = path.join(directory, 'package.json');
|
||||
const packageJsonOriginal = await readFile(packageJsonPath, 'utf8');
|
||||
const pkg = JSON.parse(packageJsonOriginal);
|
||||
|
||||
async function tryDeploy(cwd) {
|
||||
await execa(binaryPath, [...defaultArgs, '--public', '--confirm'], {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
reject: true,
|
||||
});
|
||||
|
||||
t.true(true);
|
||||
}
|
||||
|
||||
// Deploy once to populate the cache
|
||||
await tryDeploy(directory);
|
||||
|
||||
// Wait because the cache is not available right away
|
||||
// See https://codeburst.io/quick-explanation-of-the-s3-consistency-model-6c9f325e3f82
|
||||
await sleep(60000);
|
||||
|
||||
// Update build script to ensure cached files were restored in the next deploy
|
||||
pkg.scripts.build = `ls -lA && ls .cache && ls public && ${pkg.scripts.build}`;
|
||||
await writeFile(packageJsonPath, JSON.stringify(pkg));
|
||||
try {
|
||||
await tryDeploy(directory);
|
||||
} finally {
|
||||
await writeFile(packageJsonPath, packageJsonOriginal);
|
||||
}
|
||||
});
|
||||
|
||||
4
packages/now-client/.gitignore
vendored
4
packages/now-client/.gitignore
vendored
@@ -3,4 +3,6 @@ lib
|
||||
node_modules
|
||||
*.log
|
||||
|
||||
!tests/fixtures/nowignore/node_modules
|
||||
!tests/fixtures/nowignore/node_modules
|
||||
!tests/fixtures/vercelignore-allow-nodemodules/node_modules
|
||||
!tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "8.0.2-canary.2",
|
||||
"version": "8.1.1-canary.1",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
|
||||
@@ -93,6 +93,16 @@ export async function* checkDeploymentStatus(
|
||||
}
|
||||
|
||||
if (isAliasAssigned(deploymentUpdate)) {
|
||||
if (
|
||||
deploymentUpdate.aliasWarning &&
|
||||
deploymentUpdate.aliasWarning.message
|
||||
) {
|
||||
yield {
|
||||
type: 'warning',
|
||||
payload: deploymentUpdate.aliasWarning.message,
|
||||
};
|
||||
}
|
||||
|
||||
debug('Deployment alias assigned');
|
||||
return yield { type: 'alias-assigned', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readdir as readRootFolder, lstatSync } from 'fs-extra';
|
||||
|
||||
import { relative, isAbsolute, basename } from 'path';
|
||||
import { relative, isAbsolute } from 'path';
|
||||
import hashes, { mapToObject } from './utils/hashes';
|
||||
import { upload } from './upload';
|
||||
import { buildFileTree, createDebug, parseVercelConfig } from './utils';
|
||||
@@ -139,17 +139,11 @@ export default function buildCreateDeployment(version: number) {
|
||||
|
||||
// This is a useful warning because it prevents people
|
||||
// from getting confused about a deployment that renders 404.
|
||||
if (
|
||||
fileList.length === 0 ||
|
||||
fileList.every(f => (f ? basename(f).startsWith('.') : true))
|
||||
) {
|
||||
debug(
|
||||
`Deployment path has no files (or only dotfiles). Yielding a warning event`
|
||||
);
|
||||
if (fileList.length === 0) {
|
||||
debug('Deployment path has no files. Yielding a warning event');
|
||||
yield {
|
||||
type: 'warning',
|
||||
payload:
|
||||
'There are no files (or only files starting with a dot) inside your deployment.',
|
||||
payload: 'There are no files inside your deployment.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ export interface DeploymentOptions extends LegacyDeploymentOptions {
|
||||
build?: {
|
||||
env: Dictionary<string>;
|
||||
};
|
||||
source?: string;
|
||||
target?: string;
|
||||
name?: string;
|
||||
public?: boolean;
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs from 'querystring';
|
||||
import ignore from 'ignore';
|
||||
type Ignore = ReturnType<typeof ignore>;
|
||||
import { pkgVersion } from '../pkg';
|
||||
import { NowBuildError } from '@vercel/build-utils';
|
||||
import { NowClientOptions, DeploymentOptions, NowConfig } from '../types';
|
||||
import { Sema } from 'async-sema';
|
||||
import { readFile } from 'fs-extra';
|
||||
@@ -48,7 +49,7 @@ export function getApiDeploymentsUrl(
|
||||
return '/v10/now/deployments';
|
||||
}
|
||||
|
||||
return '/v12/now/deployments';
|
||||
return '/v13/now/deployments';
|
||||
}
|
||||
|
||||
export async function parseVercelConfig(filePath?: string): Promise<NowConfig> {
|
||||
@@ -76,43 +77,22 @@ const maybeRead = async function<T>(path: string, default_: T) {
|
||||
}
|
||||
};
|
||||
|
||||
export async function readdirRelative(
|
||||
path: string,
|
||||
ignores: string[],
|
||||
cwd: string
|
||||
): Promise<string[]> {
|
||||
const preprocessedIgnores = ignores.map(ignore => {
|
||||
if (ignore.endsWith('/')) {
|
||||
return ignore.slice(0, -1);
|
||||
}
|
||||
return ignore;
|
||||
});
|
||||
const dirContents = await readdir(path, preprocessedIgnores);
|
||||
return dirContents.map(filePath => relative(cwd, filePath));
|
||||
}
|
||||
|
||||
export async function buildFileTree(
|
||||
path: string | string[],
|
||||
isDirectory: boolean,
|
||||
debug: Debug
|
||||
): Promise<string[]> {
|
||||
// Get .nowignore
|
||||
let { ig, ignores } = await getVercelIgnore(path);
|
||||
|
||||
debug(`Found ${ig.ignores.length} rules in .nowignore`);
|
||||
|
||||
let fileList: string[];
|
||||
let { ig } = await getVercelIgnore(path);
|
||||
|
||||
debug(`Found ${ig.ignores.length} rules in .vercelignore`);
|
||||
debug('Building file tree...');
|
||||
|
||||
if (isDirectory && !Array.isArray(path)) {
|
||||
// Directory path
|
||||
const cwd = process.cwd();
|
||||
const relativeFileList = await readdirRelative(path, ignores, cwd);
|
||||
fileList = ig
|
||||
.filter(relativeFileList)
|
||||
.map((relativePath: string) => join(cwd, relativePath));
|
||||
|
||||
const ignores = (absPath: string) => ig.ignores(relative(cwd, absPath));
|
||||
fileList = await readdir(path, [ignores]);
|
||||
debug(`Read ${fileList.length} files in the specified directory`);
|
||||
} else if (Array.isArray(path)) {
|
||||
// Array of file paths
|
||||
@@ -166,9 +146,12 @@ export async function getVercelIgnore(
|
||||
maybeRead(join(cwd, '.nowignore'), ''),
|
||||
]);
|
||||
if (vercelignore && nowignore) {
|
||||
throw new Error(
|
||||
'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.'
|
||||
);
|
||||
throw new NowBuildError({
|
||||
code: 'CONFLICTING_IGNORE_FILES',
|
||||
message:
|
||||
'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.',
|
||||
link: 'https://vercel.link/combining-old-and-new-config',
|
||||
});
|
||||
}
|
||||
return vercelignore || nowignore;
|
||||
})
|
||||
|
||||
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/.env.local
vendored
Normal file
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/.env.local
vendored
Normal file
@@ -0,0 +1 @@
|
||||
EXCLUDE="this file should be ignored"
|
||||
6
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/.vercelignore
vendored
Normal file
6
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/.vercelignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# literally dont ignore the node_modules directory
|
||||
# so basically include the node_modules directory recursively
|
||||
!node_modules/
|
||||
|
||||
# ignore this file in addition to the defaults
|
||||
exclude.txt
|
||||
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/exclude.txt
vendored
Normal file
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/exclude.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Should be ignored
|
||||
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/hello.txt
vendored
Normal file
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/hello.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hello World
|
||||
0
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/node_modules/one.txt
generated
vendored
Normal file
0
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/node_modules/one.txt
generated
vendored
Normal file
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/sub/include.txt
vendored
Normal file
1
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/sub/include.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Should be included
|
||||
0
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules/two.txt
generated
vendored
Normal file
0
packages/now-client/tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules/two.txt
generated
vendored
Normal file
@@ -1,7 +1,8 @@
|
||||
import path from 'path';
|
||||
import { buildFileTree, getVercelIgnore, readdirRelative } from '../src/utils';
|
||||
import { join, resolve } from 'path';
|
||||
import { buildFileTree } from '../src/utils';
|
||||
|
||||
const ignoreFixturePath = path.resolve(__dirname, 'fixtures', 'nowignore');
|
||||
const fixture = (name: string) => resolve(__dirname, 'fixtures', name);
|
||||
const noop = () => {};
|
||||
|
||||
const normalizeWindowsPaths = (files: string[]) => {
|
||||
if (process.platform === 'win32') {
|
||||
@@ -10,34 +11,29 @@ const normalizeWindowsPaths = (files: string[]) => {
|
||||
return files;
|
||||
};
|
||||
|
||||
describe('buildFileTree', () => {
|
||||
it('will include the correct files', async () => {
|
||||
const expected = [
|
||||
'tests/fixtures/nowignore/.nowignore',
|
||||
'tests/fixtures/nowignore/index.txt',
|
||||
].map(p => path.join(process.cwd(), p));
|
||||
const actual = await buildFileTree(ignoreFixturePath, true, () => {});
|
||||
expect(normalizeWindowsPaths(expected).sort()).toEqual(
|
||||
normalizeWindowsPaths(actual).sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
const toAbsolutePaths = (cwd: string, files: string[]) =>
|
||||
files.map(p => join(cwd, p));
|
||||
|
||||
describe('readdirRelative', () => {
|
||||
it('will ignore the hardcoded default ignores', async () => {
|
||||
// most importantly, this method should not walk/include node_modules
|
||||
const expected = [
|
||||
'tests/fixtures/nowignore/.nowignore',
|
||||
'tests/fixtures/nowignore/ignore.txt',
|
||||
'tests/fixtures/nowignore/index.txt',
|
||||
'tests/fixtures/nowignore/folder/ignore.txt',
|
||||
];
|
||||
const { ignores } = await getVercelIgnore(ignoreFixturePath);
|
||||
const actual = await readdirRelative(
|
||||
ignoreFixturePath,
|
||||
ignores,
|
||||
process.cwd()
|
||||
describe('buildFileTree', () => {
|
||||
it('should exclude files using `.nowignore` blocklist', async () => {
|
||||
const cwd = fixture('nowignore');
|
||||
const expected = toAbsolutePaths(cwd, ['.nowignore', 'index.txt']);
|
||||
const actual = await buildFileTree(cwd, true, noop);
|
||||
expect(normalizeWindowsPaths(expected).sort()).toEqual(
|
||||
normalizeWindowsPaths(actual).sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('should include the node_modules using `.vercelignore` allowlist', async () => {
|
||||
const cwd = fixture('vercelignore-allow-nodemodules');
|
||||
const expected = toAbsolutePaths(cwd, [
|
||||
'node_modules/one.txt',
|
||||
'sub/node_modules/two.txt',
|
||||
'sub/include.txt',
|
||||
'.vercelignore',
|
||||
'hello.txt',
|
||||
]);
|
||||
const actual = await buildFileTree(cwd, true, noop);
|
||||
expect(normalizeWindowsPaths(expected).sort()).toEqual(
|
||||
normalizeWindowsPaths(actual).sort()
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
29
packages/now-go/dev-server.go
Normal file
29
packages/now-go/dev-server.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// create a new handler
|
||||
handler := http.HandlerFunc(__HANDLER_FUNC_NAME)
|
||||
|
||||
// https://stackoverflow.com/a/43425461/376773
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
file := os.NewFile(3, "pipe")
|
||||
_, err2 := file.Write([]byte(strconv.Itoa(port)))
|
||||
if err2 != nil {
|
||||
panic(err2)
|
||||
}
|
||||
|
||||
panic(http.Serve(listener, handler))
|
||||
}
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
@@ -127,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);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
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 } from '@vercel/build-utils';
|
||||
import { homedir } 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 {
|
||||
@@ -17,6 +33,8 @@ const {
|
||||
import { createGo, getAnalyzedEntrypoint, OUT_EXTENSION } from './go-helpers';
|
||||
const handlerFileName = `handler${OUT_EXTENSION}`;
|
||||
|
||||
export { shouldServe };
|
||||
|
||||
interface Analyzed {
|
||||
found?: boolean;
|
||||
packageName: string;
|
||||
@@ -24,16 +42,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,4 +460,121 @@ 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 env: typeof process.env = {
|
||||
...process.env,
|
||||
...meta.env,
|
||||
};
|
||||
|
||||
const tmpRelative = `.${sep}${entrypointDir}`;
|
||||
const child = spawn('go', ['run', tmpRelative], {
|
||||
cwd: tmp,
|
||||
env,
|
||||
stdio: ['ignore', 'inherit', 'inherit', 'pipe'],
|
||||
});
|
||||
|
||||
child.once('exit', async () => {
|
||||
await remove(tmp); // Cleanup
|
||||
});
|
||||
|
||||
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 onExit = once.spread<[number, string | null]>(child, 'exit');
|
||||
const result = await Promise.race([onPort, onExit]);
|
||||
onExit.cancel();
|
||||
|
||||
if (isPortInfo(result)) {
|
||||
return {
|
||||
port: result.port,
|
||||
pid: child.pid,
|
||||
};
|
||||
} else {
|
||||
// Got "exit" event from child process
|
||||
throw new Error(
|
||||
`Failed to start dev server for "${entrypointWithExt}" (code=${result[0]}, signal=${result[1]})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
vc "github.com/vercel/go-bridge/go/bridge"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/go",
|
||||
"version": "1.1.2-canary.2",
|
||||
"version": "1.1.3-canary.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
|
||||
@@ -19,6 +19,7 @@
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@tootallnate/once": "1.1.2",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/fs-extra": "^5.0.5",
|
||||
"@types/node-fetch": "^2.3.0",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user