Skip to content

Commit

Permalink
Feature/issue 1008 vercel adapter plugin (#1139)
Browse files Browse the repository at this point in the history
* 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
thescientist13 authored Aug 12, 2023
1 parent 17debc9 commit a00a574
Show file tree
Hide file tree
Showing 15 changed files with 660 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ packages/**/test/**/yarn.lock
packages/**/test/**/package-lock.json
packages/**/test/**/netlify
packages/**/test/**/.netlify
packages/**/test/**/.vercel
public/
adapter-outlet/
2 changes: 1 addition & 1 deletion packages/plugin-adapter-netlify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Then when you run it, you will be able to run and test a production build of you
> _Please see caveats section for more information on this feature. 👇_
## Caveats
1. [Edge runtime](https://docs.netlify.com/edge-functions/overview/) is not supported yet.
1. [Edge runtime](https://docs.netlify.com/edge-functions/overview/) is not supported ([yet](https://github.com/ProjectEvergreen/greenwood/issues/1141)).
1. Netlify CLI / Local Dev
- [`context` object](https://docs.netlify.com/functions/create/?fn-language=js#code-your-function-2) not supported when running `greenwood develop` command
- [`import.meta.url` is not supported in the Netlify CLI](https://github.com/netlify/cli/issues/4601) and in particular causes [WCC to break](https://github.com/ProjectEvergreen/greenwood-demo-adapter-netlify#-importmetaurl).
64 changes: 64 additions & 0 deletions packages/plugin-adapter-vercel/README.md
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.
```
32 changes: 32 additions & 0 deletions packages/plugin-adapter-vercel/package.json
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 <[email protected]>",
"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"
}
}
150 changes: 150 additions & 0 deletions packages/plugin-adapter-vercel/src/index.js
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 };
Loading

0 comments on commit a00a574

Please sign in to comment.