From bb705cd091f856ec0937bd55f5808a5870d6a4e1 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 22 Jun 2020 13:41:10 -0700 Subject: [PATCH] [docs] Refactor "Developing a Runtime" docs and add `startDevServer()` (#4675) Co-authored-by: Steven --- DEVELOPING_A_RUNTIME.md | 355 +++++++++++++++++++++++----------------- 1 file changed, 203 insertions(+), 152 deletions(-) diff --git a/DEVELOPING_A_RUNTIME.md b/DEVELOPING_A_RUNTIME.md index 6c2547fde267..9dfa493ff826 100644 --- a/DEVELOPING_A_RUNTIME.md +++ b/DEVELOPING_A_RUNTIME.md @@ -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 implements the following interface: + +```typescript +interface Runtime { + version: number; + build: (options: BuildOptions) => Promise; + analyze?: (options: AnalyzeOptions) => Promise; + prepareCache?: (options: PrepareCacheOptions) => Promise; + shouldServe?: (options: ShouldServeOptions) => Promise; + startDevServer?: ( + options: StartDevServerOptions + ) => Promise; +} +``` + +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. -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. +> **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' -} -``` - -### `build` - -A **required** exported function that returns a [Serverless Function](#serverless-function). - -What's a Serverless Function? Read about [Serverless Functions](https://vercel.com/docs/v2/serverless-functions/introduction) to learn more. - -```js -build({ - files: Files, - entrypoint: String, - workPath: String, - config: Object, - meta?: { - isDev?: Boolean, - requestPath?: String, - filesChanged?: Array, - filesRemoved?: Array - } -}) : { - watch?: Array, - output: Lambda, - routes?: Object -} -``` +> What's a Serverless Function? Read about [Serverless Functions](https://vercel.com/docs/v2/serverless-functions/introduction) to learn more. -If you are using TypeScript, you should use the following types: +**Example:** -```ts -import { BuildOptions } from '@vercel/build-utils' +```typescript +import { BuildOptions, createLambda } from '@vercel/build-utils'; -export build(options: BuildOptions) { - // Build the code here +export async function build(options: BuildOptions) { + // Build the code here… + const lambda = createLambda(/* … */); return { - output: { - 'path-to-file': File, - 'path-to-lambda': Lambda - }, - watch: [], - routes: {} - } + 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… + ], + }; } ``` -### `prepareCache` +### `analyze()` -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 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. -```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 { AnalyzeOptions } from '@vercel/build-utils'; -```ts -import { PrepareCacheOptions } from '@vercel/build-utils' +export async function analyze(options: AnalyzeOptions) { + // Do calculations to generate a fingerprint based off the source code here… -export prepareCache(options: PrepareCacheOptions) { - return { 'path-to-file': File } + return 'fingerprint goes here'; } ``` -### `shouldServe` +### `prepareCache()` -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. +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. -```js -shouldServe({ - entrypoint: String, - files: Files, - config: Object, - requestPath: String, - workPath: String -}) : Boolean -``` +**Example:** -If you are using TypeScript, you can import the types for each of these functions by using the following: +```typescript +import { PrepareCacheOptions } from '@vercel/build-utils'; -```ts -import { ShouldServeOptions } from '@vercel/build-utils' +export async function prepareCache(options: PrepareCacheOptions) { + // Create a mapping of file names and `File` object instances to cache here… -export shouldServe(options: ShouldServeOptions) { - return Boolean + return { + 'path-to-file': File, + }; } ``` -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). +### `shouldServe()` -### Runtime Options +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. -The exported functions [`analyze`](#analyze), [`build`](#build), and [`prepareCache`](#preparecache) receive one argument with the following properties. +**Example:** -**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`. +```typescript +import { ShouldServeOptions } from '@vercel/build-utils'; -## Examples +export async function shouldServe(options: ShouldServeOptions) { + // Determine whether or not the Runtime should respond to the request path here… -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. + return options.requestPath === options.entrypoint; +} +``` -## Technical Details +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). + +### `startDevServer()` + +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. + +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. + +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. + +> **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 }; +} +``` ### 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'; ```