diff options
Diffstat (limited to 'packages/integrations/netlify/src')
-rw-r--r-- | packages/integrations/netlify/src/index.ts | 2 | ||||
-rw-r--r-- | packages/integrations/netlify/src/integration-functions.ts | 142 | ||||
-rw-r--r-- | packages/integrations/netlify/src/integration-static.ts | 30 | ||||
-rw-r--r-- | packages/integrations/netlify/src/middleware.ts | 75 | ||||
-rw-r--r-- | packages/integrations/netlify/src/netlify-functions.ts | 225 | ||||
-rw-r--r-- | packages/integrations/netlify/src/shared.ts | 114 |
6 files changed, 0 insertions, 588 deletions
diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts deleted file mode 100644 index a374020f9..000000000 --- a/packages/integrations/netlify/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 33e7bade3..000000000 --- a/packages/integrations/netlify/src/integration-functions.ts +++ /dev/null @@ -1,142 +0,0 @@ -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 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; -} - -function netlifyFunctions({ - 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/, ''); - - if (config.output === 'static') { - console.warn( - `[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.` - ); - console.warn( - `[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.` - ); - } - }, - 'astro:build: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'; - - 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}`; - - 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 - ); - } - }, - }, - }; -} - -export { netlifyFunctions as default, netlifyFunctions }; diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts deleted file mode 100644 index af2849867..000000000 --- a/packages/integrations/netlify/src/integration-static.ts +++ /dev/null @@ -1,30 +0,0 @@ -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); - }, - }, - }; -} diff --git a/packages/integrations/netlify/src/middleware.ts b/packages/integrations/netlify/src/middleware.ts deleted file mode 100644 index 3c2f4f697..000000000 --- a/packages/integrations/netlify/src/middleware.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 -): 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 deleted file mode 100644 index 8c051d9f6..000000000 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ /dev/null @@ -1,225 +0,0 @@ -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; -} - -function parseContentType(header?: string) { - return header?.split(';')[0] ?? ''; -} - -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 }; -}; - -/* - From: https://github.com/nfriedly/set-cookie-parser/blob/5cae030d8ef0f80eec58459e3583d43a07b984cb/lib/set-cookie.js#L144 - Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas - that are within a single set-cookie field-value, such as in the Expires portion. - This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 - Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 - React Native's fetch does this for *every* header, including set-cookie. - Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 - 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; -} diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts deleted file mode 100644 index 175b9d04f..000000000 --- a/packages/integrations/netlify/src/shared.ts +++ /dev/null @@ -1,114 +0,0 @@ -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: [], - 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, - routeToDynamicTargetMap: Map<RouteData, string>, - dir: URL -) { - const _redirectsURL = new URL('./_redirects', dir); - - 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'); -} - -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 {} - } -} |