feat(@astrojs/netlify): edge middleware support (#7632)

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com>
This commit is contained in:
Emanuele Stoppa 2023-07-17 15:53:10 +01:00 committed by GitHub
parent cc8e9de881
commit 4c93bd8154
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 411 additions and 161 deletions

View file

@ -0,0 +1,10 @@
---
'@astrojs/netlify': minor
---
When a project uses the new option Astro `build.excludeMiddleware`, the
`@astrojs/netlify/functions` adapter will automatically create an Edge Middleware
that will automatically communicate with the Astro Middleware.
Check the [documentation](https://github.com/withastro/astro/blob/main/packages/integrations/netlify/README.md#edge-middleware-with-astro-middleware) for more details.

View file

@ -136,7 +136,7 @@ jobs:
- name: Use Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.34.1
deno-version: v1.35.0
- name: Install dependencies
run: pnpm install

View file

@ -115,6 +115,65 @@ Once you run `astro build` there will be a `dist/_redirects` file. Netlify will
> **Note**
> You can still include a `public/_redirects` file for manual redirects. Any redirects you specify in the redirects config are appended to the end of your own.
### Edge Middleware with Astro middleware
The `@astrojs/netlify/functions` adapter can automatically create an edge function that will act as "Edge Middleware", from an Astro middleware in your code base.
This is an opt-in feature and the `build.excludeMiddleware` option needs to be set to `true`:
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
build: {
excludeMiddleware: true,
},
});
```
Optionally, you can create a file recognized by the adapter named `netlify-edge-middleware.(js|ts)` in the [`srcDir`](https://docs.astro.build/en/reference/configuration-reference/#srcdir) folder to create [`Astro.locals`](https://docs.astro.build/en/reference/api-reference/#astrolocals).
Typings require the [`https://edge.netlify.com`](https://docs.netlify.com/edge-functions/api/#reference) types.
> Netlify edge functions run in a Deno environment, so you would need to import types using URLs.
>
> You can find more in the [Netlify documentation page](https://docs.netlify.com/edge-functions/api/#runtime-environment)
```ts
// src/netlify-edge-middleware.ts
import type { Context } from "https://edge.netlify.com";
export default function ({ request, context }: { request: Request, context: Context }): object {
// do something with request and context
return {
title: "Spider-man's blog",
};
}
```
The data returned by this function will be passed to Astro middleware.
The function:
- must export a **default** function;
- must **return** an `object`;
- accepts an object with a `request` and `context` as properties;
- `request` is typed as [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request);
- `context` is typed as [`Context`](https://docs.netlify.com/edge-functions/api/#edge-function-types);
#### Limitations and constraints
When you opt-in to this feature, there are a few constraints to note:
- The Edge middleware will always be the **first** function to receive the `Request` and the last function to receive `Response`. This is an architectural constraint that follows the [boundaries set by Netlify](https://docs.netlify.com/edge-functions/overview/#use-cases).
- Only `request` and `context` may be used to produce an `Astro.locals` object. Operations like redirects, etc. should be delegated to Astro middleware.
- `Astro.locals` **must be serializable**. Failing to do so will result in a **runtime error**. This means that you **cannot** store complex types like `Map`, `function`, `Set`, etc.
## Usage
[Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/)

View file

@ -33,8 +33,8 @@
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test-fn": "mocha --exit --timeout 20000 --file \"./test/setup.js\" test/functions/",
"test-edge": "deno test --allow-run --allow-read --allow-net --allow-env ./test/edge-functions/",
"test": "npm run test-fn"
"test-edge": "deno test --allow-run --allow-read --allow-net --allow-env --allow-write ./test/edge-functions/",
"test": "pnpm test-fn"
},
"dependencies": {
"@astrojs/underscore-redirects": "^0.2.0",

View file

@ -1,21 +1,10 @@
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
import esbuild from 'esbuild';
import * as fs from 'fs';
import * as npath from 'path';
import { fileURLToPath } from 'url';
import { createRedirects } from './shared.js';
interface BuildConfig {
server: URL;
client: URL;
serverEntry: string;
assets: string;
}
const SHIM = `globalThis.process = {
argv: [],
env: Deno.env.toObject(),
};`;
import {
bundleServerEntry,
createEdgeManifest,
createRedirects,
type NetlifyEdgeFunctionsOptions,
} from './shared.js';
export function getAdapter(): AstroAdapter {
return {
@ -25,92 +14,10 @@ export function getAdapter(): AstroAdapter {
};
}
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,
// Make route pattern serializable to match expected
// Netlify Edge validation format. Mirrors Netlify's own edge bundler:
// https://github.com/netlify/edge-bundler/blob/main/src/manifest.ts#L34
pattern: route.pattern.source.replace(/\\\//g, '/').toString(),
});
}
}
const manifest: NetlifyEdgeFunctionManifest = {
functions,
version: 1,
};
const baseDir = new URL('./.netlify/edge-functions/', dir);
await fs.promises.mkdir(baseDir, { recursive: true });
const manifestURL = new URL('./manifest.json', baseDir);
const _manifest = JSON.stringify(manifest, null, ' ');
await fs.promises.writeFile(manifestURL, _manifest, 'utf-8');
}
async function bundleServerEntry({ serverEntry, server }: BuildConfig, vite: any) {
const entryUrl = new URL(serverEntry, server);
const pth = fileURLToPath(entryUrl);
await esbuild.build({
target: 'es2020',
platform: 'browser',
entryPoints: [pth],
outfile: pth,
allowOverwrite: true,
format: 'esm',
bundle: true,
external: ['@astrojs/markdown-remark'],
banner: {
js: SHIM,
},
});
// Remove chunks, if they exist. Since we have bundled via esbuild these chunks are trash.
try {
const chunkFileNames =
vite?.build?.rollupOptions?.output?.chunkFileNames ?? `chunks/chunk.[hash].mjs`;
const chunkPath = npath.dirname(chunkFileNames);
const chunksDirUrl = new URL(chunkPath + '/', server);
await fs.promises.rm(chunksDirUrl, { recursive: true, force: true });
} catch {}
}
export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
let _buildConfig: BuildConfig;
let _buildConfig: AstroConfig['build'];
let _vite: any;
return {
name: '@astrojs/netlify/edge-functions',
@ -164,7 +71,8 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
}
},
'astro:build:done': async ({ routes, dir }) => {
await bundleServerEntry(_buildConfig, _vite);
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) => {

View file

@ -1,8 +1,12 @@
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
import { extname } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Args } from './netlify-functions.js';
import { createRedirects } from './shared.js';
import { fileURLToPath } from 'node:url';
import { generateEdgeMiddleware } from './middleware.js';
export const NETLIFY_EDGE_MIDDLEWARE_FILE = 'netlify-edge-middleware';
export const ASTRO_LOCALS_HEADER = 'x-astro-locals';
export function getAdapter(args: Args = {}): AstroAdapter {
return {
@ -27,6 +31,7 @@ function netlifyFunctions({
let _config: AstroConfig;
let _entryPoints: Map<RouteData, URL>;
let ssrEntryFile: string;
let _middlewareEntryPoint: URL;
return {
name: '@astrojs/netlify',
hooks: {
@ -40,7 +45,10 @@ function netlifyFunctions({
},
});
},
'astro:build:ssr': ({ entryPoints }) => {
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
if (middlewareEntryPoint) {
_middlewareEntryPoint = middlewareEntryPoint;
}
_entryPoints = entryPoints;
},
'astro:config:done': ({ config, setAdapter }) => {
@ -85,6 +93,18 @@ function netlifyFunctions({
await createRedirects(_config, routeToDynamicTargetMap, dir);
}
if (_middlewareEntryPoint) {
const outPath = fileURLToPath(new URL('./.netlify/edge-functions/', _config.root));
const netlifyEdgeMiddlewareHandlerPath = new URL(
NETLIFY_EDGE_MIDDLEWARE_FILE,
_config.srcDir
);
await generateEdgeMiddleware(
_middlewareEntryPoint,
outPath,
netlifyEdgeMiddlewareHandlerPath
);
}
},
},
};

View file

@ -0,0 +1,75 @@
import { fileURLToPath, pathToFileURL } from 'node:url';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { ASTRO_LOCALS_HEADER } from './integration-functions.js';
import { DENO_SHIM } from './shared.js';
/**
* It generates a Netlify edge function.
*
*/
export async function generateEdgeMiddleware(
astroMiddlewareEntryPointPath: URL,
outPath: string,
netlifyEdgeMiddlewareHandlerPath: URL
): Promise<URL> {
const entryPointPathURLAsString = JSON.stringify(
fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/')
);
const code = edgeMiddlewareTemplate(entryPointPathURLAsString, netlifyEdgeMiddlewareHandlerPath);
const bundledFilePath = join(outPath, 'edgeMiddleware.js');
const esbuild = await import('esbuild');
await esbuild.build({
stdin: {
contents: code,
resolveDir: process.cwd(),
},
target: 'es2020',
platform: 'browser',
outfile: bundledFilePath,
allowOverwrite: true,
format: 'esm',
bundle: true,
minify: false,
banner: {
js: DENO_SHIM,
},
});
return pathToFileURL(bundledFilePath);
}
function edgeMiddlewareTemplate(middlewarePath: string, netlifyEdgeMiddlewareHandlerPath: URL) {
const filePathEdgeMiddleware = fileURLToPath(netlifyEdgeMiddlewareHandlerPath);
let handlerTemplateImport = '';
let handlerTemplateCall = '{}';
if (existsSync(filePathEdgeMiddleware + '.js') || existsSync(filePathEdgeMiddleware + '.ts')) {
const stringified = JSON.stringify(filePathEdgeMiddleware.replace(/\\/g, '/'));
handlerTemplateImport = `import handler from ${stringified}`;
handlerTemplateCall = `handler({ request, context })`;
} else {
}
return `
${handlerTemplateImport}
import { onRequest } from ${middlewarePath};
import { createContext, trySerializeLocals } from 'astro/middleware';
export default async function middleware(request, context) {
const url = new URL(request.url);
const ctx = createContext({
request,
params: {}
});
ctx.locals = ${handlerTemplateCall};
const next = async () => {
request.headers.set(${JSON.stringify(ASTRO_LOCALS_HEADER)}, trySerializeLocals(ctx.locals));
return await context.next();
};
return onRequest(ctx, next);
}
export const config = {
path: "/*"
}
`;
}

View file

@ -2,6 +2,7 @@ import { polyfill } from '@astrojs/webapi';
import { builder, type Handler } from '@netlify/functions';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { ASTRO_LOCALS_HEADER } from './integration-functions.js';
polyfill(globalThis, {
exclude: 'window document',
@ -80,8 +81,14 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
const ip = headers['x-nf-client-connection-ip'];
Reflect.set(request, clientAddressSymbol, ip);
const response: Response = await app.render(request, routeData);
let locals = {};
if (request.headers.has(ASTRO_LOCALS_HEADER)) {
let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER);
if (localsAsString) {
locals = JSON.parse(localsAsString);
}
}
const response: Response = await app.render(request, routeData, locals);
const responseHeaders = Object.fromEntries(response.headers.entries());
const responseContentType = parseContentType(responseHeaders['content-type']);

View file

@ -1,6 +1,37 @@
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
import type { AstroConfig, RouteData } from 'astro';
import fs from 'node:fs';
import { fileURLToPath } from 'url';
import esbuild from 'esbuild';
import npath from 'path';
export const DENO_SHIM = `globalThis.process = {
argv: [],
env: Deno.env.toObject(),
};`;
export interface NetlifyEdgeFunctionsOptions {
dist?: URL;
}
export interface NetlifyEdgeFunctionManifestFunctionPath {
function: string;
path: string;
}
export interface NetlifyEdgeFunctionManifestFunctionPattern {
function: string;
pattern: string;
}
export type NetlifyEdgeFunctionManifestFunction =
| NetlifyEdgeFunctionManifestFunctionPath
| NetlifyEdgeFunctionManifestFunctionPattern;
export interface NetlifyEdgeFunctionManifest {
functions: NetlifyEdgeFunctionManifestFunction[];
version: 1;
}
export async function createRedirects(
config: AstroConfig,
@ -21,3 +52,63 @@ export async function createRedirects(
// If the file does not exist yet, appendFile() automatically creates it.
await fs.promises.appendFile(_redirectsURL, content, 'utf-8');
}
export 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,
// Make route pattern serializable to match expected
// Netlify Edge validation format. Mirrors Netlify's own edge bundler:
// https://github.com/netlify/edge-bundler/blob/main/src/manifest.ts#L34
pattern: route.pattern.source.replace(/\\\//g, '/').toString(),
});
}
}
const manifest: NetlifyEdgeFunctionManifest = {
functions,
version: 1,
};
const baseDir = new URL('./.netlify/edge-functions/', dir);
await fs.promises.mkdir(baseDir, { recursive: true });
const manifestURL = new URL('./manifest.json', baseDir);
const _manifest = JSON.stringify(manifest, null, ' ');
await fs.promises.writeFile(manifestURL, _manifest, 'utf-8');
}
export async function bundleServerEntry(entryUrl: URL, serverUrl?: URL, vite?: any | undefined) {
const pth = fileURLToPath(entryUrl);
await esbuild.build({
target: 'es2020',
platform: 'browser',
entryPoints: [pth],
outfile: pth,
allowOverwrite: true,
format: 'esm',
bundle: true,
external: ['@astrojs/markdown-remark', 'astro/middleware'],
banner: {
js: DENO_SHIM,
},
});
// Remove chunks, if they exist. Since we have bundled via esbuild these chunks are trash.
if (vite && serverUrl) {
try {
const chunkFileNames =
vite?.build?.rollupOptions?.output?.chunkFileNames ?? `chunks/chunk.[hash].mjs`;
const chunkPath = npath.dirname(chunkFileNames);
const chunksDirUrl = new URL(chunkPath + '/', serverUrl);
await fs.promises.rm(chunksDirUrl, { recursive: true, force: true });
} catch {}
}
}

View file

@ -5,7 +5,7 @@ export {
assert,
assertExists,
} from 'https://deno.land/std@0.132.0/testing/asserts.ts';
export * from 'https://deno.land/x/deno_dom/deno-dom-wasm.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,10 +1,12 @@
import { runBuild, runApp } from './test-utils.ts';
import { loadFixture } from './test-utils.ts';
import { assertEquals, assert, DOMParser } from './deps.ts';
Deno.test({
name: 'Dynamic imports',
permissions: 'inherit',
async fn() {
await runBuild('./fixtures/dynimport/');
const { runApp, runBuild } = await loadFixture('./fixtures/dynimport/');
await runBuild();
const stop = await runApp('./fixtures/dynimport/prod.js');
try {
@ -14,8 +16,10 @@ Deno.test({
assert(html, 'got some html');
const doc = new DOMParser().parseFromString(html, `text/html`);
const div = doc.querySelector('#thing');
assert(div, 'div exists');
if (doc) {
const div = doc.querySelector('#thing');
assert(div, 'div exists');
}
} catch (err) {
console.error(err);
} finally {

View file

@ -3,30 +3,34 @@ import { assertEquals, assert, DOMParser } from './deps.ts';
Deno.env.set('SECRET_STUFF', 'secret');
// @ts-expect-error
Deno.test({
// TODO: debug why build cannot be found in "await import"
ignore: true,
name: 'Edge Basics',
skip: true,
async fn() {
permissions: 'inherit',
async fn(t) {
const fixture = loadFixture('./fixtures/edge-basic/');
await fixture.runBuild();
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');
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 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');
const envDiv = doc.querySelector('#env');
assertEquals(envDiv?.innerText, 'secret');
});
await fixture.cleanup();
await t.step('Clean up', async () => {
await fixture.cleanup();
});
},
});

View file

@ -1,6 +1,5 @@
import { defineConfig } from 'astro/config';
import { netlifyEdgeFunctions } from '@astrojs/netlify';
import react from "@astrojs/react";
// test env var
process.env.SECRET_STUFF = 'secret'
@ -9,6 +8,5 @@ export default defineConfig({
adapter: netlifyEdgeFunctions({
dist: new URL('./dist/', import.meta.url),
}),
integrations: [react()],
output: 'server',
})

View file

@ -5,8 +5,6 @@
"dependencies": {
"astro": "workspace:*",
"@astrojs/react": "workspace:*",
"@astrojs/netlify": "workspace:*",
"react": "^18.1.0",
"react-dom": "^18.1.0"
"@astrojs/netlify": "workspace:*"
}
}

View file

@ -1,7 +0,0 @@
import React from 'react';
export default function() {
return (
<div id="react">testing</div>
)
}

View file

@ -1,6 +1,3 @@
---
import ReactComponent from '../components/React.jsx';
---
<html>
<head><title>Testing</title></head>
<body>
@ -9,7 +6,6 @@ import ReactComponent from '../components/React.jsx';
<ul>
<li><a href="/two/">Two</a></li>
</ul>
<ReactComponent />
<div id="env">{import.meta.env.SECRET_STUFF}</div>
</body>
</html>

View file

@ -3,12 +3,16 @@ import { assertEquals, assertExists, cheerio, fs } from './deps.ts';
Deno.test({
name: 'Prerender',
permissions: 'inherit',
async fn(t) {
const environmentVariables = {
PRERENDER: 'true',
};
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
await fixture.runBuild();
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(
@ -16,7 +20,7 @@ Deno.test({
);
assertExists(handler);
const response = await handler(new Request('http://example.com/index.html'));
assertEquals(response, undefined, "No response because this route doesn't exist");
assertEquals(response.status, 404, "No response because this route doesn't exist");
});
await t.step('Prerendered route exists', async () => {
@ -31,22 +35,28 @@ Deno.test({
});
Deno.env.delete('PRERENDER');
await fixture.cleanup();
await cleanup();
},
});
Deno.test({
name: 'Hybrid rendering',
permissions: 'inherit',
async fn(t) {
const environmentVariables = {
PRERENDER: 'false',
};
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
await fixture.runBuild();
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 response = await fetch('http://127.0.0.1:8085/');
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();
@ -60,7 +70,7 @@ Deno.test({
'./fixtures/prerender/.netlify/edge-functions/entry.js'
);
const response = await handler(new Request('http://example.com/index.html'));
assertEquals(response, undefined, "No response because this route doesn't exist");
assertEquals(response.status, 404, "No response because this route doesn't exist");
});
await t.step('Has no prerendered route', async () => {

View file

@ -0,0 +1,43 @@
import netlifyAdapter from '../../dist/index.js';
import { testIntegration, loadFixture } from './test-utils.js';
import { expect } from 'chai';
describe('Middleware', () => {
it('with edge handle file, should successfully build the middleware', async () => {
/** @type {import('./test-utils').Fixture} */
const fixture = await loadFixture({
root: new URL('./fixtures/middleware-with-handler-file/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/middleware-with-handler-file/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
build: {
excludeMiddleware: true,
},
});
await fixture.build();
const contents = await fixture.readFile('../.netlify/edge-functions/edgeMiddleware.js');
expect(contents.includes('"Hello world"')).to.be.true;
});
it('without edge handle file, should successfully build the middleware', async () => {
/** @type {import('./test-utils').Fixture} */
const fixture = await loadFixture({
root: new URL('./fixtures/middleware-without-handler-file/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/middleware-without-handler-file/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
build: {
excludeMiddleware: true,
},
});
await fixture.build();
const contents = await fixture.readFile('../.netlify/edge-functions/edgeMiddleware.js');
expect(contents.includes('"Hello world"')).to.be.false;
});
});

View file

@ -0,0 +1,5 @@
export const onRequest = (context, next) => {
context.locals.title = 'Middleware';
return next();
};

View file

@ -0,0 +1,5 @@
export default function ({ request, context }) {
return {
title: 'Hello world',
};
}

View file

@ -0,0 +1,12 @@
---
const title = Astro.locals.title;
---
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
</body>
</html>

View file

@ -0,0 +1,5 @@
export const onRequest = (context, next) => {
context.locals.title = 'Middleware';
return next();
};

View file

@ -0,0 +1,12 @@
---
const title = Astro.locals.title;
---
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
</body>
</html>

View file

@ -6,6 +6,7 @@
"module": "ES2022",
"outDir": "./dist",
"target": "ES2021",
"typeRoots": ["node_modules/@types", "node_modules/@netlify"]
"typeRoots": ["node_modules/@types", "node_modules/@netlify"],
"allowImportingTsExtensions": true
}
}

View file

@ -4464,12 +4464,6 @@ importers:
astro:
specifier: workspace:*
version: link:../../../../../../astro
react:
specifier: ^18.1.0
version: 18.2.0
react-dom:
specifier: ^18.1.0
version: 18.2.0(react@18.2.0)
packages/integrations/netlify/test/edge-functions/fixtures/prerender:
dependencies: