Skip to content

Commit

Permalink
Netlify Edge function support (withastro#3148)
Browse files Browse the repository at this point in the history
* Netlify Edge function support

* Update readme with edge function information

* Adds a changeset

* Disable running edge function test in CI for now
  • Loading branch information
matthewp committed Apr 19, 2022
1 parent c35e94f commit 4cf54c6
Show file tree
Hide file tree
Showing 23 changed files with 333 additions and 93 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-mayflies-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/netlify': minor
---

Adds support for Netlify Edge Functions
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ Now you can deploy!
netlify deploy
```

## Edge Functions

Netlify has two serverless platforms, Netlify Functions and Netlify Edge Functions. With Edge Functions your code is distributed closer to your users, lowering latency. You can use Edge Functions by changing the import in your astro configuration file:

```diff
import { defineConfig } from 'astro/config';
- import netlify from '@astrojs/netlify/functions';
+ import netlify from '@astrojs/netlify/edge-functions';

export default defineConfig({
adapter: netlify(),
});
```

## Configuration

### dist
Expand Down
6 changes: 5 additions & 1 deletion packages/integrations/netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
".": "./dist/index.js",
"./functions": "./dist/integration-functions.js",
"./netlify-functions.js": "./dist/netlify-functions.js",
"./edge-functions": "./dist/integration-edge-functions.js",
"./netlify-edge-functions.js": "./dist/netlify-edge-functions.js",
"./package.json": "./package.json"
},
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000"
"test-fn": "mocha --exit --timeout 20000 test/functions/",
"test-edge": "deno test --allow-run --allow-read --allow-net ./test/edge-functions/",
"test": "npm run test-fn"
},
"dependencies": {
"@astrojs/webapi": "^0.11.1"
Expand Down
4 changes: 4 additions & 0 deletions packages/integrations/netlify/src/edge-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(globalThis as any).process = {
argv: [],
env: {},
};
73 changes: 8 additions & 65 deletions packages/integrations/netlify/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,8 @@
import type { AstroAdapter, AstroIntegration, AstroConfig } from 'astro';
import fs from 'fs';

export function getAdapter(): AstroAdapter {
return {
name: '@astrojs/netlify',
serverEntrypoint: '@astrojs/netlify/netlify-functions.js',
exports: ['handler'],
args: {},
};
}

interface NetlifyFunctionsOptions {
dist?: URL;
}

function netlifyFunctions({ dist }: NetlifyFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
return {
name: '@astrojs/netlify',
hooks: {
'astro:config:setup': ({ config }) => {
if (dist) {
config.outDir = dist;
} else {
config.outDir = new URL('./netlify/', config.root);
}
},
'astro:config:done': ({ config, setAdapter }) => {
setAdapter(getAdapter());
_config = config;
},
'astro:build:start': async ({ buildConfig }) => {
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
buildConfig.client = _config.outDir;
buildConfig.server = new URL('./functions/', _config.outDir);
},
'astro:build:done': async ({ routes, dir }) => {
const _redirectsURL = new URL('./_redirects', dir);

// Create the redirects file that is used for routing.
let _redirects = '';
for (const route of routes) {
if (route.pathname) {
_redirects += `
${route.pathname} /.netlify/functions/${entryFile} 200`;
} else {
const pattern =
'/' + route.segments.map(([part]) => (part.dynamic ? '*' : part.content)).join('/');
_redirects += `
${pattern} /.netlify/functions/${entryFile} 200`;
}
}

// Always use appendFile() because the redirects file could already exist,
// e.g. due to a `/public/_redirects` file that got copied to the output dir.
// If the file does not exist yet, appendFile() automatically creates it.
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
},
},
};
}

export { netlifyFunctions, netlifyFunctions as default };
export {
netlifyFunctions,
netlifyFunctions as default
} from './integration-functions.js';

export {
netlifyEdgeFunctions
} from './integration-edge-functions.js';
98 changes: 98 additions & 0 deletions packages/integrations/netlify/src/integration-edge-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { AstroAdapter, AstroIntegration, AstroConfig, RouteData } from 'astro';
import * as fs from 'fs';

export function getAdapter(): AstroAdapter {
return {
name: '@astrojs/netlify/edge-functions',
serverEntrypoint: '@astrojs/netlify/netlify-edge-functions.js',
exports: ['default'],
};
}

interface NetlifyEdgeFunctionsOptions {
dist?: URL;
}

interface NetlifyEdgeFunctionManifestFunctionPath {
function: string;
path: string;
}

interface NetlifyEdgeFunctionManifestFunctionPattern {
function: string;
pattern: string;
}

type NetlifyEdgeFunctionManifestFunction = NetlifyEdgeFunctionManifestFunctionPath | NetlifyEdgeFunctionManifestFunctionPattern;

interface NetlifyEdgeFunctionManifest {
functions: NetlifyEdgeFunctionManifestFunction[];
version: 1;
}

async function createEdgeManifest(routes: RouteData[], entryFile: string, dir: URL) {
const functions: NetlifyEdgeFunctionManifestFunction[] = [];
for(const route of routes) {
if(route.pathname) {
functions.push({
function: entryFile,
path: route.pathname
});
} else {
functions.push({
function: entryFile,
pattern: route.pattern.source
});
}
}

const manifest: NetlifyEdgeFunctionManifest = {
functions,
version: 1
};

const manifestURL = new URL('./manifest.json', dir);
const _manifest = JSON.stringify(manifest, null, ' ');
await fs.promises.writeFile(manifestURL, _manifest, 'utf-8');
}

export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
return {
name: '@astrojs/netlify/edge-functions',
hooks: {
'astro:config:setup': ({ config }) => {
if (dist) {
config.outDir = dist;
} else {
config.outDir = new URL('./netlify/', config.root);
}
},
'astro:config:done': ({ config, setAdapter }) => {
setAdapter(getAdapter());
_config = config;
},
'astro:build:start': async ({ buildConfig }) => {
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
buildConfig.client = _config.outDir;
buildConfig.server = new URL('./edge-functions/', _config.outDir);
},
'astro:build:setup': ({ vite, target }) => {
if (target === 'server') {
vite.ssr = {
noExternal: true,
};
}
},
'astro:build:done': async ({ routes, dir }) => {

await createEdgeManifest(routes, entryFile, new URL('./edge-functions/', dir));
},
},
};
}

export {
netlifyEdgeFunctions as default
}
66 changes: 65 additions & 1 deletion packages/integrations/netlify/src/integration-functions.ts
Original file line number Diff line number Diff line change
@@ -1 +1,65 @@
export { netlifyFunctions as default } from './index.js';
import type { AstroAdapter, AstroIntegration, AstroConfig } from 'astro';
import fs from 'fs';

export function getAdapter(): AstroAdapter {
return {
name: '@astrojs/netlify/functions',
serverEntrypoint: '@astrojs/netlify/netlify-functions.js',
exports: ['handler'],
args: {},
};
}

interface NetlifyFunctionsOptions {
dist?: URL;
}

function netlifyFunctions({ dist }: NetlifyFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
return {
name: '@astrojs/netlify',
hooks: {
'astro:config:setup': ({ config }) => {
if (dist) {
config.outDir = dist;
} else {
config.outDir = new URL('./netlify/', config.root);
}
},
'astro:config:done': ({ config, setAdapter }) => {
setAdapter(getAdapter());
_config = config;
},
'astro:build:start': async ({ buildConfig }) => {
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
buildConfig.client = _config.outDir;
buildConfig.server = new URL('./functions/', _config.outDir);
},
'astro:build:done': async ({ routes, dir }) => {
const _redirectsURL = new URL('./_redirects', dir);

// Create the redirects file that is used for routing.
let _redirects = '';
for (const route of routes) {
if (route.pathname) {
_redirects += `
${route.pathname} /.netlify/functions/${entryFile} 200`;
} else {
const pattern =
'/' + route.segments.map(([part]) => (part.dynamic ? '*' : part.content)).join('/');
_redirects += `
${pattern} /.netlify/functions/${entryFile} 200`;
}
}

// Always use appendFile() because the redirects file could already exist,
// e.g. due to a `/public/_redirects` file that got copied to the output dir.
// If the file does not exist yet, appendFile() automatically creates it.
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
},
},
};
}

export { netlifyFunctions, netlifyFunctions as default };
20 changes: 20 additions & 0 deletions packages/integrations/netlify/src/netlify-edge-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import './edge-shim.js';
import { SSRManifest } from 'astro';
import { App } from 'astro/app';

export function createExports(manifest: SSRManifest) {
const app = new App(manifest);

const handler = async (request: Request): Promise<Response> => {
if(app.match(request)) {
return app.render(request);
}

return new Response(null, {
status: 404,
statusText: 'Not found'
});
};

return { 'default': handler };
}
3 changes: 3 additions & 0 deletions packages/integrations/netlify/test/edge-functions/deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @ts-nocheck
export { fromFileUrl } from 'https://deno.land/[email protected]/path/mod.ts';
export { assertEquals, assert } from 'https://deno.land/[email protected]/testing/asserts.ts';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @ts-ignore
import { runBuild } from './test-utils.ts';
// @ts-ignore
import { assertEquals, assert } from './deps.ts';

// @ts-ignore
Deno.test({
name: 'Edge Basics',
async fn() {
let close = await runBuild('./fixtures/edge-basic/');
const { default: handler } = await import('./fixtures/edge-basic/dist/edge-functions/entry.mjs');
const response = await handler(new Request('https://example.com/'));
assertEquals(response.status, 200);
const html = await response.text();
assert(html, 'got some html');
await close();
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'astro/config';
import { netlifyEdgeFunctions } from '@astrojs/netlify';

export default defineConfig({
adapter: netlifyEdgeFunctions({
dist: new URL('./dist/', import.meta.url),
}),
experimental: {
ssr: true
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/netlify-edge-astro-basic",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/netlify": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<head><title>Testing</title></head>
<body>
<h1>Test page</h1>
<h2>Links</h2>
<ul>
<li><a href="/two/">Two</a></li>
</ul>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<head><title>Page Two</title></head>
<body>
<h1>Page two</h1>
</body>
</html>
13 changes: 13 additions & 0 deletions packages/integrations/netlify/test/edge-functions/test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-ignore
import { fromFileUrl } from './deps.ts';
const dir = new URL('./', import.meta.url);

export async function runBuild(fixturePath: string) {
// @ts-ignore
let proc = Deno.run({
cmd: ['node', '../../../../../../astro/astro.js', 'build', '--silent'],
cwd: fromFileUrl(new URL(fixturePath, dir)),
});
await proc.status();
return async () => await proc.close();
}
Loading

0 comments on commit 4cf54c6

Please sign in to comment.