Remove the Nelify Edge adapter (#8029)

This commit is contained in:
Matthew Phillips 2023-08-10 14:38:51 -04:00 committed by GitHub
parent 9cc4e48e6a
commit 2ee418e06a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 27 additions and 543 deletions

View file

@ -0,0 +1,23 @@
---
'@astrojs/netlify': major
---
Remove the Netlify Edge adapter
`@astrojs/netlify/functions` now supports Edge middleware, so a separate adapter for Edge itself (deploying your entire app to the edge) is no longer necessary. Please update your Astro config to reflect this change:
```diff
// astro.config.mjs
import { defineConfig } from 'astro/config';
- import netlify from '@astrojs/netlify/edge';
+ import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify({
+ edgeMiddleware: true
}),
});
```
This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serveless + Edge Middleware, we are removing the Edge adapter.

View file

@ -55,28 +55,11 @@ If you prefer to install the adapter manually instead, complete the following tw
}); });
``` ```
### Edge Functions
Netlify has two serverless platforms, [Netlify Functions](https://docs.netlify.com/functions/overview/) and [Netlify Edge Functions](https://docs.netlify.com/edge-functions/overview/). With Edge Functions your code is distributed closer to your users, lowering latency.
To deploy with Edge Functions, use `netlify/edge-functions` in the Astro config file instead of `netlify/functions`.
```js ins={3}
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/edge-functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
});
```
### Run middleware in Edge Functions ### Run middleware in Edge Functions
When deploying to Netlify Functions, you can choose to use an Edge Function to run your Astro middleware. When deploying to Netlify Functions, you can choose to use an Edge Function to run your Astro middleware.
To enable this, set the `build.excludeMiddleware` Astro config option to `true`: To enable this, set the `edgeMiddleware` config option to `true`:
```js ins={9} ```js ins={9}
// astro.config.mjs // astro.config.mjs
@ -85,10 +68,9 @@ import netlify from '@astrojs/netlify/functions';
export default defineConfig({ export default defineConfig({
output: 'server', output: 'server',
adapter: netlify(), adapter: netlify({
build: { edgeMiddleware: true,
excludeMiddleware: true, }),
},
}); });
``` ```

View file

@ -1,3 +1,2 @@
export { netlifyEdgeFunctions } from './integration-edge-functions.js';
export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js'; export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js';
export { netlifyStatic } from './integration-static.js'; export { netlifyStatic } from './integration-static.js';

View file

@ -1,98 +0,0 @@
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
import {
bundleServerEntry,
createEdgeManifest,
createRedirects,
type NetlifyEdgeFunctionsOptions,
} from './shared.js';
export function getAdapter(): AstroAdapter {
return {
name: '@astrojs/netlify/edge-functions',
serverEntrypoint: '@astrojs/netlify/netlify-edge-functions.js',
exports: ['default'],
supportedAstroFeatures: {
hybridOutput: 'stable',
staticOutput: 'stable',
serverOutput: 'stable',
assets: {
supportKind: 'stable',
isSharpCompatible: false,
isSquooshCompatible: false,
},
},
};
}
export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
let _buildConfig: AstroConfig['build'];
let _vite: any;
return {
name: '@astrojs/netlify/edge-functions',
hooks: {
'astro:config:setup': ({ config, updateConfig }) => {
const outDir = dist ?? new URL('./dist/', config.root);
updateConfig({
outDir,
build: {
client: outDir,
server: new URL('./.netlify/edge-functions/', config.root),
// Netlify expects .js and will always interpret as ESM
serverEntry: 'entry.js',
},
});
},
'astro:config:done': ({ config, setAdapter }) => {
setAdapter(getAdapter());
_config = config;
_buildConfig = config.build;
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
if (config.output === 'static') {
console.warn(
`[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
);
console.warn(
`[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`
);
}
},
'astro:build:setup': ({ vite, target }) => {
if (target === 'server') {
_vite = vite;
vite.resolve = vite.resolve || {};
vite.resolve.alias = vite.resolve.alias || {};
const aliases = [{ find: 'react-dom/server', replacement: 'react-dom/server.browser' }];
if (Array.isArray(vite.resolve.alias)) {
vite.resolve.alias = [...vite.resolve.alias, ...aliases];
} else {
for (const alias of aliases) {
(vite.resolve.alias as Record<string, string>)[alias.find] = alias.replacement;
}
}
vite.ssr = {
noExternal: true,
};
}
},
'astro:build:done': async ({ routes, dir }) => {
const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
await bundleServerEntry(entryUrl, _buildConfig.server, _vite);
await createEdgeManifest(routes, entryFile, _config.root);
const dynamicTarget = `/.netlify/edge-functions/${entryFile}`;
const map: [RouteData, string][] = routes.map((route) => {
return [route, dynamicTarget];
});
const routeToDynamicTargetMap = new Map(Array.from(map));
await createRedirects(_config, routeToDynamicTargetMap, dir);
},
},
};
}
export { netlifyEdgeFunctions as default };

View file

@ -1,34 +0,0 @@
import type { Context } from '@netlify/edge-functions';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
const clientAddressSymbol = Symbol.for('astro.clientAddress');
export function createExports(manifest: SSRManifest) {
const app = new App(manifest);
const handler = async (request: Request, context: Context): Promise<Response | void> => {
const url = new URL(request.url);
// If this matches a static asset, just return and Netlify will forward it
// to its static asset handler.
if (manifest.assets.has(url.pathname)) {
return;
}
const routeData = app.match(request);
const ip =
request.headers.get('x-nf-client-connection-ip') ||
context?.ip ||
(context as any)?.remoteAddr?.hostname;
Reflect.set(request, clientAddressSymbol, ip);
const response = await app.render(request, routeData);
if (app.setCookieHeaders) {
for (const setCookieHeader of app.setCookieHeaders(response)) {
response.headers.append('Set-Cookie', setCookieHeader);
}
}
return response;
};
return { default: handler };
}

View file

@ -1,11 +0,0 @@
// @ts-nocheck
export { fromFileUrl } from 'https://deno.land/std@0.110.0/path/mod.ts';
export {
assertEquals,
assert,
assertExists,
} from 'https://deno.land/std@0.132.0/testing/asserts.ts';
export { DOMParser } from 'https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts';
export * from 'https://deno.land/std@0.142.0/streams/conversion.ts';
export * as cheerio from 'https://cdn.skypack.dev/cheerio?dts';
export * as fs from 'https://deno.land/std/fs/mod.ts';

View file

@ -1,29 +0,0 @@
import { loadFixture } from './test-utils.ts';
import { assertEquals, assert, DOMParser } from './deps.ts';
Deno.test({
name: 'Dynamic imports',
permissions: 'inherit',
async fn() {
const { runApp, runBuild } = await loadFixture('./fixtures/dynimport/');
await runBuild();
const stop = await runApp('./fixtures/dynimport/prod.js');
try {
const response = await fetch('http://127.0.0.1:8085/');
assertEquals(response.status, 200);
const html = await response.text();
assert(html, 'got some html');
const doc = new DOMParser().parseFromString(html, `text/html`);
if (doc) {
const div = doc.querySelector('#thing');
assert(div, 'div exists');
}
} catch (err) {
console.error(err);
} finally {
await stop();
}
},
});

View file

@ -1,36 +0,0 @@
import { loadFixture } from './test-utils.ts';
import { assertEquals, assert, DOMParser } from './deps.ts';
Deno.env.set('SECRET_STUFF', 'secret');
Deno.test({
ignore: true,
name: 'Edge Basics',
permissions: 'inherit',
async fn(t) {
const fixture = loadFixture('./fixtures/edge-basic/');
await t.step('Run the build', async () => {
await fixture.runBuild();
});
await t.step('Should correctly render the response', async () => {
const { default: handler } = await import(
'./fixtures/edge-basic/.netlify/edge-functions/entry.js'
);
const response = await handler(new Request('http://example.com/'));
assertEquals(response.status, 200);
const html = await response.text();
assert(html, 'got some html');
const doc = new DOMParser().parseFromString(html, `text/html`)!;
const div = doc.querySelector('#react');
assert(div, 'div exists');
const envDiv = doc.querySelector('#env');
assertEquals(envDiv?.innerText, 'secret');
});
await t.step('Clean up', async () => {
await fixture.cleanup();
});
},
});

View file

@ -1,9 +0,0 @@
import { defineConfig } from 'astro/config';
import { netlifyEdgeFunctions } from '@astrojs/netlify';
export default defineConfig({
adapter: netlifyEdgeFunctions({
dist: new URL('./dist/', import.meta.url),
}),
output: 'server',
})

View file

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

View file

@ -1,11 +0,0 @@
import handler from './.netlify/edge-functions/entry.js';
import { Server } from 'https://deno.land/std@0.132.0/http/server.ts';
const _server = new Server({
port: 8085,
hostname: '0.0.0.0',
handler,
});
_server.listenAndServe();
console.error(`Server running on port 8085`);

View file

@ -1,4 +0,0 @@
---
---
<div id="thing">testing</div>

View file

@ -1,11 +0,0 @@
---
const { default: Thing } = await import('../components/Thing.astro');
---
<html>
<head>
<title>testing</title>
</head>
<body>
<Thing />
</body>
</html>

View file

@ -1,12 +0,0 @@
import { defineConfig } from 'astro/config';
import { netlifyEdgeFunctions } from '@astrojs/netlify';
// test env var
process.env.SECRET_STUFF = 'secret'
export default defineConfig({
adapter: netlifyEdgeFunctions({
dist: new URL('./dist/', import.meta.url),
}),
output: 'server',
})

View file

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

View file

@ -1,11 +0,0 @@
<html>
<head><title>Testing</title></head>
<body>
<h1>Test page</h1>
<h2>Links</h2>
<ul>
<li><a href="/two/">Two</a></li>
</ul>
<div id="env">{import.meta.env.SECRET_STUFF}</div>
</body>
</html>

View file

@ -1,7 +0,0 @@
---
title: Hey there!
---
# {frontmatter.title}!
It's a markdown file!

View file

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

View file

@ -1,16 +0,0 @@
import { defineConfig } from "astro/config";
import { netlifyEdgeFunctions } from "@astrojs/netlify";
const isHybridMode = process.env.PRERENDER === "false";
/** @type {import('astro').AstroConfig} */
const partialConfig = {
output: isHybridMode ? "hybrid" : "server",
};
export default defineConfig({
adapter: netlifyEdgeFunctions({
dist: new URL("./dist/", import.meta.url),
}),
...partialConfig,
});

View file

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

View file

@ -1,12 +0,0 @@
---
export const prerender = import.meta.env.PRERENDER;
---
<html>
<head>
<title>testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>

View file

@ -1,9 +0,0 @@
import { defineConfig } from 'astro/config';
import { netlifyEdgeFunctions } from '@astrojs/netlify';
export default defineConfig({
adapter: netlifyEdgeFunctions({
dist: new URL('./dist/', import.meta.url),
}),
output: 'server',
})

View file

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

View file

@ -1,9 +0,0 @@
<html>
<head>
<title>Testing</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,86 +0,0 @@
import { loadFixture } from './test-utils.ts';
import { assertEquals, assertExists, cheerio, fs } from './deps.ts';
Deno.test({
name: 'Prerender',
permissions: 'inherit',
async fn(t) {
const environmentVariables = {
PRERENDER: 'true',
};
const { runBuild, cleanup } = loadFixture('./fixtures/prerender/', environmentVariables);
await t.step('Run the build', async () => {
await runBuild();
});
await t.step('Handler can process requests to non-existing routes', async () => {
const { default: handler } = await import(
'./fixtures/prerender/.netlify/edge-functions/entry.js'
);
assertExists(handler);
const response = await handler(new Request('http://example.com/index.html'));
assertEquals(response.status, 404, "No response because this route doesn't exist");
});
await t.step('Prerendered route exists', async () => {
let content: string | null = null;
try {
const path = new URL('./fixtures/prerender/dist/index.html', import.meta.url);
content = Deno.readTextFileSync(path);
} catch (e) {}
assertExists(content);
const $ = cheerio.load(content);
assertEquals($('h1').text(), 'testing');
});
Deno.env.delete('PRERENDER');
await cleanup();
},
});
Deno.test({
name: 'Hybrid rendering',
permissions: 'inherit',
async fn(t) {
const environmentVariables = {
PRERENDER: 'false',
};
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
await t.step('Run the build', async () => {
await fixture.runBuild();
});
const stop = await fixture.runApp('./fixtures/prerender/prod.js');
await t.step('Can fetch server route', async () => {
const { default: handler } = await import(
'./fixtures/prerender/.netlify/edge-functions/entry.js'
);
const response = await handler(new Request('http://example.com/'));
assertEquals(response.status, 200);
const html = await response.text();
const $ = cheerio.load(html);
assertEquals($('h1').text(), 'testing');
});
stop();
await t.step('Handler can process requests to non-existing routes', async () => {
const { default: handler } = await import(
'./fixtures/prerender/.netlify/edge-functions/entry.js'
);
const response = await handler(new Request('http://example.com/index.html'));
assertEquals(response.status, 404, "No response because this route doesn't exist");
});
await t.step('Has no prerendered route', async () => {
let prerenderedRouteExists = false;
try {
const path = new URL('./fixtures/prerender/dist/index.html', import.meta.url);
prerenderedRouteExists = fs.existsSync(path);
} catch (e) {}
assertEquals(prerenderedRouteExists, false);
});
await fixture.cleanup();
},
});

View file

@ -1,19 +0,0 @@
import { loadFixture } from './test-utils.ts';
import { assertEquals } from './deps.ts';
Deno.test({
// TODO: debug why build cannot be found in "await import"
ignore: true,
name: 'Assets are preferred over HTML routes',
async fn() {
const fixture = loadFixture('./fixtures/root-dynamic/');
await fixture.runBuild();
const { default: handler } = await import(
'./fixtures/root-dynamic/.netlify/edge-functions/entry.js'
);
const response = await handler(new Request('http://example.com/styles.css'));
assertEquals(response, undefined, 'No response because this is an asset');
await fixture.cleanup();
},
});

View file

@ -1,50 +0,0 @@
import { fromFileUrl, readableStreamFromReader } from './deps.ts';
const dir = new URL('./', import.meta.url);
export function loadFixture(fixturePath: string, envionmentVariables?: Record<string, string>) {
async function runBuild() {
const proc = Deno.run({
cmd: ['node', '../../../../../../astro/astro.js', 'build'],
env: envionmentVariables,
cwd: fromFileUrl(new URL(fixturePath, dir)),
});
await proc.status();
proc.close();
}
async function runApp(entryPath: string) {
const entryUrl = new URL(entryPath, dir);
let proc = Deno.run({
cmd: ['deno', 'run', '--allow-env', '--allow-net', fromFileUrl(entryUrl)],
env: envionmentVariables,
//cwd: fromFileUrl(entryUrl),
stderr: 'piped',
});
const stderr = readableStreamFromReader(proc.stderr);
const dec = new TextDecoder();
for await (let bytes of stderr) {
let msg = dec.decode(bytes);
if (msg.includes(`Server running`)) {
break;
}
}
return () => proc.close();
}
async function cleanup() {
const netlifyPath = new URL('.netlify', new URL(fixturePath, dir));
const distPath = new URL('dist', new URL(fixturePath, dir));
// remove the netlify folder
await Deno.remove(netlifyPath, { recursive: true });
// remove the dist folder
await Deno.remove(distPath, { recursive: true });
}
return {
runApp,
runBuild,
cleanup,
};
}