-
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 netlify adapter plugin (#1128)
* create initial working version of a netlify adapter plugin * add test case for greeting API route adapter * add test case for SSR page output * README clarifications * add fragments API and HTTP method support from Netlify event * file output setup and zipping refactoring * auto-generate _redirects file based on pages and APIs * redirects should be rewrites instead * document recommended Netlify project configuration setup * document caveats * link to demonstration repo * document adapter netlify on custom plugins page * add netlify-cli as a dependency * Netlify CLI integration and clarifications * disable linting due to typescript version conflicts * clarify redirects and rewrites in README * update pathname handling for windows interop * add test coverage for SSR pages content type * README refresh
- Loading branch information
1 parent
fdf2b14
commit 37c6a17
Showing
18 changed files
with
5,966 additions
and
77 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
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,85 @@ | ||
# @greenwood/plugin-adapter-netlify | ||
|
||
## Overview | ||
This plugin enables usage of the [Netlify](https://www.netlify.com/) platform for hosting a Greenwood application. | ||
|
||
> This package assumes you already have `@greenwood/cli` installed. | ||
## Features | ||
|
||
In addition to publishing a project's static assets to the Netlify 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 Netlify [Serverless functions](https://docs.netlify.com/functions/overview/) using their [custom build](https://docs.netlify.com/functions/deploy/?fn-language=js#custom-build-2) approach | ||
|
||
This plugin will automatically generate a custom [__redirects_](https://docs.netlify.com/routing/redirects/) file to correctly map your SSR page and API route URLs to the corresponding Netlify function endpoint (as a rewrite). You can continue to customize your Netlify project using your _netlify.toml_ file as needed. | ||
|
||
> _**Note:** You can see a working example of this plugin [here](https://github.com/ProjectEvergreen/greenwood-demo-adapter-netlify)_. | ||
|
||
## Installation | ||
You can use your favorite JavaScript package manager to install this package. | ||
|
||
_examples:_ | ||
```bash | ||
# npm | ||
npm install @greenwood/plugin-adapter-netlify --save-dev | ||
|
||
# yarn | ||
yarn add @greenwood/plugin-adapter-netlify --dev | ||
``` | ||
|
||
|
||
You will then want to create a _netlify.toml_ file at the root of your project (or configure it via the Netlify UI), updating each value as needed per your own project's setup. | ||
|
||
```toml | ||
[build] | ||
publish = "public/" | ||
command = "npm run build" # or yarn, pnpm, etc | ||
|
||
[build.processing] | ||
skip_processing = true | ||
|
||
[build.environment] | ||
NODE_VERSION = "18.x" # or pin to a specific version, like 18.15.0 | ||
``` | ||
|
||
## Usage | ||
Add this plugin to your _greenwood.config.js_. | ||
|
||
```javascript | ||
import { greenwoodPluginAdapterNetlify } from '@greenwood/plugin-adapter-netlify'; | ||
|
||
export default { | ||
... | ||
|
||
plugins: [ | ||
greenwoodPluginAdapterNetlify() | ||
] | ||
} | ||
``` | ||
|
||
Optionally, your API routes will have access to Netlify's `context` object as the second parameter to the `handler` function. For example: | ||
```js | ||
export async function handler(request, context = {}) { | ||
console.log({ request, context }); | ||
} | ||
``` | ||
|
||
> _Please see caveats section for more information on this feature. 👇_ | ||
## Netlify CLI / Local Development | ||
|
||
This plugin comes with the Netlify CLI as a dependency to support some local development testing for previewing a Netlify build locally. Simply add a script like this to your _package.json_ | ||
```json | ||
{ | ||
"serve:netlify": "greenwood build && netlify dev" | ||
} | ||
``` | ||
|
||
Then when you run it, you will be able to run and test a production build of your site locally. | ||
|
||
> _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. 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). |
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,37 @@ | ||
{ | ||
"name": "@greenwood/plugin-adapter-netlify", | ||
"version": "0.29.0-alpha.1", | ||
"description": "A Greenwood plugin for supporting Netlify serverless and edge runtimes.", | ||
"repository": "https://github.com/ProjectEvergreen/greenwood", | ||
"homepage": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-adapter-netlify", | ||
"author": "Owen Buckley <[email protected]>", | ||
"license": "MIT", | ||
"keywords": [ | ||
"Greenwood", | ||
"Static Site Generator", | ||
"SSR", | ||
"Full Stack Web Development", | ||
"Netlify", | ||
"Serverless", | ||
"Edge" | ||
], | ||
"main": "src/index.js", | ||
"type": "module", | ||
"files": [ | ||
"src/" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"peerDependencies": { | ||
"@greenwood/cli": "^0.28.0" | ||
}, | ||
"dependencies": { | ||
"zip-a-folder": "^2.0.0", | ||
"netlify-cli": "^15.10.0" | ||
}, | ||
"devDependencies": { | ||
"@greenwood/cli": "^0.29.0-alpha.1", | ||
"extract-zip": "^2.0.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,170 @@ | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import { checkResourceExists, normalizePathnameForWindows } from '@greenwood/cli/src/lib/resource-utils.js'; | ||
import { zip } from 'zip-a-folder'; | ||
|
||
// https://docs.netlify.com/functions/create/?fn-language=js | ||
function generateOutputFormat(id) { | ||
return ` | ||
import { handler as ${id} } from './__${id}.js'; | ||
export async function handler (event, context = {}) { | ||
const { rawUrl, headers, httpMethod } = event; | ||
const request = new Request(rawUrl, { | ||
method: httpMethod, | ||
headers: new Headers(headers) | ||
}); | ||
const response = await ${id}(request, context); | ||
return { | ||
statusCode: response.status, | ||
body: await response.text(), | ||
headers: response.headers || new Headers() | ||
}; | ||
} | ||
`; | ||
} | ||
|
||
async function setupOutputDirectory(id, outputRoot, outputType) { | ||
const outputFormat = generateOutputFormat(id, outputType); | ||
const filename = outputType === 'api' | ||
? `api-${id}` | ||
: `${id}`; | ||
|
||
await fs.mkdir(outputRoot, { recursive: true }); | ||
await fs.writeFile(new URL(`./${filename}.js`, outputRoot), outputFormat); | ||
await fs.writeFile(new URL('./package.json', outputRoot), JSON.stringify({ | ||
type: 'module' | ||
})); | ||
} | ||
|
||
// TODO manifest options, like node version? | ||
// https://github.com/netlify/zip-it-and-ship-it#options | ||
async function createOutputZip(id, outputType, outputRootUrl, projectDirectory) { | ||
const filename = outputType === 'api' | ||
? `api-${id}` | ||
: `${id}`; | ||
|
||
await zip( | ||
normalizePathnameForWindows(outputRootUrl), | ||
normalizePathnameForWindows(new URL(`./netlify/functions/${filename}.zip`, projectDirectory)) | ||
); | ||
} | ||
|
||
async function netlifyAdapter(compilation) { | ||
const { outputDir, projectDirectory, scratchDir } = compilation.context; | ||
const adapterOutputUrl = new URL('./netlify/functions/', scratchDir); | ||
const ssrPages = compilation.graph.filter(page => page.isSSR); | ||
const apiRoutes = compilation.manifest.apis; | ||
// https://docs.netlify.com/routing/redirects/ | ||
// https://docs.netlify.com/routing/redirects/rewrites-proxies/ | ||
// When you assign an HTTP status code of 200 to a redirect rule, it becomes a rewrite. | ||
let redirects = ''; | ||
|
||
if (!await checkResourceExists(adapterOutputUrl)) { | ||
await fs.mkdir(adapterOutputUrl, { recursive: true }); | ||
} | ||
|
||
const files = await fs.readdir(outputDir); | ||
const isExecuteRouteModule = files.find(file => file.startsWith('execute-route-module')); | ||
|
||
await fs.mkdir(new URL('./netlify/functions/', projectDirectory), { recursive: true }); | ||
|
||
for (const page of ssrPages) { | ||
const { id } = page; | ||
const outputType = 'page'; | ||
const outputRoot = new URL(`./${id}/`, adapterOutputUrl); | ||
|
||
await setupOutputDirectory(id, outputRoot, outputType); | ||
|
||
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 } | ||
); | ||
} | ||
|
||
await createOutputZip(id, outputType, new URL(`./${id}/`, adapterOutputUrl), projectDirectory); | ||
|
||
redirects += `/${id}/ /.netlify/functions/${id} 200 | ||
`; | ||
} | ||
|
||
if (apiRoutes.size > 0) { | ||
redirects += '/api/* /.netlify/functions/api-:splat 200'; | ||
} | ||
|
||
for (const [key] of apiRoutes) { | ||
const outputType = 'api'; | ||
const id = key.replace('/api/', ''); | ||
const outputRoot = new URL(`./api/${id}/`, adapterOutputUrl); | ||
|
||
await setupOutputDirectory(id, outputRoot, outputType); | ||
|
||
// 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 } | ||
); | ||
|
||
if (await checkResourceExists(new URL('./api/assets/', outputDir))) { | ||
await fs.cp( | ||
new URL('./api/assets/', outputDir), | ||
new URL('./assets/', outputRoot), | ||
{ recursive: true } | ||
); | ||
} | ||
|
||
// NOTE: All functions must live at the top level | ||
// https://github.com/netlify/netlify-lambda/issues/90#issuecomment-486047201 | ||
await createOutputZip(id, outputType, outputRoot, projectDirectory); | ||
} | ||
|
||
if (redirects !== '') { | ||
await fs.writeFile(new URL('./_redirects', outputDir), redirects); | ||
} | ||
} | ||
|
||
const greenwoodPluginAdapterNetlify = (options = {}) => [{ | ||
type: 'adapter', | ||
name: 'plugin-adapter-netlify', | ||
provider: (compilation) => { | ||
return async () => { | ||
await netlifyAdapter(compilation, options); | ||
}; | ||
} | ||
}]; | ||
|
||
export { greenwoodPluginAdapterNetlify }; |
Oops, something went wrong.