diff options
| author | 2023-10-16 14:10:41 +0000 | |
|---|---|---|
| committer | 2023-10-16 14:10:41 +0000 | |
| commit | 3d1329414e507345b19e1dee81fe1bbff4eb3ae0 (patch) | |
| tree | 69c822b1dc12f177b4919337109e6300a04df91d /packages/integrations/netlify/src | |
| parent | 63977f5e438062b7e9302b8feff41b662bfa6231 (diff) | |
| download | astro-3d1329414e507345b19e1dee81fe1bbff4eb3ae0.tar.gz astro-3d1329414e507345b19e1dee81fe1bbff4eb3ae0.tar.zst astro-3d1329414e507345b19e1dee81fe1bbff4eb3ae0.zip | |
[ci] format
Diffstat (limited to 'packages/integrations/netlify/src')
| -rw-r--r-- | packages/integrations/netlify/src/env.d.ts | 2 | ||||
| -rw-r--r-- | packages/integrations/netlify/src/index.ts | 7 | ||||
| -rw-r--r-- | packages/integrations/netlify/src/integration-functions.ts | 273 | ||||
| -rw-r--r-- | packages/integrations/netlify/src/integration-static.ts | 54 | ||||
| -rw-r--r-- | packages/integrations/netlify/src/middleware.ts | 101 | ||||
| -rw-r--r-- | packages/integrations/netlify/src/netlify-functions.ts | 413 | ||||
| -rw-r--r-- | packages/integrations/netlify/src/shared.ts | 171 |
7 files changed, 541 insertions, 480 deletions
diff --git a/packages/integrations/netlify/src/env.d.ts b/packages/integrations/netlify/src/env.d.ts index 8c34fb45e..f964fe0cf 100644 --- a/packages/integrations/netlify/src/env.d.ts +++ b/packages/integrations/netlify/src/env.d.ts @@ -1 +1 @@ -/// <reference types="astro/client" />
\ No newline at end of file +/// <reference types="astro/client" /> diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index a374020f9..f9a974d61 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -1,2 +1,5 @@ -export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js'; -export { netlifyStatic } from './integration-static.js'; +export { + netlifyFunctions as default, + netlifyFunctions, +} from "./integration-functions.js"; +export { netlifyStatic } from "./integration-static.js"; diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts index ed16e0200..e4586d654 100644 --- a/packages/integrations/netlify/src/integration-functions.ts +++ b/packages/integrations/netlify/src/integration-functions.ts @@ -1,144 +1,165 @@ -import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; -import { writeFile } from 'node:fs/promises'; -import { extname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { generateEdgeMiddleware } from './middleware.js'; -import type { Args } from './netlify-functions.js'; -import { createRedirects } from './shared.js'; +import type { + AstroAdapter, + AstroConfig, + AstroIntegration, + RouteData, +} from "astro"; +import { writeFile } from "node:fs/promises"; +import { extname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { generateEdgeMiddleware } from "./middleware.js"; +import type { Args } from "./netlify-functions.js"; +import { createRedirects } from "./shared.js"; -export const NETLIFY_EDGE_MIDDLEWARE_FILE = 'netlify-edge-middleware'; -export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; +export const NETLIFY_EDGE_MIDDLEWARE_FILE = "netlify-edge-middleware"; +export const ASTRO_LOCALS_HEADER = "x-astro-locals"; -export function getAdapter({ functionPerRoute, edgeMiddleware, ...args }: Args): AstroAdapter { - return { - name: '@astrojs/netlify/functions', - serverEntrypoint: '@astrojs/netlify/netlify-functions.js', - exports: ['handler'], - args, - adapterFeatures: { - functionPerRoute, - edgeMiddleware, - }, - supportedAstroFeatures: { - hybridOutput: 'stable', - staticOutput: 'stable', - serverOutput: 'stable', - assets: { - supportKind: 'stable', - isSharpCompatible: true, - isSquooshCompatible: true, - }, - }, - }; +export function getAdapter({ + functionPerRoute, + edgeMiddleware, + ...args +}: Args): AstroAdapter { + return { + name: "@astrojs/netlify/functions", + serverEntrypoint: "@astrojs/netlify/netlify-functions.js", + exports: ["handler"], + args, + adapterFeatures: { + functionPerRoute, + edgeMiddleware, + }, + supportedAstroFeatures: { + hybridOutput: "stable", + staticOutput: "stable", + serverOutput: "stable", + assets: { + supportKind: "stable", + isSharpCompatible: true, + isSquooshCompatible: true, + }, + }, + }; } interface NetlifyFunctionsOptions { - dist?: URL; - builders?: boolean; - binaryMediaTypes?: string[]; - edgeMiddleware?: boolean; - functionPerRoute?: boolean; + dist?: URL; + builders?: boolean; + binaryMediaTypes?: string[]; + edgeMiddleware?: boolean; + functionPerRoute?: boolean; } function netlifyFunctions({ - dist, - builders, - binaryMediaTypes, - functionPerRoute = false, - edgeMiddleware = false, + dist, + builders, + binaryMediaTypes, + functionPerRoute = false, + edgeMiddleware = false, }: NetlifyFunctionsOptions = {}): AstroIntegration { - let _config: AstroConfig; - let _entryPoints: Map<RouteData, URL>; - let ssrEntryFile: string; - let _middlewareEntryPoint: URL; - return { - name: '@astrojs/netlify', - hooks: { - 'astro:config:setup': ({ config, updateConfig }) => { - const outDir = dist ?? new URL('./dist/', config.root); - updateConfig({ - outDir, - build: { - redirects: false, - client: outDir, - server: new URL('./.netlify/functions-internal/', config.root), - }, - }); - }, - 'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => { - if (middlewareEntryPoint) { - _middlewareEntryPoint = middlewareEntryPoint; - } - _entryPoints = entryPoints; - }, - 'astro:config:done': ({ config, setAdapter }) => { - setAdapter(getAdapter({ binaryMediaTypes, builders, functionPerRoute, edgeMiddleware })); - _config = config; - ssrEntryFile = config.build.serverEntry.replace(/\.m?js/, ''); + let _config: AstroConfig; + let _entryPoints: Map<RouteData, URL>; + let ssrEntryFile: string; + let _middlewareEntryPoint: URL; + return { + name: "@astrojs/netlify", + hooks: { + "astro:config:setup": ({ config, updateConfig }) => { + const outDir = dist ?? new URL("./dist/", config.root); + updateConfig({ + outDir, + build: { + redirects: false, + client: outDir, + server: new URL("./.netlify/functions-internal/", config.root), + }, + }); + }, + "astro:build:ssr": async ({ entryPoints, middlewareEntryPoint }) => { + if (middlewareEntryPoint) { + _middlewareEntryPoint = middlewareEntryPoint; + } + _entryPoints = entryPoints; + }, + "astro:config:done": ({ config, setAdapter }) => { + setAdapter( + getAdapter({ + binaryMediaTypes, + builders, + functionPerRoute, + edgeMiddleware, + }), + ); + _config = config; + ssrEntryFile = config.build.serverEntry.replace(/\.m?js/, ""); - if (config.output === 'static') { - // eslint-disable-next-line no-console - console.warn( - `[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.` - ); - // eslint-disable-next-line no-console - console.warn( - `[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.` - ); - } - }, - 'astro:build:done': async ({ routes, dir }) => { - const functionsConfig = { - version: 1, - config: { - nodeModuleFormat: 'esm', - }, - }; - const functionsConfigPath = join(fileURLToPath(_config.build.server), 'entry.json'); - await writeFile(functionsConfigPath, JSON.stringify(functionsConfig)); + if (config.output === "static") { + // eslint-disable-next-line no-console + console.warn( + `[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`, + ); + // eslint-disable-next-line no-console + console.warn( + `[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.`, + ); + } + }, + "astro:build:done": async ({ routes, dir }) => { + const functionsConfig = { + version: 1, + config: { + nodeModuleFormat: "esm", + }, + }; + const functionsConfigPath = join( + fileURLToPath(_config.build.server), + "entry.json", + ); + await writeFile(functionsConfigPath, JSON.stringify(functionsConfig)); - const type = builders ? 'builders' : 'functions'; - const kind = type ?? 'functions'; + const type = builders ? "builders" : "functions"; + const kind = type ?? "functions"; - if (_entryPoints.size) { - const routeToDynamicTargetMap = new Map(); - for (const [route, entryFile] of _entryPoints) { - const wholeFileUrl = fileURLToPath(entryFile); + if (_entryPoints.size) { + const routeToDynamicTargetMap = new Map(); + for (const [route, entryFile] of _entryPoints) { + const wholeFileUrl = fileURLToPath(entryFile); - const extension = extname(wholeFileUrl); - const relative = wholeFileUrl - .replace(fileURLToPath(_config.build.server), '') - .replace(extension, '') - .replaceAll('\\', '/'); - const dynamicTarget = `/.netlify/${kind}/${relative}`; + const extension = extname(wholeFileUrl); + const relative = wholeFileUrl + .replace(fileURLToPath(_config.build.server), "") + .replace(extension, "") + .replaceAll("\\", "/"); + const dynamicTarget = `/.netlify/${kind}/${relative}`; - routeToDynamicTargetMap.set(route, dynamicTarget); - } - await createRedirects(_config, routeToDynamicTargetMap, dir); - } else { - const dynamicTarget = `/.netlify/${kind}/${ssrEntryFile}`; - const map: [RouteData, string][] = routes.map((route) => { - return [route, dynamicTarget]; - }); - const routeToDynamicTargetMap = new Map(Array.from(map)); + routeToDynamicTargetMap.set(route, dynamicTarget); + } + await createRedirects(_config, routeToDynamicTargetMap, dir); + } else { + const dynamicTarget = `/.netlify/${kind}/${ssrEntryFile}`; + const map: [RouteData, string][] = routes.map((route) => { + return [route, dynamicTarget]; + }); + const routeToDynamicTargetMap = new Map(Array.from(map)); - 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 - ); - } - }, - }, - }; + 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, + ); + } + }, + }, + }; } export { netlifyFunctions as default, netlifyFunctions }; diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts index af2849867..beebdef05 100644 --- a/packages/integrations/netlify/src/integration-static.ts +++ b/packages/integrations/netlify/src/integration-static.ts @@ -1,30 +1,30 @@ -import type { AstroIntegration, RouteData } from 'astro'; -import { createRedirects } from './shared.js'; +import type { AstroIntegration, RouteData } from "astro"; +import { createRedirects } from "./shared.js"; export function netlifyStatic(): AstroIntegration { - let _config: any; - return { - name: '@astrojs/netlify', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - build: { - // Do not output HTML redirects because we are building a `_redirects` file. - redirects: false, - }, - }); - }, - 'astro:config:done': ({ config }) => { - _config = config; - }, - 'astro:build:done': async ({ dir, routes }) => { - const mappedRoutes: [RouteData, string][] = routes.map((route) => [ - route, - `/.netlify/static/`, - ]); - const routesToDynamicTargetMap = new Map(Array.from(mappedRoutes)); - await createRedirects(_config, routesToDynamicTargetMap, dir); - }, - }, - }; + let _config: any; + return { + name: "@astrojs/netlify", + hooks: { + "astro:config:setup": ({ updateConfig }) => { + updateConfig({ + build: { + // Do not output HTML redirects because we are building a `_redirects` file. + redirects: false, + }, + }); + }, + "astro:config:done": ({ config }) => { + _config = config; + }, + "astro:build:done": async ({ dir, routes }) => { + const mappedRoutes: [RouteData, string][] = routes.map((route) => [ + route, + `/.netlify/static/`, + ]); + const routesToDynamicTargetMap = new Map(Array.from(mappedRoutes)); + await createRedirects(_config, routesToDynamicTargetMap, dir); + }, + }, + }; } diff --git a/packages/integrations/netlify/src/middleware.ts b/packages/integrations/netlify/src/middleware.ts index 3c2f4f697..c07dd04fa 100644 --- a/packages/integrations/netlify/src/middleware.ts +++ b/packages/integrations/netlify/src/middleware.ts @@ -1,55 +1,68 @@ -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { ASTRO_LOCALS_HEADER } from './integration-functions.js'; -import { DENO_SHIM } from './shared.js'; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +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 + astroMiddlewareEntryPointPath: URL, + outPath: string, + netlifyEdgeMiddlewareHandlerPath: URL, ): Promise<URL> { - const entryPointPathURLAsString = JSON.stringify( - fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/') - ); + 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); + 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 ` +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'; @@ -61,7 +74,9 @@ export default async function middleware(request, context) { }); ctx.locals = ${handlerTemplateCall}; const next = async () => { - request.headers.set(${JSON.stringify(ASTRO_LOCALS_HEADER)}, trySerializeLocals(ctx.locals)); + request.headers.set(${JSON.stringify( + ASTRO_LOCALS_HEADER, + )}, trySerializeLocals(ctx.locals)); return await context.next(); }; diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index 8c051d9f6..54a9ea7d3 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -1,144 +1,157 @@ -import { builder, type Handler } from '@netlify/functions'; -import type { SSRManifest } from 'astro'; -import { App } from 'astro/app'; -import { applyPolyfills } from 'astro/app/node'; -import { ASTRO_LOCALS_HEADER } from './integration-functions.js'; +import { builder, type Handler } from "@netlify/functions"; +import type { SSRManifest } from "astro"; +import { App } from "astro/app"; +import { applyPolyfills } from "astro/app/node"; +import { ASTRO_LOCALS_HEADER } from "./integration-functions.js"; applyPolyfills(); export interface Args { - builders?: boolean; - binaryMediaTypes?: string[]; - edgeMiddleware: boolean; - functionPerRoute: boolean; + builders?: boolean; + binaryMediaTypes?: string[]; + edgeMiddleware: boolean; + functionPerRoute: boolean; } function parseContentType(header?: string) { - return header?.split(';')[0] ?? ''; + return header?.split(";")[0] ?? ""; } -const clientAddressSymbol = Symbol.for('astro.clientAddress'); +const clientAddressSymbol = Symbol.for("astro.clientAddress"); export const createExports = (manifest: SSRManifest, args: Args) => { - const app = new App(manifest); - - const builders = args.builders ?? false; - const binaryMediaTypes = args.binaryMediaTypes ?? []; - const knownBinaryMediaTypes = new Set([ - 'audio/3gpp', - 'audio/3gpp2', - 'audio/aac', - 'audio/midi', - 'audio/mpeg', - 'audio/ogg', - 'audio/opus', - 'audio/wav', - 'audio/webm', - 'audio/x-midi', - 'image/avif', - 'image/bmp', - 'image/gif', - 'image/vnd.microsoft.icon', - 'image/heif', - 'image/jpeg', - 'image/png', - 'image/svg+xml', - 'image/tiff', - 'image/webp', - 'video/3gpp', - 'video/3gpp2', - 'video/mp2t', - 'video/mp4', - 'video/mpeg', - 'video/ogg', - 'video/x-msvideo', - 'video/webm', - ...binaryMediaTypes, - ]); - - const myHandler: Handler = async (event) => { - const { httpMethod, headers, rawUrl, body: requestBody, isBase64Encoded } = event; - const init: RequestInit = { - method: httpMethod, - headers: new Headers(headers as any), - }; - // Attach the event body the request, with proper encoding. - if (httpMethod !== 'GET' && httpMethod !== 'HEAD') { - const encoding = isBase64Encoded ? 'base64' : 'utf-8'; - init.body = - typeof requestBody === 'string' ? Buffer.from(requestBody, encoding) : requestBody; - } - - const request = new Request(rawUrl, init); - - const routeData = app.match(request); - const ip = headers['x-nf-client-connection-ip']; - Reflect.set(request, clientAddressSymbol, ip); - - let locals: Record<string, unknown> = {}; - - if (request.headers.has(ASTRO_LOCALS_HEADER)) { - let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER); - if (localsAsString) { - locals = JSON.parse(localsAsString); - } - } - - let responseTtl = undefined; - - locals.runtime = builders - ? { - setBuildersTtl(ttl: number) { - responseTtl = ttl; - }, - } - : {}; - - const response: Response = await app.render(request, routeData, locals); - const responseHeaders = Object.fromEntries(response.headers.entries()); - - const responseContentType = parseContentType(responseHeaders['content-type']); - const responseIsBase64Encoded = knownBinaryMediaTypes.has(responseContentType); - - let responseBody: string; - if (responseIsBase64Encoded) { - const ab = await response.arrayBuffer(); - responseBody = Buffer.from(ab).toString('base64'); - } else { - responseBody = await response.text(); - } - - const fnResponse: any = { - statusCode: response.status, - headers: responseHeaders, - body: responseBody, - isBase64Encoded: responseIsBase64Encoded, - ttl: responseTtl, - }; - - const cookies = response.headers.get('set-cookie'); - if (cookies) { - fnResponse.multiValueHeaders = { - 'set-cookie': Array.isArray(cookies) ? cookies : splitCookiesString(cookies), - }; - } - - // Apply cookies set via Astro.cookies.set/delete - if (app.setCookieHeaders) { - const setCookieHeaders = Array.from(app.setCookieHeaders(response)); - fnResponse.multiValueHeaders = fnResponse.multiValueHeaders || {}; - if (!fnResponse.multiValueHeaders['set-cookie']) { - fnResponse.multiValueHeaders['set-cookie'] = []; - } - fnResponse.multiValueHeaders['set-cookie'].push(...setCookieHeaders); - } - - return fnResponse; - }; - - const handler = builders ? builder(myHandler) : myHandler; - - return { handler }; + const app = new App(manifest); + + const builders = args.builders ?? false; + const binaryMediaTypes = args.binaryMediaTypes ?? []; + const knownBinaryMediaTypes = new Set([ + "audio/3gpp", + "audio/3gpp2", + "audio/aac", + "audio/midi", + "audio/mpeg", + "audio/ogg", + "audio/opus", + "audio/wav", + "audio/webm", + "audio/x-midi", + "image/avif", + "image/bmp", + "image/gif", + "image/vnd.microsoft.icon", + "image/heif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/webp", + "video/3gpp", + "video/3gpp2", + "video/mp2t", + "video/mp4", + "video/mpeg", + "video/ogg", + "video/x-msvideo", + "video/webm", + ...binaryMediaTypes, + ]); + + const myHandler: Handler = async (event) => { + const { + httpMethod, + headers, + rawUrl, + body: requestBody, + isBase64Encoded, + } = event; + const init: RequestInit = { + method: httpMethod, + headers: new Headers(headers as any), + }; + // Attach the event body the request, with proper encoding. + if (httpMethod !== "GET" && httpMethod !== "HEAD") { + const encoding = isBase64Encoded ? "base64" : "utf-8"; + init.body = + typeof requestBody === "string" + ? Buffer.from(requestBody, encoding) + : requestBody; + } + + const request = new Request(rawUrl, init); + + const routeData = app.match(request); + const ip = headers["x-nf-client-connection-ip"]; + Reflect.set(request, clientAddressSymbol, ip); + + let locals: Record<string, unknown> = {}; + + if (request.headers.has(ASTRO_LOCALS_HEADER)) { + let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER); + if (localsAsString) { + locals = JSON.parse(localsAsString); + } + } + + let responseTtl = undefined; + + locals.runtime = builders + ? { + setBuildersTtl(ttl: number) { + responseTtl = ttl; + }, + } + : {}; + + const response: Response = await app.render(request, routeData, locals); + const responseHeaders = Object.fromEntries(response.headers.entries()); + + const responseContentType = parseContentType( + responseHeaders["content-type"], + ); + const responseIsBase64Encoded = + knownBinaryMediaTypes.has(responseContentType); + + let responseBody: string; + if (responseIsBase64Encoded) { + const ab = await response.arrayBuffer(); + responseBody = Buffer.from(ab).toString("base64"); + } else { + responseBody = await response.text(); + } + + const fnResponse: any = { + statusCode: response.status, + headers: responseHeaders, + body: responseBody, + isBase64Encoded: responseIsBase64Encoded, + ttl: responseTtl, + }; + + const cookies = response.headers.get("set-cookie"); + if (cookies) { + fnResponse.multiValueHeaders = { + "set-cookie": Array.isArray(cookies) + ? cookies + : splitCookiesString(cookies), + }; + } + + // Apply cookies set via Astro.cookies.set/delete + if (app.setCookieHeaders) { + const setCookieHeaders = Array.from(app.setCookieHeaders(response)); + fnResponse.multiValueHeaders = fnResponse.multiValueHeaders || {}; + if (!fnResponse.multiValueHeaders["set-cookie"]) { + fnResponse.multiValueHeaders["set-cookie"] = []; + } + fnResponse.multiValueHeaders["set-cookie"].push(...setCookieHeaders); + } + + return fnResponse; + }; + + const handler = builders ? builder(myHandler) : myHandler; + + return { handler }; }; /* @@ -152,74 +165,74 @@ export const createExports = (manifest: SSRManifest, args: Args) => { Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation */ function splitCookiesString(cookiesString: string): string[] { - if (Array.isArray(cookiesString)) { - return cookiesString; - } - if (typeof cookiesString !== 'string') { - return []; - } - - let cookiesStrings = []; - let pos = 0; - let start; - let ch; - let lastComma; - let nextStart; - let cookiesSeparatorFound; - - function skipWhitespace() { - while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { - pos += 1; - } - return pos < cookiesString.length; - } - - function notSpecialChar() { - ch = cookiesString.charAt(pos); - - return ch !== '=' && ch !== ';' && ch !== ','; - } - - while (pos < cookiesString.length) { - start = pos; - cookiesSeparatorFound = false; - - while (skipWhitespace()) { - ch = cookiesString.charAt(pos); - if (ch === ',') { - // ',' is a cookie separator if we have later first '=', not ';' or ',' - lastComma = pos; - pos += 1; - - skipWhitespace(); - nextStart = pos; - - while (pos < cookiesString.length && notSpecialChar()) { - pos += 1; - } - - // currently special character - if (pos < cookiesString.length && cookiesString.charAt(pos) === '=') { - // we found cookies separator - cookiesSeparatorFound = true; - // pos is inside the next cookie, so back up and return it. - pos = nextStart; - cookiesStrings.push(cookiesString.substring(start, lastComma)); - start = pos; - } else { - // in param ',' or param separator ';', - // we continue from that comma - pos = lastComma + 1; - } - } else { - pos += 1; - } - } - - if (!cookiesSeparatorFound || pos >= cookiesString.length) { - cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); - } - } - - return cookiesStrings; + if (Array.isArray(cookiesString)) { + return cookiesString; + } + if (typeof cookiesString !== "string") { + return []; + } + + let cookiesStrings = []; + let pos = 0; + let start; + let ch; + let lastComma; + let nextStart; + let cookiesSeparatorFound; + + function skipWhitespace() { + while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { + pos += 1; + } + return pos < cookiesString.length; + } + + function notSpecialChar() { + ch = cookiesString.charAt(pos); + + return ch !== "=" && ch !== ";" && ch !== ","; + } + + while (pos < cookiesString.length) { + start = pos; + cookiesSeparatorFound = false; + + while (skipWhitespace()) { + ch = cookiesString.charAt(pos); + if (ch === ",") { + // ',' is a cookie separator if we have later first '=', not ';' or ',' + lastComma = pos; + pos += 1; + + skipWhitespace(); + nextStart = pos; + + while (pos < cookiesString.length && notSpecialChar()) { + pos += 1; + } + + // currently special character + if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { + // we found cookies separator + cookiesSeparatorFound = true; + // pos is inside the next cookie, so back up and return it. + pos = nextStart; + cookiesStrings.push(cookiesString.substring(start, lastComma)); + start = pos; + } else { + // in param ',' or param separator ';', + // we continue from that comma + pos = lastComma + 1; + } + } else { + pos += 1; + } + } + + if (!cookiesSeparatorFound || pos >= cookiesString.length) { + cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); + } + } + + return cookiesStrings; } diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 175b9d04f..4003f812f 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,9 +1,9 @@ -import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; -import type { AstroConfig, RouteData } from 'astro'; -import esbuild from 'esbuild'; -import fs from 'node:fs'; -import npath from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { createRedirectsFromAstroRoutes } from "@astrojs/underscore-redirects"; +import type { AstroConfig, RouteData } from "astro"; +import esbuild from "esbuild"; +import fs from "node:fs"; +import npath from "node:path"; +import { fileURLToPath } from "node:url"; export const DENO_SHIM = `globalThis.process = { argv: [], @@ -11,104 +11,113 @@ export const DENO_SHIM = `globalThis.process = { };`; export interface NetlifyEdgeFunctionsOptions { - dist?: URL; + dist?: URL; } export interface NetlifyEdgeFunctionManifestFunctionPath { - function: string; - path: string; + function: string; + path: string; } export interface NetlifyEdgeFunctionManifestFunctionPattern { - function: string; - pattern: string; + function: string; + pattern: string; } export type NetlifyEdgeFunctionManifestFunction = - | NetlifyEdgeFunctionManifestFunctionPath - | NetlifyEdgeFunctionManifestFunctionPattern; + | NetlifyEdgeFunctionManifestFunctionPath + | NetlifyEdgeFunctionManifestFunctionPattern; export interface NetlifyEdgeFunctionManifest { - functions: NetlifyEdgeFunctionManifestFunction[]; - version: 1; + functions: NetlifyEdgeFunctionManifestFunction[]; + version: 1; } export async function createRedirects( - config: AstroConfig, - routeToDynamicTargetMap: Map<RouteData, string>, - dir: URL + config: AstroConfig, + routeToDynamicTargetMap: Map<RouteData, string>, + dir: URL, ) { - const _redirectsURL = new URL('./_redirects', dir); + const _redirectsURL = new URL("./_redirects", dir); - const _redirects = createRedirectsFromAstroRoutes({ - config, - routeToDynamicTargetMap, - dir, - }); - const content = _redirects.print(); + const _redirects = createRedirectsFromAstroRoutes({ + config, + routeToDynamicTargetMap, + dir, + }); + const content = _redirects.print(); - // 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, content, 'utf-8'); + // 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, 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(), - }); - } - } +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 manifest: NetlifyEdgeFunctionManifest = { + functions, + version: 1, + }; - const baseDir = new URL('./.netlify/edge-functions/', dir); - await fs.promises.mkdir(baseDir, { recursive: true }); + 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'); + 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, - }, - }); +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 {} - } + // 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 {} + } } |
