mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-28 11:49:14 +00:00
Compare commits
55 Commits
@vercel/cl
...
@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 |
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
"version": "0.0.16-canary.0",
|
||||
"main": "frameworks.json",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.4.0",
|
||||
"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.';
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "19.1.0",
|
||||
"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.4.0",
|
||||
"@vercel/go": "1.1.2",
|
||||
"@vercel/next": "2.6.6",
|
||||
"@vercel/node": "1.7.0",
|
||||
"@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.2",
|
||||
"@vercel/static-build": "0.17.2"
|
||||
"@vercel/ruby": "1.2.3-canary.0",
|
||||
"@vercel/static-build": "0.17.4-canary.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/node": "5.5.0",
|
||||
|
||||
@@ -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';
|
||||
@@ -738,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');
|
||||
|
||||
|
||||
@@ -314,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);
|
||||
@@ -638,12 +623,18 @@ 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.`
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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>[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -50,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';
|
||||
@@ -93,6 +83,7 @@ import {
|
||||
HttpHeadersConfig,
|
||||
EnvConfigs,
|
||||
} from './types';
|
||||
import { ProjectSettings } from '../../types';
|
||||
|
||||
interface FSEvent {
|
||||
type: string;
|
||||
@@ -117,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;
|
||||
@@ -140,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;
|
||||
@@ -154,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;
|
||||
@@ -388,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
|
||||
@@ -446,6 +437,9 @@ export default class DevServer {
|
||||
`Cleaning up "blockingBuildsPromise" after error: ${err}`
|
||||
);
|
||||
this.blockingBuildsPromise = null;
|
||||
if (err) {
|
||||
this.output.prettyError(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -542,7 +536,7 @@ export default class DevServer {
|
||||
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());
|
||||
@@ -589,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,
|
||||
@@ -693,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 {
|
||||
@@ -1503,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 (
|
||||
@@ -2203,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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -145,6 +145,7 @@ export default class Now extends EventEmitter {
|
||||
regions,
|
||||
target: target || undefined,
|
||||
projectSettings,
|
||||
source: 'cli',
|
||||
};
|
||||
|
||||
// Ignore specific items from vercel.json
|
||||
|
||||
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/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,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
@@ -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';
|
||||
@@ -526,6 +527,35 @@ test('[vercel dev] reflects changes to config and env without restart', async t
|
||||
}
|
||||
});
|
||||
|
||||
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(
|
||||
'[vercel dev] validate routes that use `check: true`',
|
||||
testFixtureStdio('routes-check-true', async testPath => {
|
||||
@@ -636,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/);
|
||||
@@ -664,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -675,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -684,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 => {
|
||||
@@ -694,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -705,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -716,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -727,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -977,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');
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1059,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');
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1066,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);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1090,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/);
|
||||
})
|
||||
);
|
||||
@@ -1100,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 }
|
||||
)
|
||||
@@ -1111,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 }
|
||||
)
|
||||
@@ -1122,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 }
|
||||
)
|
||||
@@ -1133,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 }
|
||||
)
|
||||
@@ -1144,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 }
|
||||
)
|
||||
@@ -1397,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();
|
||||
}
|
||||
});
|
||||
@@ -1566,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>',
|
||||
|
||||
175
packages/now-cli/test/integration.js
vendored
175
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 => {
|
||||
@@ -2343,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 => {
|
||||
@@ -2453,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'));
|
||||
|
||||
@@ -2484,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 => {
|
||||
@@ -2934,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "8.1.0",
|
||||
"version": "8.1.1-canary.1",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/next",
|
||||
"version": "2.6.6",
|
||||
"version": "2.6.8-canary.4",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/resolve-from": "5.0.1",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "2.4.1",
|
||||
"@zeit/node-file-trace": "0.6.1",
|
||||
"@zeit/node-file-trace": "0.6.5",
|
||||
"async-sema": "3.0.1",
|
||||
"buffer-crc32": "0.2.13",
|
||||
"escape-string-regexp": "3.0.0",
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import {
|
||||
lstatSync,
|
||||
lstat,
|
||||
pathExists,
|
||||
readFile,
|
||||
unlink as unlinkFile,
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
validateEntrypoint,
|
||||
} from './utils';
|
||||
// import findUp from 'find-up';
|
||||
import { Sema } from 'async-sema';
|
||||
|
||||
interface BuildParamsMeta {
|
||||
isDev: boolean | undefined;
|
||||
@@ -208,7 +209,7 @@ export const build = async ({
|
||||
// Limit for max size each lambda can be, 50 MB if no custom limit
|
||||
const lambdaCompressedByteLimit = config.maxLambdaSize || 50 * 1000 * 1000;
|
||||
|
||||
const entryDirectory = path.dirname(entrypoint);
|
||||
let entryDirectory = path.dirname(entrypoint);
|
||||
const entryPath = path.join(workPath, entryDirectory);
|
||||
const outputDirectory = config.outputDirectory || '.next';
|
||||
const dotNextStatic = path.join(entryPath, outputDirectory, 'static');
|
||||
@@ -422,17 +423,16 @@ export const build = async ({
|
||||
const headers: Route[] = [];
|
||||
const rewrites: Route[] = [];
|
||||
const redirects: Route[] = [];
|
||||
const nextBasePathRoute: Route[] = [];
|
||||
const dataRoutes: Route[] = [];
|
||||
let dynamicRoutes: Route[] = [];
|
||||
let nextBasePath: string | undefined;
|
||||
// whether they have enabled pages/404.js as the custom 404 page
|
||||
let hasPages404 = false;
|
||||
|
||||
if (routesManifest) {
|
||||
switch (routesManifest.version) {
|
||||
case 1:
|
||||
case 2: {
|
||||
case 2:
|
||||
case 3: {
|
||||
redirects.push(...convertRedirects(routesManifest.redirects));
|
||||
rewrites.push(...convertRewrites(routesManifest.rewrites));
|
||||
|
||||
@@ -468,7 +468,14 @@ export const build = async ({
|
||||
// make sure to route SSG data route to the data prerender
|
||||
// output, we don't do this for SSP routes since they don't
|
||||
// have a separate data output
|
||||
(ssgDataRoute && ssgDataRoute.dataRoute) || dataRoute.page
|
||||
(ssgDataRoute && ssgDataRoute.dataRoute) || dataRoute.page,
|
||||
`${
|
||||
dataRoute.routeKeys
|
||||
? `?${Object.keys(dataRoute.routeKeys)
|
||||
.map(key => `${dataRoute.routeKeys![key]}=$${key}`)
|
||||
.join('&')}`
|
||||
: ''
|
||||
}`
|
||||
),
|
||||
check: true,
|
||||
});
|
||||
@@ -480,7 +487,7 @@ export const build = async ({
|
||||
}
|
||||
|
||||
if (routesManifest.basePath && routesManifest.basePath !== '/') {
|
||||
nextBasePath = routesManifest.basePath;
|
||||
const nextBasePath = routesManifest.basePath;
|
||||
|
||||
if (!nextBasePath.startsWith('/')) {
|
||||
throw new NowBuildError({
|
||||
@@ -497,11 +504,7 @@ export const build = async ({
|
||||
});
|
||||
}
|
||||
|
||||
nextBasePathRoute.push({
|
||||
src: `^${nextBasePath}(?:$|/(.*))$`,
|
||||
dest: `/$1`,
|
||||
continue: true,
|
||||
});
|
||||
entryDirectory = path.join(entryDirectory, nextBasePath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -565,9 +568,6 @@ export const build = async ({
|
||||
routes: [
|
||||
// TODO: low priority: handle trailingSlash
|
||||
|
||||
// Add top level rewrite for basePath if provided
|
||||
...nextBasePathRoute,
|
||||
|
||||
// User headers
|
||||
...headers,
|
||||
|
||||
@@ -597,7 +597,7 @@ export const build = async ({
|
||||
src: path.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
|
||||
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
|
||||
),
|
||||
status: 404,
|
||||
check: true,
|
||||
@@ -617,7 +617,7 @@ export const build = async ({
|
||||
src: path.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
|
||||
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
|
||||
),
|
||||
// Next.js assets contain a hash or entropy in their filenames, so they
|
||||
// are guaranteed to be unique and cacheable indefinitely.
|
||||
@@ -914,16 +914,29 @@ export const build = async ({
|
||||
|
||||
debug(`node-file-trace result for pages: ${fileList}`);
|
||||
|
||||
const lstatSema = new Sema(25, {
|
||||
capacity: fileList.length + apiFileList.length,
|
||||
});
|
||||
const lstatResults: { [key: string]: ReturnType<typeof lstat> } = {};
|
||||
|
||||
const collectTracedFiles = (
|
||||
reasons: NodeFileTraceReasons,
|
||||
files: { [filePath: string]: FileFsRef }
|
||||
) => (file: string) => {
|
||||
) => async (file: string) => {
|
||||
const reason = reasons[file];
|
||||
if (reason && reason.type === 'initial') {
|
||||
// Initial files are manually added to the lambda later
|
||||
return;
|
||||
}
|
||||
const { mode } = lstatSync(path.join(workPath, file));
|
||||
const filePath = path.join(workPath, file);
|
||||
|
||||
if (!lstatResults[filePath]) {
|
||||
lstatResults[filePath] = lstatSema
|
||||
.acquire()
|
||||
.then(() => lstat(filePath))
|
||||
.finally(() => lstatSema.release());
|
||||
}
|
||||
const { mode } = await lstatResults[filePath];
|
||||
|
||||
files[file] = new FileFsRef({
|
||||
fsPath: path.join(workPath, file),
|
||||
@@ -931,8 +944,12 @@ export const build = async ({
|
||||
});
|
||||
};
|
||||
|
||||
fileList.forEach(collectTracedFiles(nonApiReasons, tracedFiles));
|
||||
apiFileList.forEach(collectTracedFiles(apiReasons, apiTracedFiles));
|
||||
await Promise.all(
|
||||
fileList.map(collectTracedFiles(nonApiReasons, tracedFiles))
|
||||
);
|
||||
await Promise.all(
|
||||
apiFileList.map(collectTracedFiles(apiReasons, apiTracedFiles))
|
||||
);
|
||||
console.timeEnd(tracingLabel);
|
||||
|
||||
const zippingLabel = 'Compressed shared serverless function files';
|
||||
@@ -1209,77 +1226,86 @@ export const build = async ({
|
||||
`
|
||||
const url = require('url');
|
||||
page = function(req, res) {
|
||||
const pages = {
|
||||
${groupPageKeys
|
||||
.map(
|
||||
page =>
|
||||
`'${page}': require('./${path.join(
|
||||
'./',
|
||||
group.pages[page].pageFileName
|
||||
)}')`
|
||||
)
|
||||
.join(',\n')}
|
||||
${
|
||||
'' /*
|
||||
creates a mapping of the page and the page's module e.g.
|
||||
'/about': require('./.next/serverless/pages/about.js')
|
||||
*/
|
||||
try {
|
||||
const pages = {
|
||||
${groupPageKeys
|
||||
.map(
|
||||
page =>
|
||||
`'${page}': require('./${path.join(
|
||||
'./',
|
||||
group.pages[page].pageFileName
|
||||
)}')`
|
||||
)
|
||||
.join(',\n')}
|
||||
${
|
||||
'' /*
|
||||
creates a mapping of the page and the page's module e.g.
|
||||
'/about': require('./.next/serverless/pages/about.js')
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
let toRender = req.headers['x-nextjs-page']
|
||||
let toRender = req.headers['x-nextjs-page']
|
||||
|
||||
if (!toRender) {
|
||||
try {
|
||||
const { pathname } = url.parse(req.url)
|
||||
toRender = pathname
|
||||
} catch (_) {
|
||||
// handle failing to parse url
|
||||
res.statusCode = 400
|
||||
return res.end('Bad Request')
|
||||
if (!toRender) {
|
||||
try {
|
||||
const { pathname } = url.parse(req.url)
|
||||
toRender = pathname
|
||||
} catch (_) {
|
||||
// handle failing to parse url
|
||||
res.statusCode = 400
|
||||
return res.end('Bad Request')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentPage = pages[toRender]
|
||||
let currentPage = pages[toRender]
|
||||
|
||||
if (
|
||||
toRender &&
|
||||
!currentPage &&
|
||||
toRender.includes('/_next/data')
|
||||
) {
|
||||
toRender = toRender
|
||||
.replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
|
||||
.replace(/\\.json$/, '')
|
||||
if (
|
||||
toRender &&
|
||||
!currentPage
|
||||
) {
|
||||
if (toRender.includes('/_next/data')) {
|
||||
toRender = toRender
|
||||
.replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
|
||||
.replace(/\\.json$/, '')
|
||||
|
||||
currentPage = pages[toRender]
|
||||
currentPage = pages[toRender]
|
||||
}
|
||||
|
||||
if (!currentPage) {
|
||||
// for prerendered dynamic routes (/blog/post-1) we need to
|
||||
// find the match since it won't match the page directly
|
||||
const dynamicRoutes = ${JSON.stringify(
|
||||
dynamicRoutes.map(route => ({
|
||||
src: route.src,
|
||||
dest: route.dest,
|
||||
}))
|
||||
)}
|
||||
if (!currentPage) {
|
||||
// for prerendered dynamic routes (/blog/post-1) we need to
|
||||
// find the match since it won't match the page directly
|
||||
const dynamicRoutes = ${JSON.stringify(
|
||||
dynamicRoutes.map(route => ({
|
||||
src: route.src,
|
||||
dest: route.dest,
|
||||
}))
|
||||
)}
|
||||
|
||||
for (const route of dynamicRoutes) {
|
||||
const matcher = new RegExp(route.src)
|
||||
for (const route of dynamicRoutes) {
|
||||
const matcher = new RegExp(route.src)
|
||||
|
||||
if (matcher.test(toRender)) {
|
||||
toRender = route.dest
|
||||
currentPage = pages[toRender]
|
||||
break
|
||||
if (matcher.test(toRender)) {
|
||||
toRender = url.parse(route.dest).pathname
|
||||
currentPage = pages[toRender]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentPage) {
|
||||
res.statusCode = 500
|
||||
return res.end('internal server error')
|
||||
if (!currentPage) {
|
||||
console.error(
|
||||
"Failed to find matching page for", toRender, "in lambda"
|
||||
)
|
||||
res.statusCode = 500
|
||||
return res.end('internal server error')
|
||||
}
|
||||
const method = currentPage.render || currentPage.default || currentPage
|
||||
return method(req, res)
|
||||
} catch (err) {
|
||||
console.error('Unhandled error during request:', err)
|
||||
throw err
|
||||
}
|
||||
const method = currentPage.render || currentPage.default || currentPage
|
||||
return method(req, res)
|
||||
}
|
||||
`
|
||||
);
|
||||
@@ -1583,9 +1609,6 @@ export const build = async ({
|
||||
- Builder rewrites
|
||||
*/
|
||||
routes: [
|
||||
// Add top level rewrite for basePath if provided
|
||||
...nextBasePathRoute,
|
||||
|
||||
// headers
|
||||
...headers,
|
||||
|
||||
@@ -1620,7 +1643,7 @@ export const build = async ({
|
||||
src: path.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
|
||||
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
|
||||
),
|
||||
status: 404,
|
||||
check: true,
|
||||
@@ -1650,7 +1673,7 @@ export const build = async ({
|
||||
src: path.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
|
||||
'_next/static/(?:[^/]+/pages|pages|chunks|runtime|css|media)/.+'
|
||||
),
|
||||
// Next.js assets contain a hash or entropy in their filenames, so they
|
||||
// are guaranteed to be unique and cacheable indefinitely.
|
||||
|
||||
@@ -255,17 +255,21 @@ async function getRoutes(
|
||||
}
|
||||
|
||||
routes.push(
|
||||
...(await getDynamicRoutes(entryPath, entryDirectory, dynamicPages).then(
|
||||
arr =>
|
||||
arr.map((route: Source) => {
|
||||
// convert to make entire RegExp match as one group
|
||||
route.src = route.src
|
||||
.replace('^', `^${prefix}(`)
|
||||
.replace('(\\/', '(')
|
||||
.replace('$', ')$');
|
||||
route.dest = `${url}/$1`;
|
||||
return route;
|
||||
})
|
||||
...(await getDynamicRoutes(
|
||||
entryPath,
|
||||
entryDirectory,
|
||||
dynamicPages,
|
||||
true
|
||||
).then(arr =>
|
||||
arr.map((route: Source) => {
|
||||
// convert to make entire RegExp match as one group
|
||||
route.src = route.src
|
||||
.replace('^', `^${prefix}(`)
|
||||
.replace('(\\/', '(')
|
||||
.replace('$', ')$');
|
||||
route.dest = `${url}/$1`;
|
||||
return route;
|
||||
})
|
||||
))
|
||||
);
|
||||
|
||||
@@ -312,9 +316,16 @@ export type RoutesManifest = {
|
||||
dynamicRoutes: {
|
||||
page: string;
|
||||
regex: string;
|
||||
namedRegex?: string;
|
||||
routeKeys?: { [named: string]: string };
|
||||
}[];
|
||||
version: number;
|
||||
dataRoutes?: Array<{ page: string; dataRouteRegex: string }>;
|
||||
dataRoutes?: Array<{
|
||||
page: string;
|
||||
dataRouteRegex: string;
|
||||
namedDataRouteRegex?: string;
|
||||
routeKeys?: { [named: string]: string };
|
||||
}>;
|
||||
};
|
||||
|
||||
export async function getRoutesManifest(
|
||||
@@ -352,6 +363,20 @@ export async function getRoutesManifest(
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const routesManifest: RoutesManifest = require(pathRoutesManifest);
|
||||
|
||||
// massage temporary array based routeKeys from v1/v2 of routes
|
||||
// manifest into new object based
|
||||
for (const route of [
|
||||
...(routesManifest.dataRoutes || []),
|
||||
...(routesManifest.dynamicRoutes || []),
|
||||
]) {
|
||||
if (Array.isArray(route.routeKeys)) {
|
||||
route.routeKeys = route.routeKeys.reduce((prev, cur) => {
|
||||
prev[cur] = cur;
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
return routesManifest;
|
||||
}
|
||||
|
||||
@@ -383,6 +408,25 @@ export async function getDynamicRoutes(
|
||||
};
|
||||
});
|
||||
}
|
||||
case 3: {
|
||||
return routesManifest.dynamicRoutes
|
||||
.filter(({ page }) =>
|
||||
omittedRoutes ? !omittedRoutes.has(page) : true
|
||||
)
|
||||
.map(({ page, namedRegex, regex, routeKeys }) => {
|
||||
return {
|
||||
src: namedRegex || regex,
|
||||
dest: `${!isDev ? path.join('/', entryDirectory, page) : page}${
|
||||
routeKeys
|
||||
? `?${Object.keys(routeKeys)
|
||||
.map(key => `${routeKeys[key]}=$${key}`)
|
||||
.join('&')}`
|
||||
: ''
|
||||
}`,
|
||||
check: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
default: {
|
||||
// update MIN_ROUTES_MANIFEST_VERSION
|
||||
throw new NowBuildError({
|
||||
@@ -450,7 +494,7 @@ export async function getDynamicRoutes(
|
||||
routes.push({
|
||||
src: pageMatcher.matcher.source,
|
||||
dest,
|
||||
check: true,
|
||||
check: !isDev,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "9.2.1-canary.8",
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'build-id'
|
||||
return 'testing-build-id';
|
||||
},
|
||||
experimental: {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
destination: '/params'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
destination: '/params',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/_next/static/build-id/pages/hello.js",
|
||||
"path": "/_next/__NEXT_SCRIPT__(/hello)",
|
||||
"mustContain": "hello world"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "9.2.1-canary.8",
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/_next/static/testing-build-id/pages/index.js",
|
||||
"path": "/_next/__NEXT_SCRIPT__(/)",
|
||||
"responseHeaders": {
|
||||
"cache-control": "public,max-age=31536000,immutable"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,5 @@ module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
experimental: {
|
||||
basePath: '/docs',
|
||||
},
|
||||
basePath: '/docs',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/docs/_next/static/testing-build-id/pages/index.js",
|
||||
"path": "/docs/_next/__NEXT_SCRIPT__(/,/docs)",
|
||||
"responseHeaders": {
|
||||
"cache-control": "public,max-age=31536000,immutable"
|
||||
}
|
||||
@@ -19,6 +19,22 @@
|
||||
{
|
||||
"path": "/docs/another",
|
||||
"mustContain": "hello from another"
|
||||
},
|
||||
{
|
||||
"path": "/",
|
||||
"mustNotContain": "hello from index"
|
||||
},
|
||||
{
|
||||
"path": "/another",
|
||||
"mustNotContain": "hello from another"
|
||||
},
|
||||
{
|
||||
"path": "/_next/__NEXT_SCRIPT__(/)",
|
||||
"mustNotContain": "hello from index"
|
||||
},
|
||||
{
|
||||
"path": "/_next/__NEXT_SCRIPT__(/another)",
|
||||
"mustNotContain": "hello from another"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "9.3.1",
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
|
||||
159
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/additional.js
vendored
Normal file
159
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/additional.js
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
/* eslint-env jest */
|
||||
const fetch = require('node-fetch');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
module.exports = function(ctx) {
|
||||
it('should revalidate content properly from pathname', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/another`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
let $ = cheerio.load(await res.text());
|
||||
const initialTime = $('#time').text();
|
||||
const initialRandom = $('#random').text();
|
||||
expect($('#hello').text()).toBe('hello: world');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/another`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
$ = cheerio.load(await res2.text());
|
||||
expect($('#hello').text()).toBe('hello: world');
|
||||
expect(initialTime).not.toBe($('#time').text());
|
||||
expect(initialRandom).not.toBe($('#random').text());
|
||||
});
|
||||
|
||||
it('should revalidate content properly from dynamic pathname', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
let $ = cheerio.load(await res.text());
|
||||
const initialTime = $('#time').text();
|
||||
const initialRandom = $('#random').text();
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
$ = cheerio.load(await res2.text());
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
expect(initialTime).not.toBe($('#time').text());
|
||||
expect(initialRandom).not.toBe($('#random').text());
|
||||
});
|
||||
|
||||
it('should revalidate content properly from dynamic pathnames', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
let $ = cheerio.load(await res.text());
|
||||
const initialTime = $('#time').text();
|
||||
const initialRandom = $('#random').text();
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
expect($('#comment').text()).toBe('Comment: comment-321');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
$ = cheerio.load(await res2.text());
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
expect($('#comment').text()).toBe('Comment: comment-321');
|
||||
expect(initialTime).not.toBe($('#time').text());
|
||||
expect(initialRandom).not.toBe($('#random').text());
|
||||
});
|
||||
|
||||
it('should revalidate content properly from /_next/data pathname', async () => {
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { pageProps: data } = await res.json();
|
||||
const initialTime = data.time;
|
||||
const initialRandom = data.random;
|
||||
expect(data.world).toBe('world');
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const { pageProps: data2 } = await res2.json();
|
||||
expect(data2.world).toBe('world');
|
||||
expect(isNaN(data2.time)).toBe(false);
|
||||
expect(isNaN(data2.random)).toBe(false);
|
||||
expect(initialTime).not.toBe(data2.time);
|
||||
expect(initialRandom).not.toBe(data2.random);
|
||||
});
|
||||
|
||||
it('should revalidate content properly from /_next/data dynamic pathname', async () => {
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { pageProps: data } = await res.json();
|
||||
const initialTime = data.time;
|
||||
const initialRandom = data.random;
|
||||
expect(data.post).toBe('post-123');
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const { pageProps: data2 } = await res2.json();
|
||||
expect(data2.post).toBe('post-123');
|
||||
expect(isNaN(data2.time)).toBe(false);
|
||||
expect(isNaN(data2.random)).toBe(false);
|
||||
expect(initialTime).not.toBe(data2.time);
|
||||
expect(initialRandom).not.toBe(data2.random);
|
||||
});
|
||||
|
||||
it('should revalidate content properly from /_next/data dynamic pathnames', async () => {
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { pageProps: data } = await res.json();
|
||||
const initialTime = data.time;
|
||||
const initialRandom = data.random;
|
||||
expect(data.post).toBe('post-123');
|
||||
expect(data.comment).toBe('comment-321');
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const { pageProps: data2 } = await res2.json();
|
||||
expect(data2.post).toBe('post-123');
|
||||
expect(data2.comment).toBe('comment-321');
|
||||
expect(isNaN(data2.time)).toBe(false);
|
||||
expect(isNaN(data2.random)).toBe(false);
|
||||
expect(initialTime).not.toBe(data2.time);
|
||||
expect(initialRandom).not.toBe(data2.random);
|
||||
});
|
||||
};
|
||||
5
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/next.config.js
vendored
Normal file
5
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/next.config.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
};
|
||||
146
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/now.json
vendored
Normal file
146
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/now.json
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@vercel/next",
|
||||
"config": {
|
||||
"sharedLambdas": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/lambda",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "MISS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/forever",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/forever",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-3",
|
||||
"status": 200,
|
||||
"mustContain": "loading..."
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/blog/post-3",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/HIT|STALE/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/blog/post-4.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "MISS"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/blog/post-4.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/HIT|STALE/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-3",
|
||||
"status": 200,
|
||||
"mustContain": "post-3"
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-3/comment-3",
|
||||
"status": 200,
|
||||
"mustContain": "loading..."
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/lambda.json",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/forever.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/PRERENDER|HIT/"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/blog/post-3/comment-3",
|
||||
"status": 200,
|
||||
"mustContain": "comment-3"
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/forever.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/another2.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/another2.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nofallback/one",
|
||||
"status": 200,
|
||||
"mustContain": "one"
|
||||
},
|
||||
{
|
||||
"path": "/nofallback/two",
|
||||
"status": 200,
|
||||
"mustContain": "two"
|
||||
},
|
||||
{
|
||||
"path": "/nofallback/nope",
|
||||
"status": 404,
|
||||
"mustContain": "This page could not be found"
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/nofallback/one.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/HIT|STALE|PRERENDER/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/nofallback/two.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/HIT|STALE|PRERENDER/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/nofallback/nope.json",
|
||||
"status": 404
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/package.json
vendored
Normal file
7
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
23
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/another.js
vendored
Normal file
23
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/another.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
random: Math.random(),
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
unstable_revalidate: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time, random }) => {
|
||||
return (
|
||||
<>
|
||||
<p id="hello">hello: {world}</p>
|
||||
<span id="time">time: {time}</span>
|
||||
<span id="random">random: {random}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
21
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/another2.js
vendored
Normal file
21
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/another2.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
unstable_revalidate: 5,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
40
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/blog/[post]/[comment].js
vendored
Normal file
40
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/blog/[post]/[comment].js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
'/blog/post-1/comment-1',
|
||||
{ params: { post: 'post-2', comment: 'comment-2' } },
|
||||
'/blog/post-1337/comment-1337',
|
||||
'/blog/post-123/comment-321'
|
||||
],
|
||||
fallback: true,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
post: params.post,
|
||||
random: Math.random(),
|
||||
comment: params.comment,
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
unstable_revalidate: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ post, comment, time, random }) => {
|
||||
if (!post) return <p>loading...</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id='post'>Post: {post}</p>
|
||||
<p id='comment'>Comment: {comment}</p>
|
||||
<span id='time'>time: {time}</span>
|
||||
<span id='random'>random: {random}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
43
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/blog/[post]/index.js
vendored
Normal file
43
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/blog/[post]/index.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
'/blog/post-1',
|
||||
{ params: { post: 'post-2' } },
|
||||
'/blog/post-123',
|
||||
],
|
||||
fallback: true,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticProps({ params }) {
|
||||
if (params.post === 'post-10') {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
post: params.post,
|
||||
random: Math.random(),
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
unstable_revalidate: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ post, time, random }) => {
|
||||
if (!post) return <p>loading...</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id='post'>Post: {post}</p>
|
||||
<span id='time'>time: {time}</span>
|
||||
<span id='random'>random: {random}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
21
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/forever.js
vendored
Normal file
21
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/forever.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
unstable_revalidate: false,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/index.js
vendored
Normal file
1
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default () => 'Hi';
|
||||
5
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/lambda.js
vendored
Normal file
5
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/lambda.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const Page = ({ data }) => <p>{data} world</p>;
|
||||
|
||||
Page.getInitialProps = () => ({ data: 'hello' });
|
||||
|
||||
export default Page;
|
||||
31
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/nofallback/[slug].js
vendored
Normal file
31
packages/now-next/test/fixtures/22-ssg-v2-shared-lambdas/pages/nofallback/[slug].js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: ['/nofallback/one', { params: { slug: 'two' } }],
|
||||
fallback: false,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
slug: params.slug,
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
unstable_revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ slug, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Slug ({slug.length}): {slug}
|
||||
</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
159
packages/now-next/test/fixtures/22-ssg-v2/additional.js
vendored
Normal file
159
packages/now-next/test/fixtures/22-ssg-v2/additional.js
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
/* eslint-env jest */
|
||||
const fetch = require('node-fetch');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
module.exports = function(ctx) {
|
||||
it('should revalidate content properly from pathname', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/another`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
let $ = cheerio.load(await res.text());
|
||||
const initialTime = $('#time').text();
|
||||
const initialRandom = $('#random').text();
|
||||
expect($('#hello').text()).toBe('hello: world');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/another`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
$ = cheerio.load(await res2.text());
|
||||
expect($('#hello').text()).toBe('hello: world');
|
||||
expect(initialTime).not.toBe($('#time').text());
|
||||
expect(initialRandom).not.toBe($('#random').text());
|
||||
});
|
||||
|
||||
it('should revalidate content properly from dynamic pathname', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
let $ = cheerio.load(await res.text());
|
||||
const initialTime = $('#time').text();
|
||||
const initialRandom = $('#random').text();
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
$ = cheerio.load(await res2.text());
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
expect(initialTime).not.toBe($('#time').text());
|
||||
expect(initialRandom).not.toBe($('#random').text());
|
||||
});
|
||||
|
||||
it('should revalidate content properly from dynamic pathnames', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
let $ = cheerio.load(await res.text());
|
||||
const initialTime = $('#time').text();
|
||||
const initialRandom = $('#random').text();
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
expect($('#comment').text()).toBe('Comment: comment-321');
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
$ = cheerio.load(await res2.text());
|
||||
expect($('#post').text()).toBe('Post: post-123');
|
||||
expect($('#comment').text()).toBe('Comment: comment-321');
|
||||
expect(initialTime).not.toBe($('#time').text());
|
||||
expect(initialRandom).not.toBe($('#random').text());
|
||||
});
|
||||
|
||||
it('should revalidate content properly from /_next/data pathname', async () => {
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { pageProps: data } = await res.json();
|
||||
const initialTime = data.time;
|
||||
const initialRandom = data.random;
|
||||
expect(data.world).toBe('world');
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const { pageProps: data2 } = await res2.json();
|
||||
expect(data2.world).toBe('world');
|
||||
expect(isNaN(data2.time)).toBe(false);
|
||||
expect(isNaN(data2.random)).toBe(false);
|
||||
expect(initialTime).not.toBe(data2.time);
|
||||
expect(initialRandom).not.toBe(data2.random);
|
||||
});
|
||||
|
||||
it('should revalidate content properly from /_next/data dynamic pathname', async () => {
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { pageProps: data } = await res.json();
|
||||
const initialTime = data.time;
|
||||
const initialRandom = data.random;
|
||||
expect(data.post).toBe('post-123');
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const { pageProps: data2 } = await res2.json();
|
||||
expect(data2.post).toBe('post-123');
|
||||
expect(isNaN(data2.time)).toBe(false);
|
||||
expect(isNaN(data2.random)).toBe(false);
|
||||
expect(initialTime).not.toBe(data2.time);
|
||||
expect(initialRandom).not.toBe(data2.random);
|
||||
});
|
||||
|
||||
it('should revalidate content properly from /_next/data dynamic pathnames', async () => {
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const { pageProps: data } = await res.json();
|
||||
const initialTime = data.time;
|
||||
const initialRandom = data.random;
|
||||
expect(data.post).toBe('post-123');
|
||||
expect(data.comment).toBe('comment-321');
|
||||
expect(isNaN(initialTime)).toBe(false);
|
||||
expect(isNaN(initialRandom)).toBe(false);
|
||||
|
||||
// wait for revalidation to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const res2 = await fetch(
|
||||
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
|
||||
);
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
const { pageProps: data2 } = await res2.json();
|
||||
expect(data2.post).toBe('post-123');
|
||||
expect(data2.comment).toBe('comment-321');
|
||||
expect(isNaN(data2.time)).toBe(false);
|
||||
expect(isNaN(data2.random)).toBe(false);
|
||||
expect(initialTime).not.toBe(data2.time);
|
||||
expect(initialRandom).not.toBe(data2.random);
|
||||
});
|
||||
};
|
||||
@@ -24,51 +24,6 @@
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/another",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/another",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-1",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/blog/post-1",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-2",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{ "delay": 2000 },
|
||||
{
|
||||
"path": "/blog/post-2",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-3",
|
||||
"status": 200,
|
||||
@@ -102,20 +57,6 @@
|
||||
"status": 200,
|
||||
"mustContain": "post-3"
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-1/comment-1",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-2/comment-2",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/blog/post-3/comment-3",
|
||||
"status": 200,
|
||||
@@ -145,13 +86,6 @@
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/another.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/HIT|STALE/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/another2.json",
|
||||
"status": 200,
|
||||
@@ -167,20 +101,6 @@
|
||||
"x-vercel-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/blog/post-1.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "/HIT|STALE|PRERENDER/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/_next/data/testing-build-id/blog/post-1337/comment-1337.json",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"x-vercel-cache": "PRERENDER"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nofallback/one",
|
||||
"status": 200,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "9.2.3-canary.14",
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps() {
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
random: Math.random(),
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: 5,
|
||||
unstable_revalidate: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time }) => {
|
||||
export default ({ world, time, random }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
<p id="hello">hello: {world}</p>
|
||||
<span id="time">time: {time}</span>
|
||||
<span id="random">random: {random}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps() {
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: 5,
|
||||
unstable_revalidate: 5,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticPaths() {
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
'/blog/post-1/comment-1',
|
||||
{ params: { post: 'post-2', comment: 'comment-2' } },
|
||||
'/blog/post-1337/comment-1337',
|
||||
'/blog/post-123/comment-321'
|
||||
],
|
||||
fallback: true,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps({ params }) {
|
||||
export async function getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
post: params.post,
|
||||
random: Math.random(),
|
||||
comment: params.comment,
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: 2,
|
||||
unstable_revalidate: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ post, comment, time }) => {
|
||||
export default ({ post, comment, time, random }) => {
|
||||
if (!post) return <p>loading...</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Post: {post}</p>
|
||||
<p>Comment: {comment}</p>
|
||||
<span>time: {time}</span>
|
||||
<p id='post'>Post: {post}</p>
|
||||
<p id='comment'>Comment: {comment}</p>
|
||||
<span id='time'>time: {time}</span>
|
||||
<span id='random'>random: {random}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticPaths() {
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: ['/blog/post-1', { params: { post: 'post-2' } }],
|
||||
paths: [
|
||||
'/blog/post-1',
|
||||
{ params: { post: 'post-2' } },
|
||||
'/blog/post-123',
|
||||
],
|
||||
fallback: true,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps({ params }) {
|
||||
export async function getStaticProps({ params }) {
|
||||
if (params.post === 'post-10') {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 1000);
|
||||
@@ -19,19 +23,21 @@ export async function unstable_getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
post: params.post,
|
||||
random: Math.random(),
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
revalidate: 10,
|
||||
unstable_revalidate: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ post, time }) => {
|
||||
export default ({ post, time, random }) => {
|
||||
if (!post) return <p>loading...</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Post: {post}</p>
|
||||
<span>time: {time}</span>
|
||||
<p id='post'>Post: {post}</p>
|
||||
<span id='time'>time: {time}</span>
|
||||
<span id='random'>random: {random}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps() {
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: false,
|
||||
unstable_revalidate: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticPaths() {
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: ['/nofallback/one', { params: { slug: 'two' } }],
|
||||
fallback: false,
|
||||
@@ -9,13 +9,13 @@ export async function unstable_getStaticPaths() {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps({ params }) {
|
||||
export async function getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
slug: params.slug,
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
revalidate: 10,
|
||||
unstable_revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ module.exports = {
|
||||
source: '/:path/post-321',
|
||||
destination: '/with-params',
|
||||
},
|
||||
{
|
||||
source: '/a/catch-all/:path*',
|
||||
destination: '/a/catch-all',
|
||||
},
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
|
||||
// should match /_next file after rewrite
|
||||
{
|
||||
"path": "/hidden/_next/static/testing-build-id/pages/hello.js",
|
||||
"path": "/hidden/_next/__NEXT_SCRIPT__(/hello)",
|
||||
"mustContain": "createElement"
|
||||
},
|
||||
|
||||
@@ -245,6 +245,20 @@
|
||||
"path": "/hello/post-123.html",
|
||||
"status": 200,
|
||||
"mustContain": "123"
|
||||
},
|
||||
|
||||
// should rewrite to catch-all with dash in segment name
|
||||
{
|
||||
"path": "/catchall-dash/hello/world",
|
||||
"status": 200,
|
||||
"mustContain": "hello/world"
|
||||
},
|
||||
|
||||
// should rewrite and normalize catch-all rewrite param
|
||||
{
|
||||
"path": "/a/catch-all/hello/world",
|
||||
"status": 200,
|
||||
"mustContain": "hello/world"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user