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:
parent
c35e94f544
commit
4cf54c60aa
23 changed files with 332 additions and 92 deletions
5
.changeset/proud-mayflies-drum.md
Normal file
5
.changeset/proud-mayflies-drum.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/netlify': minor
|
||||
---
|
||||
|
||||
Adds support for Netlify Edge Functions
|
|
@ -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
|
|
@ -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"
|
||||
|
|
4
packages/integrations/netlify/src/edge-shim.ts
Normal file
4
packages/integrations/netlify/src/edge-shim.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
(globalThis as any).process = {
|
||||
argv: [],
|
||||
env: {},
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
packages/integrations/netlify/src/netlify-edge-functions.ts
Normal file
20
packages/integrations/netlify/src/netlify-edge-functions.ts
Normal 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 };
|
||||
}
|
|
@ -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';
|
|
@ -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();
|
||||
},
|
||||
});
|
|
@ -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
|
||||
}
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/netlify-edge-astro-basic",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/netlify": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
<html>
|
||||
<head><title>Page Two</title></head>
|
||||
<body>
|
||||
<h1>Page two</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
29
packages/integrations/netlify/test/functions/test-utils.js
Normal file
29
packages/integrations/netlify/test/functions/test-utils.js
Normal 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)
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue