Netlify Edge function support (#3148)

* Netlify Edge function support

* Update readme with edge function information

* Adds a changeset

* Disable running edge function test in CI for now
This commit is contained in:
Matthew Phillips 2022-04-19 11:22:15 -04:00 committed by GitHub
parent c35e94f544
commit 4cf54c60aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 332 additions and 92 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/netlify': minor
---
Adds support for Netlify Edge Functions

View file

@ -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

View file

@ -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"

View file

@ -0,0 +1,4 @@
(globalThis as any).process = {
argv: [],
env: {},
};

View file

@ -1,65 +1,8 @@
import type { AstroAdapter, AstroIntegration, AstroConfig } from 'astro';
import fs from 'fs';
export {
netlifyFunctions,
netlifyFunctions as default
} from './integration-functions.js';
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 {
netlifyEdgeFunctions
} from './integration-edge-functions.js';

View file

@ -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
}

View file

@ -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 };

View file

@ -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 };
}

View file

@ -0,0 +1,3 @@
// @ts-nocheck
export { fromFileUrl } from 'https://deno.land/std@0.110.0/path/mod.ts';
export { assertEquals, assert } from 'https://deno.land/std@0.132.0/testing/asserts.ts';

View file

@ -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('http://example.com/'));
assertEquals(response.status, 200);
const html = await response.text();
assert(html, 'got some html');
await close();
},
});

View file

@ -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
}
})

View file

@ -0,0 +1,9 @@
{
"name": "@test/netlify-edge-astro-basic",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/netlify": "workspace:*"
}
}

View file

@ -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>

View file

@ -0,0 +1,6 @@
<html>
<head><title>Page Two</title></head>
<body>
<h1>Page two</h1>
</body>
</html>

View file

@ -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();
}

View file

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../../astro/test/test-utils.js';
import netlifyAdapter from '../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
import netlifyAdapter from '../../dist/index.js';
import { fileURLToPath } from 'url';
describe('Cookies', () => {
@ -18,15 +18,7 @@ describe('Cookies', () => {
dist: new URL('./fixtures/cookies/dist/', import.meta.url),
}),
site: `http://example.com`,
vite: {
resolve: {
alias: {
'@astrojs/netlify/netlify-functions.js': fileURLToPath(
new URL('../dist/netlify-functions.js', import.meta.url)
),
},
},
},
integrations: [ testIntegration() ]
});
await fixture.build();
});

View file

@ -1,12 +1,9 @@
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../../astro/test/test-utils.js';
import netlifyAdapter from '../dist/index.js';
import { fileURLToPath } from 'url';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
// Asset bundling
describe('Dynamic pages', () => {
/** @type {import('../../../astro/test/test-utils').Fixture} */
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
@ -19,15 +16,7 @@ describe('Dynamic pages', () => {
dist: new URL('./fixtures/dynamic-route/dist/', import.meta.url),
}),
site: `http://example.com`,
vite: {
resolve: {
alias: {
'@astrojs/netlify/netlify-functions.js': fileURLToPath(
new URL('../dist/netlify-functions.js', import.meta.url)
),
},
},
},
integrations: [ testIntegration() ]
});
await fixture.build();
});

View file

@ -0,0 +1,29 @@
// @ts-check
import { fileURLToPath } from 'url';
export * from '../../../../astro/test/test-utils.js';
/**
*
* @returns {import('../../../../astro/dist/types/@types/astro').AstroIntegration}
*/
export function testIntegration() {
return {
name: '@astrojs/netlify/test-integration',
hooks: {
'astro:config:setup':({ updateConfig }) => {
updateConfig({
vite: {
resolve: {
alias: {
'@astrojs/netlify/netlify-functions.js': fileURLToPath(
new URL('../../dist/netlify-functions.js', import.meta.url)
),
},
},
},
});
}
}
};
}

View file

@ -1271,6 +1271,14 @@ importers:
astro: link:../../astro
astro-scripts: link:../../../scripts
packages/integrations/netlify/test/edge-functions/fixtures/edge-basic:
specifiers:
'@astrojs/netlify': workspace:*
astro: workspace:*
dependencies:
'@astrojs/netlify': link:../../../..
astro: link:../../../../../../astro
packages/integrations/node:
specifiers:
'@astrojs/webapi': ^0.11.1