-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/issue 1008 vercel adapter plugin (#1139)
* initial implementation of a vercel adapter plugin * add test case for static build output * handle request and response properties * finishing touches to README.md * add to custom plugins docs page * link to edge runtime support issue in caveats sections * handle windows pathname interop
- Loading branch information
1 parent
37c6a17
commit 69a61ca
Showing
15 changed files
with
660 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# @greenwood/plugin-adapter-vercel | ||
|
||
## Overview | ||
Enables usage of Vercel Serverless runtimes for API routes and SSR pages. | ||
|
||
> This package assumes you already have `@greenwood/cli` installed. | ||
## Features | ||
|
||
In addition to publishing a project's static assets to the Vercel's CDN, this plugin adapts Greenwood [API routes](https://www.greenwoodjs.io/docs/api-routes/) and [SSR pages](https://www.greenwoodjs.io/docs/server-rendering/) into Vercel [Serverless functions](https://vercel.com/docs/concepts/functions/serverless-functions) using their [Build Output API](https://vercel.com/docs/build-output-api/v3). | ||
|
||
> _**Note:** You can see a working example of this plugin [here](https://github.com/ProjectEvergreen/greenwood-demo-adapter-vercel)_. | ||
|
||
## Installation | ||
You can use your favorite JavaScript package manager to install this package. | ||
|
||
_examples:_ | ||
```bash | ||
# npm | ||
npm install @greenwood/plugin-adapter-vercel --save-dev | ||
|
||
# yarn | ||
yarn add @greenwood/plugin-adapter-vercel --dev | ||
``` | ||
|
||
You will then want to create a _vercel.json_ file, customized to match your project. Assuming you have an npm script called `build` | ||
```json | ||
{ | ||
"scripts": { | ||
"build": "greenwood build" | ||
} | ||
} | ||
``` | ||
|
||
This would be the minimum _vercel.json_ configuration you would need | ||
```json | ||
{ | ||
"buildCommand": "npm run build" | ||
} | ||
``` | ||
|
||
## Usage | ||
Add this plugin to your _greenwood.config.js_. | ||
|
||
```javascript | ||
import { greenwoodPluginAdapterVercel } from '@greenwood/plugin-adapter-vercel'; | ||
|
||
export default { | ||
... | ||
|
||
plugins: [ | ||
greenwoodPluginAdapterVercel() | ||
] | ||
} | ||
``` | ||
|
||
|
||
## Caveats | ||
1. [Edge runtime](https://vercel.com/docs/concepts/functions/edge-functions) is not supported ([yet](https://github.com/ProjectEvergreen/greenwood/issues/1141)). | ||
1. The Vercel CLI (`vercel dev`) is not compatible with Build Output v3. | ||
```sh | ||
Error: Detected Build Output v3 from "npm run build", but it is not supported for `vercel dev`. Please set the Development Command in your Project Settings. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@greenwood/plugin-adapter-vercel", | ||
"version": "0.29.0-alpha.1", | ||
"description": "A Greenwood plugin for supporting Vercel serverless and edge runtimes.", | ||
"repository": "https://github.com/ProjectEvergreen/greenwood", | ||
"homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-adapter-vercel", | ||
"author": "Owen Buckley <owen@thegreenhouse.io>", | ||
"license": "MIT", | ||
"keywords": [ | ||
"Greenwood", | ||
"Static Site Generator", | ||
"SSR", | ||
"Full Stack Web Development", | ||
"Vercel", | ||
"Serverless", | ||
"Edge" | ||
], | ||
"main": "src/index.js", | ||
"type": "module", | ||
"files": [ | ||
"src/" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"peerDependencies": { | ||
"@greenwood/cli": "^0.28.0" | ||
}, | ||
"devDependencies": { | ||
"@greenwood/cli": "^0.29.0-alpha.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js'; | ||
|
||
function generateOutputFormat(id, type) { | ||
const path = type === 'page' | ||
? `__${id}` | ||
: id; | ||
|
||
return ` | ||
import { handler as ${id} } from './${path}.js'; | ||
export default async function handler (request, response) { | ||
const { url, headers, method } = request; | ||
const req = new Request(new URL(url, \`http://\${headers.host}\`), { | ||
headers: new Headers(headers), | ||
method | ||
}); | ||
const res = await ${id}(req); | ||
res.headers.forEach((value, key) => { | ||
response.setHeader(key, value); | ||
}); | ||
response.status(res.status); | ||
response.send(await res.text()); | ||
} | ||
`; | ||
} | ||
|
||
async function setupFunctionBuildFolder(id, outputType, outputRoot) { | ||
const outputFormat = generateOutputFormat(id, outputType); | ||
|
||
await fs.mkdir(outputRoot, { recursive: true }); | ||
await fs.writeFile(new URL('./index.js', outputRoot), outputFormat); | ||
await fs.writeFile(new URL('./package.json', outputRoot), JSON.stringify({ | ||
type: 'module' | ||
})); | ||
await fs.writeFile(new URL('./.vc-config.json', outputRoot), JSON.stringify({ | ||
runtime: 'nodejs18.x', | ||
handler: 'index.js', | ||
launcherType: 'Nodejs', | ||
shouldAddHelpers: true | ||
})); | ||
} | ||
|
||
async function vercelAdapter(compilation) { | ||
const { outputDir, projectDirectory } = compilation.context; | ||
const adapterOutputUrl = new URL('./.vercel/output/functions/', projectDirectory); | ||
const ssrPages = compilation.graph.filter(page => page.isSSR); | ||
const apiRoutes = compilation.manifest.apis; | ||
|
||
if (!await checkResourceExists(adapterOutputUrl)) { | ||
await fs.mkdir(adapterOutputUrl, { recursive: true }); | ||
} | ||
|
||
await fs.writeFile(new URL('./.vercel/output/config.json', projectDirectory), JSON.stringify({ | ||
'version': 3 | ||
})); | ||
|
||
const files = await fs.readdir(outputDir); | ||
const isExecuteRouteModule = files.find(file => file.startsWith('execute-route-module')); | ||
|
||
for (const page of ssrPages) { | ||
const outputType = 'page'; | ||
const { id } = page; | ||
const outputRoot = new URL(`./${id}.func/`, adapterOutputUrl); | ||
|
||
await setupFunctionBuildFolder(id, outputType, outputRoot); | ||
|
||
await fs.cp( | ||
new URL(`./_${id}.js`, outputDir), | ||
new URL(`./_${id}.js`, outputRoot), | ||
{ recursive: true } | ||
); | ||
|
||
await fs.cp( | ||
new URL(`./__${id}.js`, outputDir), | ||
new URL(`./__${id}.js`, outputRoot), | ||
{ recursive: true } | ||
); | ||
|
||
// TODO quick hack to make serverless pages are fully self-contained | ||
// for example, execute-route-module.js will only get code split if there are more than one SSR pages | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1118 | ||
if (isExecuteRouteModule) { | ||
await fs.cp( | ||
new URL(`./${isExecuteRouteModule}`, outputDir), | ||
new URL(`./${isExecuteRouteModule}`, outputRoot) | ||
); | ||
} | ||
|
||
// TODO how to track SSR resources that get dumped out in the public directory? | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1118 | ||
const ssrPageAssets = (await fs.readdir(outputDir)) | ||
.filter(file => !path.basename(file).startsWith('_') | ||
&& !path.basename(file).startsWith('execute') | ||
&& path.basename(file).endsWith('.js') | ||
); | ||
|
||
for (const asset of ssrPageAssets) { | ||
await fs.cp( | ||
new URL(`./${asset}`, outputDir), | ||
new URL(`./${asset}`, outputRoot), | ||
{ recursive: true } | ||
); | ||
} | ||
} | ||
|
||
for (const [key] of apiRoutes) { | ||
const outputType = 'api'; | ||
const id = key.replace('/api/', ''); | ||
const outputRoot = new URL(`./api/${id}.func/`, adapterOutputUrl); | ||
|
||
await setupFunctionBuildFolder(id, outputType, outputRoot); | ||
|
||
// TODO ideally all functions would be self contained | ||
// https://github.com/ProjectEvergreen/greenwood/issues/1118 | ||
await fs.cp( | ||
new URL(`./api/${id}.js`, outputDir), | ||
new URL(`./${id}.js`, outputRoot), | ||
{ recursive: true } | ||
); | ||
await fs.cp( | ||
new URL('./api/assets/', outputDir), | ||
new URL('./assets/', outputRoot), | ||
{ recursive: true } | ||
); | ||
} | ||
|
||
// static assets / build | ||
await fs.cp( | ||
outputDir, | ||
new URL('./.vercel/output/static/', projectDirectory), | ||
{ | ||
recursive: true | ||
} | ||
); | ||
} | ||
|
||
const greenwoodPluginAdapterVercel = (options = {}) => [{ | ||
type: 'adapter', | ||
name: 'plugin-adapter-vercel', | ||
provider: (compilation) => { | ||
return async () => { | ||
await vercelAdapter(compilation, options); | ||
}; | ||
} | ||
}]; | ||
|
||
export { greenwoodPluginAdapterVercel }; |
Oops, something went wrong.