summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/great-days-judge.md10
-rw-r--r--.github/workflows/ci.yml2
-rw-r--r--packages/integrations/netlify/README.md59
-rw-r--r--packages/integrations/netlify/package.json4
-rw-r--r--packages/integrations/netlify/src/integration-edge-functions.ts110
-rw-r--r--packages/integrations/netlify/src/integration-functions.ts24
-rw-r--r--packages/integrations/netlify/src/middleware.ts75
-rw-r--r--packages/integrations/netlify/src/netlify-functions.ts11
-rw-r--r--packages/integrations/netlify/src/shared.ts91
-rw-r--r--packages/integrations/netlify/test/edge-functions/deps.ts2
-rw-r--r--packages/integrations/netlify/test/edge-functions/dynamic-import.test.ts (renamed from packages/integrations/netlify/test/edge-functions/dynamic-import.test.js)12
-rw-r--r--packages/integrations/netlify/test/edge-functions/edge-basic.test.ts40
-rw-r--r--packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs2
-rw-r--r--packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json4
-rw-r--r--packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx7
-rw-r--r--packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro4
-rw-r--r--packages/integrations/netlify/test/edge-functions/prerender.test.ts24
-rw-r--r--packages/integrations/netlify/test/functions/edge-middleware.test.js43
-rw-r--r--packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts5
-rw-r--r--packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js5
-rw-r--r--packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro12
-rw-r--r--packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts5
-rw-r--r--packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro12
-rw-r--r--packages/integrations/netlify/tsconfig.json3
-rw-r--r--pnpm-lock.yaml6
25 files changed, 411 insertions, 161 deletions
diff --git a/.changeset/great-days-judge.md b/.changeset/great-days-judge.md
new file mode 100644
index 000000000..5c08417fe
--- /dev/null
+++ b/.changeset/great-days-judge.md
@@ -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.
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8c94e8d4d..329835b80 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md
index 5e7895217..31337b97d 100644
--- a/packages/integrations/netlify/README.md
+++ b/packages/integrations/netlify/README.md
@@ -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/)
diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json
index 0b3ada36b..632e5af19 100644
--- a/packages/integrations/netlify/package.json
+++ b/packages/integrations/netlify/package.json
@@ -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",
diff --git a/packages/integrations/netlify/src/integration-edge-functions.ts b/packages/integrations/netlify/src/integration-edge-functions.ts
index 72721bd59..ac7c124fb 100644
--- a/packages/integrations/netlify/src/integration-edge-functions.ts
+++ b/packages/integrations/netlify/src/integration-edge-functions.ts
@@ -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) => {
diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts
index 3e8c476e5..64bdbb203 100644
--- a/packages/integrations/netlify/src/integration-functions.ts
+++ b/packages/integrations/netlify/src/integration-functions.ts
@@ -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
+ );
+ }
},
},
};
diff --git a/packages/integrations/netlify/src/middleware.ts b/packages/integrations/netlify/src/middleware.ts
new file mode 100644
index 000000000..a53d4fbde
--- /dev/null
+++ b/packages/integrations/netlify/src/middleware.ts
@@ -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: "/*"
+}
+`;
+}
diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts
index 915f72955..8d0196d5e 100644
--- a/packages/integrations/netlify/src/netlify-functions.ts
+++ b/packages/integrations/netlify/src/netlify-functions.ts
@@ -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']);
diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts
index ca45dc752..c0ed9ec17 100644
--- a/packages/integrations/netlify/src/shared.ts
+++ b/packages/integrations/netlify/src/shared.ts
@@ -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 {}
+ }
+}
diff --git a/packages/integrations/netlify/test/edge-functions/deps.ts b/packages/integrations/netlify/test/edge-functions/deps.ts
index c6ced8814..6d729970d 100644
--- a/packages/integrations/netlify/test/edge-functions/deps.ts
+++ b/packages/integrations/netlify/test/edge-functions/deps.ts
@@ -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';
diff --git a/packages/integrations/netlify/test/edge-functions/dynamic-import.test.js b/packages/integrations/netlify/test/edge-functions/dynamic-import.test.ts
index bfa895290..89a640b0b 100644
--- a/packages/integrations/netlify/test/edge-functions/dynamic-import.test.js
+++ b/packages/integrations/netlify/test/edge-functions/dynamic-import.test.ts
@@ -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 {
diff --git a/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts b/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts
index 9f2a7bde3..699ab0014 100644
--- a/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts
+++ b/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts
@@ -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();
+ });
},
});
diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs
index 310088c88..ac15ad4e9 100644
--- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs
+++ b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs
@@ -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',
})
diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json
index e84f1d5f3..16ff30088 100644
--- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json
+++ b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json
@@ -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:*"
}
}
diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx
deleted file mode 100644
index c6cf39aa5..000000000
--- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-export default function() {
- return (
- <div id="react">testing</div>
- )
-}
diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro
index 1247ba8f6..a480cf46c 100644
--- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro
+++ b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro
@@ -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>
diff --git a/packages/integrations/netlify/test/edge-functions/prerender.test.ts b/packages/integrations/netlify/test/edge-functions/prerender.test.ts
index 4d4dfc9c6..2c066b9b8 100644
--- a/packages/integrations/netlify/test/edge-functions/prerender.test.ts
+++ b/packages/integrations/netlify/test/edge-functions/prerender.test.ts
@@ -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 () => {
diff --git a/packages/integrations/netlify/test/functions/edge-middleware.test.js b/packages/integrations/netlify/test/functions/edge-middleware.test.js
new file mode 100644
index 000000000..219fd1ced
--- /dev/null
+++ b/packages/integrations/netlify/test/functions/edge-middleware.test.js
@@ -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;
+ });
+});
diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts
new file mode 100644
index 000000000..8cab418c1
--- /dev/null
+++ b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts
@@ -0,0 +1,5 @@
+export const onRequest = (context, next) => {
+ context.locals.title = 'Middleware';
+
+ return next();
+};
diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js
new file mode 100644
index 000000000..bf69edb3e
--- /dev/null
+++ b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js
@@ -0,0 +1,5 @@
+export default function ({ request, context }) {
+ return {
+ title: 'Hello world',
+ };
+}
diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro
new file mode 100644
index 000000000..d97f70698
--- /dev/null
+++ b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+const title = Astro.locals.title;
+---
+
+<html>
+<head>
+ <title>{title}</title>
+</head>
+<body>
+<h1>{title}</h1>
+</body>
+</html>
diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts
new file mode 100644
index 000000000..8cab418c1
--- /dev/null
+++ b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts
@@ -0,0 +1,5 @@
+export const onRequest = (context, next) => {
+ context.locals.title = 'Middleware';
+
+ return next();
+};
diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro
new file mode 100644
index 000000000..d97f70698
--- /dev/null
+++ b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+const title = Astro.locals.title;
+---
+
+<html>
+<head>
+ <title>{title}</title>
+</head>
+<body>
+<h1>{title}</h1>
+</body>
+</html>
diff --git a/packages/integrations/netlify/tsconfig.json b/packages/integrations/netlify/tsconfig.json
index 9f96de7cd..4442d4c36 100644
--- a/packages/integrations/netlify/tsconfig.json
+++ b/packages/integrations/netlify/tsconfig.json
@@ -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
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 036b745f2..4e9db8a0f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: