Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/issue 1142 AWS Adapter #1419

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ packages/**/test/**/netlify
packages/**/test/**/.netlify
packages/**/test/**/.vercel
public/
adapter-output/
adapter-output/
.aws-output/
53 changes: 53 additions & 0 deletions packages/plugin-adapter-aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# @greenwood/plugin-adapter-aws

## Overview

Enables usage of [AWS](https://aws.amazon.com/) hosting for API routes and SSR pages. For more information and complete docs on Greenwood, please visit [our website](https://www.greenwoodjs.dev).

> This package assumes you already have `@greenwood/cli` installed.

## Features

This plugin "adapts" SSR pages and API routes to be compatible with AWS Lambda and ready to use with IaC (Infrastructure as Code) tools like [SST](https://sst.dev/) and [Architect](https://arc.codes/).

> _**Note:** You can see a working example of this plugin [here](https://github.com/ProjectEvergreen/greenwood-demo-adapter-aws)_.


## Installation

You can use your favorite JavaScript package manager to install this package.

```bash
# npm
$ npm i -D @greenwood/plugin-adapter-aws

# yarn
$ yarn add @greenwood/plugin-adapter-aws --dev

# pnpm
$ pnpm add -D @greenwood/plugin-adapter-aws
```

## Usage

Add this plugin to your _greenwood.config.js_:

```javascript
import { greenwoodPluginAdapterAws } from '@greenwood/plugin-adapter-aws';

export default {
// ...

plugins: [
greenwoodPluginAdapterAws()
]
}
```

## Options

TODO

## Caveats

TODO
33 changes: 33 additions & 0 deletions packages/plugin-adapter-aws/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@greenwood/plugin-adapter-aws",
"version": "0.31.1",
"description": "A Greenwood plugin for supporting AWS serverless and edge runtimes.",
"repository": "https://github.com/ProjectEvergreen/greenwood",
"homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-adapter-aws",
"author": "Owen Buckley <owen@thegreenhouse.io>",
"license": "MIT",
"keywords": [
"Greenwood",
"Static Site Generator",
"Server Side Rendering",
"Full Stack Web Development",
"Serverless",
"Web Components",
"AWS",
"Edge"
],
"main": "src/index.js",
"type": "module",
"files": [
"src/"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@greenwood/cli": "^0.31.0"
},
"devDependencies": {
"@greenwood/cli": "^0.31.1"
}
}
134 changes: 134 additions & 0 deletions packages/plugin-adapter-aws/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import fs from "fs/promises";
import path from "path";
import { checkResourceExists } from "@greenwood/cli/src/lib/resource-utils.js";

// https://vercel.com/docs/functions/serverless-functions/runtimes/node-js#node.js-helpers
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woops!

function generateOutputFormat(id, type) {
const handlerAlias = "$handler";
const path = type === "page" ? `${id}.route` : id;

return `
import { handler as ${handlerAlias} } from './${path}.js';
export async function handler (event, context) {
const { body, headers = {}, rawPath = '', rawQueryString = '', routeKey = '' } = event;
const method = routeKey.split(' ')[0];
const queryParams = rawQueryString === '' ? '' : \`?\${rawQueryString}\`;
const contentType = headers['content-type'] || '';
let format = body;

if (['GET', 'HEAD'].includes(method.toUpperCase())) {
format = null
} else if (contentType.includes('application/x-www-form-urlencoded') && event.isBase64Encoded) {
const formData = new FormData();
const formParams = new URLSearchParams(atob(body));

formParams.forEach((value, key) => {
formData.append(key, value);
});

// when using FormData, let Request set the correct headers
// or else it will come out as multipart/form-data
// https://stackoverflow.com/a/43521052/417806
format = formData;
delete headers['content-type'];
} else if(contentType.includes('application/json')) {
format = JSON.stringify(body);
}

const req = new Request(new URL(\`\${rawPath}\${queryParams}\`, \`http://\${headers.host}\`), {
body: format,
headers: new Headers(headers),
method
});

// https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event
const res = await $handler(req);
return {
"body": await res.text(),
"statusCode": res.status,
"headers": Object.fromEntries(res.headers)
}
}
`;
}

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

async function awsAdapter(compilation) {
const { outputDir, projectDirectory } = compilation.context;
const { basePath } = compilation.config;
const adapterOutputUrl = new URL("./.aws-output/", projectDirectory);
const ssrPages = compilation.graph.filter((page) => page.isSSR);
const apiRoutes = compilation.manifest.apis;

if (!(await checkResourceExists(adapterOutputUrl))) {
await fs.mkdir(adapterOutputUrl, { recursive: true });
}

for (const page of ssrPages) {
const outputType = "page";
const { id, outputHref } = page;
const outputRoot = new URL(`./routes/${basePath}/${id}/`, adapterOutputUrl);
const chunks = (await fs.readdir(outputDir)).filter(
(file) => file.startsWith(`${id}.route.chunk`) && file.endsWith(".js"),
);

await setupFunctionBuildFolder(id, outputType, outputRoot);

// handle user's actual route entry file
await fs.cp(
new URL(outputHref),
new URL(`./${outputHref.replace(outputDir.href, "")}`, outputRoot),
{ recursive: true },
);

// and any (URL) chunks for the page
for (const chunk of chunks) {
await fs.cp(new URL(`./${chunk}`, outputDir), new URL(`./${chunk}`, outputRoot), {
recursive: true,
});
}
}

for (const [key, value] of apiRoutes.entries()) {
const outputType = "api";
const { id, outputHref } = apiRoutes.get(key);
const outputRoot = new URL(`.${basePath}/api/${id}/`, adapterOutputUrl);
const { assets = [] } = value;

await setupFunctionBuildFolder(id, outputType, outputRoot);

await fs.cp(new URL(outputHref), new URL(`./${id}.js`, outputRoot), { recursive: true });

for (const asset of assets) {
const name = path.basename(asset);

await fs.cp(new URL(asset), new URL(`./${name}`, outputRoot), { recursive: true });
}
}
}

const greenwoodPluginAdapterAws = () => [
{
type: "adapter",
name: "plugin-adapter-aws",
provider: (compilation) => {
return async () => {
await awsAdapter(compilation);
};
},
},
];

export { greenwoodPluginAdapterAws };
Loading
Loading