Compare commits

..

35 Commits

Author SHA1 Message Date
Steven
49e2274d81 Publish Canary
- @vercel/build-utils@2.4.1-canary.0
 - vercel@19.1.2-canary.8
2020-06-24 09:24:46 -04:00
Steven
9af3938544 [cli][build-utils] Refactor ajv error message (#4705)
This PR is a follow up to #4694 so we can use this same helper function in the API.

I tried to follow the existing error message format so that the API can use a different prefix than the CLI.

The API currently returns a 400 with `Invalid request` when a Git deploy fails validation:

![api-comment](https://user-images.githubusercontent.com/229881/85466548-1c140b00-b578-11ea-9bed-1eb14df2685a.png)
2020-06-24 13:23:51 +00:00
Nathan Rajlich
7b5bf061c2 Publish Canary
- vercel@19.1.2-canary.7
 - @vercel/go@1.1.3-canary.0
2020-06-22 19:12:28 -07:00
Nathan Rajlich
ffb98781f1 [cli] Remove caching of vercel.json config in vc dev (#4697)
Since the filesystem watcher may be slow, it's actually faster and more
reliable to simply re-read the `vercel.json` configuration for every
HTTP request. This also simplifies the logic as an added benefit.

Some `sleep()` calls are removed from relevant tests that were
previously necessary due to the lag in the filesystem watcher.
2020-06-22 23:45:23 +00:00
Steven
3411fcbb68 Publish Canary
- vercel@19.1.2-canary.6
2020-06-22 18:42:23 -04:00
Steven
55cfd33338 [cli] Improve validation error message for vercel.json (#4694)
This PR improves the validation error message when the user has an invalid `vercel.json` file.

Previously, the error message did not account for nested properties so `{"foo": "will error"}` looked fine because it would mention there is an additional property `foo`. However, the error message for `{ "routes": [{ "foo": "will error" }] }` did not mention anything about `routes` when it explaining there was an additional property `foo`. This became more apparent as we added nested properties for `rewrites` and `redirects` (see tests in this PR).

This PR also adds suggestions for common mistakes such as `src` vs `source`.
2020-06-22 22:38:13 +00:00
JJ Kasper
93a9e5bed3 Publish Canary
- vercel@19.1.2-canary.5
 - @vercel/next@2.6.8-canary.2
2020-06-22 16:38:23 -05:00
JJ Kasper
b454021234 [next] Update tests to run against latest canary (#4682)
In the latest canary of Next.js pages are no longer nested under the `BUILD_ID` folder and instead are nested under a hash for the page bundle content. To prevent these tests from breaking too often from changes in canary this updates to locate the page bundle using the `buildManifest`. This also updates our latest SSG fixture to test against the latest canary to help ensure the feature doesn't break with a new canary release
2020-06-22 21:31:37 +00:00
Nathan Rajlich
bb705cd091 [docs] Refactor "Developing a Runtime" docs and add startDevServer() (#4675)
Co-authored-by: Steven <steven@ceriously.com>
2020-06-22 13:41:10 -07:00
Naoyuki Kanezawa
0986f4bcb6 [cli] Do not confirm vc env pull overwrite if created by CLI (#4678)
Append `# Created by Vercel CLI` to the head of `.env` file and automatically overwrite the file if it's there next time without confirmation.

https://app.clubhouse.io/vercel/story/316
2020-06-22 18:47:34 +00:00
Steven
83f77223aa Publish Canary
- vercel@19.1.2-canary.4
2020-06-19 16:23:32 -04:00
Steven
effda1fa6c [cli] Fix error message when no internet connectivity (#4687)
This PR fixes the error message when the client does not have internet connectivity or perhaps DNS is misconfigured such that the hostname cannot be resolved.
2020-06-19 15:57:26 -04:00
Steven
2ab6a7ef0c Publish Canary
- vercel@19.1.2-canary.3
 - @vercel/next@2.6.8-canary.1
2020-06-18 19:57:28 -04:00
Jean Helou
200bf5e996 [next] Fix nextjs routes in vercel dev (#4510)
This PR fixes #4239 where using `vercel dev` to work on monorepos where
the Next.js app is not in the topmost directory fails to correctly route
to dynamic pages.
After investigating the devServer router, @styfle prompted me to
investigate the @vercel/next builder. He also suggested restricting
`check` to be false only when running in `now dev`.

Co-authored-by: Steven <steven@ceriously.com>
2020-06-18 19:28:53 -04:00
Steven
94ab2512e9 Publish Canary
- vercel@19.1.2-canary.2
 - @vercel/ruby@1.2.3-canary.0
2020-06-18 17:19:29 -04:00
Jared White
da3207278e [ruby] Fix for UTF-8 responses in Ruby functions (#4593)
There's an unexpected string encoding issue with `Net::HTTP`, so this is a workaround. Further details:
https://bugs.ruby-lang.org/issues/15517

Co-authored-by: Steven <steven@ceriously.com>
2020-06-18 17:12:28 -04:00
nkzawa
8e10a82b43 Publish Canary
- vercel@19.1.2-canary.1
2020-06-18 20:24:49 +09:00
Naoyuki Kanezawa
d7731d191b Fix to not render no token error, prompt login instead (#4659)
When `auth.json` exists but no `token` property exists in it, we currently render error like the following screenshot.
<img width="488" alt="Screen Shot 2020-06-15 at 21 32 55" src="https://user-images.githubusercontent.com/775227/84657782-cd4eeb80-af4f-11ea-901d-2ad4b52b6667.png">

This PR fixes to prompt login instead.
<img width="994" alt="Screen Shot 2020-06-15 at 21 28 09" src="https://user-images.githubusercontent.com/775227/84657706-aa243c00-af4f-11ea-915e-4ada7de9c467.png">
2020-06-17 17:20:08 +00:00
Steven
0f5f99e667 Publish Canary
- vercel@19.1.2-canary.0
 - @vercel/next@2.6.8-canary.0
 - @vercel/node@1.7.2-canary.0
2020-06-16 17:41:45 -04:00
Steven
93a7831943 [node][next] Bump node-file-trace to 0.6.5 (#4667)
Bump `node-file-trace` to version [0.6.5](https://github.com/vercel/node-file-trace/releases/tag/0.6.5) to fix a webpack wrappers bug
2020-06-16 21:41:13 +00:00
Steven
0eacbeae11 Publish Stable
- vercel@19.1.1
 - @vercel/next@2.6.7
 - @vercel/node@1.7.1
 - @vercel/static-build@0.17.3
2020-06-16 13:09:42 -04:00
Steven
e50417dc47 Publish Canary
- vercel@19.1.1-canary.4
 - @vercel/static-build@0.17.3-canary.0
2020-06-16 08:55:29 -04:00
Paco
4840a25d30 [static-build] Apply Gatsby default caching routes automatically (#4645)
CH-702
Fixes #3331

If the file generated by `gatsby-plugin-now` does not exist, we'll set up these routes automatically.
2020-06-15 22:15:14 +00:00
Steven
785e91979d Publish Canary
- vercel@19.1.1-canary.3
 - @vercel/next@2.6.7-canary.1
 - @vercel/node@1.7.1-canary.1
2020-06-15 16:46:03 -04:00
Steven
7a776c54b8 [node][next] Bump node-file-trace to 0.6.4 (#4617)
Bump node-file-trace to fix a webpack tracing bug that looks like the following:


```
TypeError: Cannot read property 'type' of null
    at handleWrappers (/vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:12763:58)
    at module.exports.447.module.exports (/vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:14674:3)
    at Job.emitDependency (/vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:11730:40)
    at /vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:11755:20
    at async Promise.all (index 9)
    at async Job.emitDependency (/vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:11736:5)
    at async Promise.all (index 1)
    at async Object.module.exports.328.module.exports [as default] (/vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:11514:3)
    at async Object.module.exports.178.exports.build (/vercel/e77875438c1cd74b/.build-utils/.builder/node_modules/@vercel/next/dist/index.js:6076:69)
    at async buildStep (/var/task/sandbox-worker.js:20407:20)
```

- [0.6.2](https://github.com/vercel/node-file-trace/releases/tag/0.6.2)
- [0.6.3](https://github.com/vercel/node-file-trace/releases/tag/0.6.3) 
- [0.6.4](https://github.com/vercel/node-file-trace/releases/tag/0.6.4)

Depends on #4355
2020-06-15 20:45:15 +00:00
JJ Kasper
997031c53b [next] Update handling experimental basePath (#3791)
As mentioned by @dav-is we can prefix the outputs with the `basePath` instead of using a rewrite so that we make sure to 404 when the basePath isn't prefixed on the output. This behavior is also matched in Next.js with [this PR](https://github.com/zeit/next.js/pull/9988). 

x-ref: https://github.com/zeit/now/pull/3478
2020-06-15 17:43:37 +00:00
Steven
fdee03a599 Publish Canary
- vercel@19.1.1-canary.2
2020-06-15 12:38:11 -04:00
Steven
c5a93ecdad [cli] Fix vc dev proxy to upstream frontend dev server (#4644)
This PR fixes a longstanding issue introduced in #3673 that prevents routing properties from applying to the framework's upstream dev server.

This mimic's the older proxy logic used in build matches here: 5035fa537f/packages/now-cli/src/util/dev/server.ts (L1535-L1539)

- Related to #3777
- Related to #4510
2020-06-15 16:37:08 +00:00
JJ Kasper
5a9391b7ce Publish Canary
- vercel@19.1.1-canary.1
 - @vercel/next@2.6.7-canary.0
2020-06-15 10:20:38 -05:00
JJ Kasper
99e49473b8 [next] Update dynamic route named regexes and route keys (#4355)
This updates to leverage changes from https://github.com/zeit/next.js/pull/12801 which resolves invalid named regexes being used when the segments contain non-word characters e.g. `/[hello-world]`. 

Failing page names have also been added to the `23-custom-routes-verbose` fixture. Since the routeKeys aren't applied for dynamic routes in this PR until the routes-manifest version is bumped in the latest canary of Next.js the added test cases will be passing now and should be re-run to ensure passing after a new canary of Next.js is released with the routes-manifest version bump
2020-06-15 15:15:38 +00:00
Nathan Rajlich
de1cc6f9a7 [cli] Add a test case for resolving default "typescript" version in @vercel/node (#4656)
Adds a test case for #4655 (it got auto-merged too quickly).
2020-06-14 21:52:33 +00:00
Nathan Rajlich
08eedd8f34 Publish Canary
- vercel@19.1.1-canary.0
 - @vercel/node@1.7.1-canary.0
2020-06-14 14:00:04 -07:00
Nathan Rajlich
5021a71a8e [node] Don't resolve "typescript" from the dist dir (#4655)
On Node 10, the `require.resolve()` with "paths" does not return the
proper value relative to the `node_modules` directory. To wit:

```
$ node -v
v10.16.3

$ node -p "require.resolve('typescript', { paths: [process.cwd()] })"
/Users/nrajlich/Code/vercel/vercel/packages/now-node/dist/typescript.js

$ node -v
v14.4.0

$ node -p "require.resolve('typescript', { paths: [process.cwd()] })"
/Users/nrajlich/Code/vercel/vercel/node_modules/typescript/lib/typescript.js
```

(**Note:** cwd when running these commands is the `dist` dir of `@vercel/node`)

So the solution is to just let `require.resolve()` throw an error so the
default string "typescript" is used instead of a resolved absolute path.
2020-06-14 20:47:23 +00:00
Max Leiter
56671d7c2f [tests] Don't revoke VERCEL_TOKEN after tests run (#4643)
`VERCEL_TOKEN` will no longer be invalidated during tests

Story: https://app.clubhouse.io/vercel/story/2281
2020-06-12 22:49:37 +00:00
Steven
5035fa537f [cli] Add test for gatsby cache (#4447)
This PR adds a E2E CLI test to ensure that the Gatsby example deploys correctly and that the second deployment has the proper cached directories.

Co-authored-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
2020-06-12 16:51:55 -04:00
72 changed files with 7090 additions and 480 deletions

View File

@@ -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';
```

View File

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

View File

@@ -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.';
}

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "19.1.0",
"version": "19.1.2-canary.8",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -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.0",
"@vercel/go": "1.1.3-canary.0",
"@vercel/next": "2.6.8-canary.2",
"@vercel/node": "1.7.2-canary.0",
"@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.3"
},
"devDependencies": {
"@sentry/node": "5.5.0",

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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';
@@ -122,7 +112,6 @@ export default class DevServer {
public address: string;
public devCacheDir: string;
private cachedNowConfig: NowConfig | null;
private caseSensitive: boolean;
private apiDir: string | null;
private apiExtensions: Set<string>;
@@ -141,7 +130,6 @@ export default class DevServer {
private devProcessPort?: number;
private devServerPids: Set<number>;
private getNowConfigPromise: Promise<NowConfig> | null;
private blockingBuildsPromise: Promise<void> | null;
private updateBuildersPromise: Promise<void> | null;
private updateBuildersTimeout: NodeJS.Timeout | undefined;
@@ -155,7 +143,6 @@ export default class DevServer {
this.address = '';
this.devCommand = options.devCommand;
this.frameworkSlug = options.frameworkSlug;
this.cachedNowConfig = null;
this.caseSensitive = false;
this.apiDir = null;
this.apiExtensions = new Set();
@@ -171,7 +158,6 @@ export default class DevServer {
this.inProgressBuilds = new Map();
this.devCacheDir = join(getVercelDirectory(cwd), 'cache');
this.getNowConfigPromise = null;
this.blockingBuildsPromise = null;
this.updateBuildersPromise = null;
@@ -235,16 +221,7 @@ export default class DevServer {
}
}
const nowConfig = await this.getNowConfig(false);
// Update the env vars configuration
const nowConfigBuild = nowConfig.build || {};
const [runEnv, buildEnv] = await Promise.all([
this.getLocalEnv('.env', nowConfig.env),
this.getLocalEnv('.env.build', nowConfigBuild.env),
]);
const allEnv = { ...buildEnv, ...runEnv };
this.envConfigs = { buildEnv, runEnv, allEnv };
const nowConfig = await this.getNowConfig();
// Update the build matches in case an entrypoint was created or deleted
await this.updateBuildMatches(nowConfig);
@@ -511,23 +488,7 @@ export default class DevServer {
return {};
}
async getNowConfig(canUseCache: boolean = true): Promise<NowConfig> {
if (this.getNowConfigPromise) {
return this.getNowConfigPromise;
}
this.getNowConfigPromise = this._getNowConfig(canUseCache);
try {
return await this.getNowConfigPromise;
} finally {
this.getNowConfigPromise = null;
}
}
async _getNowConfig(canUseCache: boolean = true): Promise<NowConfig> {
if (canUseCache && this.cachedNowConfig) {
return this.cachedNowConfig;
}
async getNowConfig(): Promise<NowConfig> {
const pkg = await this.getPackageJson();
// The default empty `vercel.json` is used to serve all files as static
@@ -542,7 +503,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());
@@ -644,10 +605,19 @@ export default class DevServer {
await this.validateNowConfig(config);
this.cachedNowConfig = config;
this.caseSensitive = hasNewRoutingProperties(config);
this.apiDir = detectApiDirectory(config.builds || []);
this.apiExtensions = detectApiExtensions(config.builds || []);
// Update the env vars configuration
const configBuild = config.build || {};
const [runEnv, buildEnv] = await Promise.all([
this.getLocalEnv('.env', config.env),
this.getLocalEnv('.env.build', configBuild.env),
]);
const allEnv = { ...buildEnv, ...runEnv };
this.envConfigs = { buildEnv, runEnv, allEnv };
return config;
}
@@ -693,14 +663,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 {
@@ -770,14 +738,7 @@ export default class DevServer {
this.filter = ig.createFilter();
// Retrieve the path of the native module
const nowConfig = await this.getNowConfig(false);
const nowConfigBuild = nowConfig.build || {};
const [runEnv, buildEnv] = await Promise.all([
this.getLocalEnv('.env', nowConfig.env),
this.getLocalEnv('.env.build', nowConfigBuild.env),
]);
const allEnv = { ...buildEnv, ...runEnv };
this.envConfigs = { buildEnv, runEnv, allEnv };
const nowConfig = await this.getNowConfig();
const opts = { output: this.output, isBuilds: true };
const files = await getFiles(this.cwd, nowConfig, opts);
@@ -1503,16 +1464,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 (

View File

@@ -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;

View 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'
);
});

View File

@@ -3,7 +3,7 @@
"private": true,
"scripts": {
"build": "gridsome build",
"develop": "gridsome develop",
"dev": "gridsome develop -p $PORT",
"explore": "gridsome explore"
},
"dependencies": {

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
{
"redirects": [{ "source": "/support", "destination": "/about?ref=support" }]
}

View File

@@ -0,0 +1 @@
<h1>Contact Us</h1>

View File

@@ -0,0 +1,3 @@
{
"rewrites": [{ "source": "/support", "destination": "/contact.html" }]
}

View File

@@ -0,0 +1,3 @@
export default function Contact() {
return <h1>Contact Page</h1>;
}

View File

@@ -0,0 +1,3 @@
{
"rewrites": [{ "source": "/support", "destination": "/contact" }]
}

View File

@@ -0,0 +1,6 @@
module.exports = {
assetPrefix: '/blog',
env: {
ASSET_PREFIX: '/blog',
},
};

View File

@@ -0,0 +1,10 @@
{
"name": "with-zones-blog",
"version": "1.0.0",
"dependencies": {
"next": "latest",
"react": "latest",
"react-dom": "latest"
},
"license": "ISC"
}

View File

@@ -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>
);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,7 @@
const Header = () => (
<div>
<h2>The Company</h2>
</div>
);
export default Header;

View File

@@ -0,0 +1,10 @@
{
"name": "with-zones-home",
"version": "1.0.0",
"dependencies": {
"next": "latest",
"react": "latest",
"react-dom": "latest"
},
"license": "ISC"
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -0,0 +1,11 @@
{
"name": "with-zones-example",
"private": true,
"workspaces": [
"home",
"blog"
],
"devDependencies": {
"vercel": "canary"
}
}

View File

@@ -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" }
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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';
@@ -410,8 +411,6 @@ test('[vercel dev] `vercel.json` should be invalidated if deleted', async t => {
{
// Env var should not be set after `vercel.json` is deleted
await fs.remove(configPath);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/api`);
const body = await res.json();
t.is(body.FOO, undefined);
@@ -453,8 +452,6 @@ test('[vercel dev] reflects changes to config and env without restart', async t
],
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=bar`);
const body = await res.json();
t.is(body.hasHelpers, false);
@@ -475,8 +472,6 @@ test('[vercel dev] reflects changes to config and env without restart', async t
],
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=baz`);
const body = await res.json();
t.is(body.hasHelpers, true);
@@ -494,8 +489,6 @@ test('[vercel dev] reflects changes to config and env without restart', async t
},
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=baz`);
const body = await res.json();
t.is(body.hasHelpers, false);
@@ -513,8 +506,6 @@ test('[vercel dev] reflects changes to config and env without restart', async t
},
};
await fs.writeJSON(configPath, config);
await sleep(1000);
const res = await fetch(`http://localhost:${port}/?foo=boo`);
const body = await res.json();
t.is(body.hasHelpers, true);
@@ -526,6 +517,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 => {
@@ -664,7 +684,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 +695,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 +704,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 +717,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 +728,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 +739,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 +750,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,6 +1000,19 @@ 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 => {
@@ -1059,6 +1095,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 +1109,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 +1136,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 +1148,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 +1160,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 +1172,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 +1184,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 +1196,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 }
)

View File

@@ -138,6 +138,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,

View File

@@ -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();
@@ -1473,9 +1527,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 +1551,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 => {
@@ -2934,3 +3015,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);
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "1.1.2",
"version": "1.1.3-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "2.6.6",
"version": "2.6.8-canary.2",
"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",

View File

@@ -208,7 +208,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 +422,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 +467,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 +486,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 +503,7 @@ export const build = async ({
});
}
nextBasePathRoute.push({
src: `^${nextBasePath}(?:$|/(.*))$`,
dest: `/$1`,
continue: true,
});
entryDirectory = path.join(entryDirectory, nextBasePath);
}
break;
}
@@ -565,9 +567,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 +596,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 +616,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.
@@ -1583,9 +1582,6 @@ export const build = async ({
- Builder rewrites
*/
routes: [
// Add top level rewrite for basePath if provided
...nextBasePathRoute,
// headers
...headers,
@@ -1620,7 +1616,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 +1646,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.

View File

@@ -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,
});
}
});

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "9.2.1-canary.8",
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}

View File

@@ -1,15 +1,15 @@
module.exports = {
generateBuildId() {
return 'build-id'
return 'testing-build-id';
},
experimental: {
async rewrites() {
return [
{
source: '/:path*',
destination: '/params'
}
]
}
}
}
destination: '/params',
},
];
},
},
};

View File

@@ -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"
},
{

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "9.2.1-canary.8",
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}

View File

@@ -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"
}

View File

@@ -2,7 +2,5 @@ module.exports = {
generateBuildId() {
return 'testing-build-id';
},
experimental: {
basePath: '/docs',
},
basePath: '/docs',
};

View File

@@ -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"
}
]
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "9.2.3-canary.14",
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -1,7 +1,7 @@
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',
@@ -13,14 +13,14 @@ export async function unstable_getStaticPaths() {
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps({ params }) {
export async function getStaticProps({ params }) {
return {
props: {
post: params.post,
comment: params.comment,
time: new Date().getTime(),
},
revalidate: 2,
unstable_revalidate: 2,
};
}

View File

@@ -1,7 +1,7 @@
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' } }],
fallback: true,
@@ -9,7 +9,7 @@ export async function unstable_getStaticPaths() {
}
// 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);
@@ -21,7 +21,7 @@ export async function unstable_getStaticProps({ params }) {
post: params.post,
time: (await import('perf_hooks')).performance.now(),
},
revalidate: 10,
unstable_revalidate: 10,
};
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -85,6 +85,10 @@ module.exports = {
source: '/:path/post-321',
destination: '/with-params',
},
{
source: '/a/catch-all/:path*',
destination: '/a/catch-all',
},
];
},
async redirects() {

View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,9 @@
import { useRouter } from 'next/router';
export default () => <p>{useRouter().query.path?.join('/')}</p>;
export const getServerSideProps = () => {
return {
props: {},
};
};

View File

@@ -0,0 +1,17 @@
import { useRouter } from 'next/router'
const Page = () => {
return (
<p>path: {useRouter().query['hello-world']?.join('/')}</p>
)
}
export default Page
export const getServerSideProps = () => {
return {
props: {
hello: 'world'
}
}
}

View File

@@ -0,0 +1 @@
export default () => 'hi'

View File

@@ -22,10 +22,10 @@ it(
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
filePath.match(/static.*\/pages\/_app-.*\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
filePath.match(/static.*\/pages\/_error-.*\.js$/)
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
@@ -310,10 +310,10 @@ it(
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
filePath.match(/static.*\/pages\/_app-.*\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
filePath.match(/static.*\/pages\/_error-.*\.js$/)
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
@@ -345,10 +345,10 @@ it(
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
filePath.match(/static.*\/pages\/_app-.*\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
filePath.match(/static.*\/pages\/_error-.*\.js$/)
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
@@ -380,10 +380,10 @@ it(
filePath.match(/_error/)
);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
filePath.match(/static.*\/pages\/_app-.*\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
filePath.match(/static.*\/pages\/_error-.*\.js$/)
);
const hasBuildFile = await fs.pathExists(
path.join(__dirname, 'serverless-no-config-build'),

View File

@@ -117,7 +117,7 @@ describe('build meta dev', () => {
{
src: '^/(nested\\/([^/]+?)(?:\\/)?)$',
dest: 'http://localhost:5000/$1',
check: true,
check: false /* We cannot check the filesystem for a url */,
},
{ src: '/data.txt', dest: 'http://localhost:5000/data.txt' },
]);

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "1.7.0",
"version": "1.7.2-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -33,7 +33,7 @@
"@types/etag": "1.8.0",
"@types/test-listen": "1.1.0",
"@zeit/ncc": "0.20.4",
"@zeit/node-file-trace": "0.6.1",
"@zeit/node-file-trace": "0.6.5",
"content-type": "1.0.4",
"cookie": "0.4.0",
"etag": "1.8.1",

View File

@@ -15,7 +15,7 @@ import { register } from 'ts-node';
let compiler: string;
try {
compiler = require.resolve('typescript', {
paths: [process.cwd(), __dirname],
paths: [process.cwd()],
});
} catch (e) {
compiler = 'typescript';

View File

@@ -60,9 +60,19 @@ def webrick_handler(httpMethod, path, body, headers)
server.shutdown
Thread.kill(th)
# Net::HTTP doesnt read the set the encoding so we must set manually.
# Bug: https://bugs.ruby-lang.org/issues/15517
# More: https://yehudakatz.com/2010/05/17/encodings-unabridged/
res_headers = res.each_capitalized.to_h
if res_headers["Content-Type"] && res_headers["Content-Type"].include?("charset=")
res_encoding = res_headers["Content-Type"].match(/charset=([^;]*)/)[1]
res.body.force_encoding(res_encoding)
res.body = res.body.encode(res_encoding)
end
{
:statusCode => res.code.to_i,
:headers => res.each_capitalized.to_h,
:headers => res_headers,
:body => res.body,
}
end

View File

@@ -1,7 +1,7 @@
{
"name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.2.2",
"version": "1.2.3-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",

View File

@@ -0,0 +1,6 @@
Handler = Proc.new do |req, res|
res.status = 200
res['Content-Type'] = 'text/text; charset=utf-8'
res.body = '🐄RANDOMNESS_PLACEHOLDER'
end

View File

@@ -0,0 +1,5 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }],
"probes": [{ "path": "/", "mustContain": "🐄RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "0.17.2",
"version": "0.17.3",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/static-builds",

View File

@@ -43,6 +43,7 @@ const frameworkList: Framework[] = [
buildCommand: 'gatsby build',
getOutputDirName: async () => 'public',
defaultRoutes: async (dirPrefix: string) => {
// This file could be generated by gatsby-plugin-now or gatsby-plugin-zeit-now
try {
const nowRoutesPath = join(
dirPrefix,
@@ -58,8 +59,34 @@ const frameworkList: Framework[] = [
}
return nowRoutes;
} catch (err) {
// if the file doesn't exist, we don't create routes
return [];
// if the file doesn't exist, we implement gatsby's recommendations
// https://www.gatsbyjs.org/docs/caching
return [
{
src: '^/static/(.*)$',
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{
src: '^/.*\\.(js|css)$',
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{
src: '^/(sw\\.js|app-data\\.json|.*\\.html|page-data/.*)$',
headers: { 'cache-control': 'public,max-age=0,must-revalidate' },
continue: true,
},
{
handle: 'filesystem',
},
{
src: '.*',
status: 404,
dest: '404.html',
},
];
}
},
cachePattern: '{.cache,public}/**',

View File

@@ -7,5 +7,49 @@
"config": { "zeroConfig": true }
}
],
"probes": [{ "path": "/", "mustContain": "Welcome to your new Gatsby site" }]
"probes": [
{ "path": "/", "mustContain": "Welcome to your new Gatsby site" },
{
"path": "/static/d/856328897.json",
"responseHeaders": {
"cache-control": "public,max-age=31536000,immutable"
}
},
{
"path": "/app-08a9fb9626777db1bf33.js",
"responseHeaders": {
"cache-control": "public,max-age=31536000,immutable"
}
},
{
"path": "/styles.fc4fa5e094d218207796.css",
"responseHeaders": {
"cache-control": "public,max-age=31536000,immutable"
}
},
{
"path": "/sw.js",
"responseHeaders": { "cache-control": "public,max-age=0,must-revalidate" }
},
{
"path": "/app-data.json",
"responseHeaders": { "cache-control": "public,max-age=0,must-revalidate" }
},
{
"path": "/index.html",
"responseHeaders": { "cache-control": "public,max-age=0,must-revalidate" }
},
{
"path": "/page-data/404/page-data.json",
"responseHeaders": { "cache-control": "public,max-age=0,must-revalidate" }
},
{
"path": "/path-that-does-not-exist",
"mustContain": "You just hit a route that doesn&#x27;t exist..."
},
{
"path": "/path-that-does-not-exist",
"status": 404
}
]
}

View File

@@ -95,6 +95,7 @@ async function testDeployment(
bodies[configName] = Buffer.from(JSON.stringify(nowJson));
delete bodies['probe.js'];
const { deploymentId, deploymentUrl } = await nowDeploy(bodies, randomness);
let nextBuildManifest;
for (const probe of nowJson.probes || []) {
console.log('testing', JSON.stringify(probe));
@@ -102,6 +103,42 @@ async function testDeployment(
await new Promise(resolve => setTimeout(resolve, probe.delay));
continue;
}
const nextScriptIndex = probe.path.indexOf('__NEXT_SCRIPT__(');
if (nextScriptIndex > -1) {
const scriptNameEnd = probe.path.lastIndexOf(')');
let scriptName = probe.path.substring(
nextScriptIndex + '__NEXT_SCRIPT__('.length,
scriptNameEnd
);
const scriptArgs = scriptName.split(',');
scriptName = scriptArgs.shift();
const manifestPrefix = scriptArgs.shift() || '';
if (!nextBuildManifest) {
const manifestUrl = `https://${deploymentUrl}${manifestPrefix}/_next/static/testing-build-id/_buildManifest.js`;
console.log('fetching buildManifest at', manifestUrl);
const { text: manifestContent } = await fetchDeploymentUrl(manifestUrl);
// we must eval it since we use devalue to stringify it
global.__BUILD_MANIFEST_CB = null;
nextBuildManifest = eval(
manifestContent
.replace('self.__BUILD_MANIFEST', 'manifest')
.replace(/self.__BUILD_MANIFEST_CB.*/, '')
);
}
const scriptRelativePath = nextBuildManifest[scriptName];
probe.path =
probe.path.substring(0, nextScriptIndex) +
scriptRelativePath +
probe.path.substr(scriptNameEnd + 1);
}
const probeUrl = `https://${deploymentUrl}${probe.path}`;
const fetchOpts = {
...probe.fetchOptions,

View File

@@ -2266,10 +2266,10 @@
resolved "https://registry.yarnpkg.com/@zeit/ncc/-/ncc-0.20.4.tgz#00f0a25a88cac3712af4ba66561d9e281c6f05c9"
integrity sha512-fmq+F/QxPec+k/zvT7HiVpk7oiGFseS6brfT/AYqmCUp6QFRK7vZf2Ref46MImsg/g2W3g5X6SRvGRmOAvEfdA==
"@zeit/node-file-trace@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@zeit/node-file-trace/-/node-file-trace-0.6.1.tgz#44d832c4bf71bc4d142cc5e2ef3e708673cdd2b4"
integrity sha512-rcaY2xxhjBzGJEtpdLFbS87HYx8bVbi/a+0MBVpMVvURrmjVAY58VL+Ynz4QPNs1C6o8e0FB1gVQFtN47ZnkMw==
"@zeit/node-file-trace@0.6.5":
version "0.6.5"
resolved "https://registry.yarnpkg.com/@zeit/node-file-trace/-/node-file-trace-0.6.5.tgz#ffd443e4648eb88591c53b1a871a47bff651b62e"
integrity sha512-PbxtiZBU+axKtR9dU2/iQgK9+aP/ip94SqI/FCMWppmFPGlxGKHf8UnJZskFuqLZeWWzL+L+8SeipsNHATO9nw==
dependencies:
acorn "^7.1.1"
acorn-class-fields "^0.3.2"