summaryrefslogtreecommitdiff
path: root/packages/integrations/netlify/src
diff options
context:
space:
mode:
authorGravatar Emanuele Stoppa <my.burning@gmail.com> 2023-07-17 15:53:10 +0100
committerGravatar GitHub <noreply@github.com> 2023-07-17 15:53:10 +0100
commit4c93bd8154c210ebce6ad2889bd8bfdf4c349a78 (patch)
treee0b9fb9474845411b35f177260408f444d0631ff /packages/integrations/netlify/src
parentcc8e9de88179d2ed4b70980c60b41448db393429 (diff)
downloadastro-4c93bd8154c210ebce6ad2889bd8bfdf4c349a78.tar.gz
astro-4c93bd8154c210ebce6ad2889bd8bfdf4c349a78.tar.zst
astro-4c93bd8154c210ebce6ad2889bd8bfdf4c349a78.zip
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>
Diffstat (limited to 'packages/integrations/netlify/src')
-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
5 files changed, 206 insertions, 105 deletions
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 {}
+ }
+}