summaryrefslogtreecommitdiff
path: root/packages/integrations/netlify/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/netlify/src')
-rw-r--r--packages/integrations/netlify/src/env.d.ts1
-rw-r--r--packages/integrations/netlify/src/functions.ts6
-rw-r--r--packages/integrations/netlify/src/image-service.ts57
-rw-r--r--packages/integrations/netlify/src/index.ts313
-rw-r--r--packages/integrations/netlify/src/integration-functions.ts151
-rw-r--r--packages/integrations/netlify/src/integration-static.ts30
-rw-r--r--packages/integrations/netlify/src/middleware.ts75
-rw-r--r--packages/integrations/netlify/src/netlify-functions.ts225
-rw-r--r--packages/integrations/netlify/src/shared.ts114
-rw-r--r--packages/integrations/netlify/src/ssr-function.ts56
-rw-r--r--packages/integrations/netlify/src/static.ts6
-rw-r--r--packages/integrations/netlify/src/types.d.ts1
12 files changed, 437 insertions, 598 deletions
diff --git a/packages/integrations/netlify/src/env.d.ts b/packages/integrations/netlify/src/env.d.ts
deleted file mode 100644
index f964fe0cf..000000000
--- a/packages/integrations/netlify/src/env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-/// <reference types="astro/client" />
diff --git a/packages/integrations/netlify/src/functions.ts b/packages/integrations/netlify/src/functions.ts
new file mode 100644
index 000000000..7a84087af
--- /dev/null
+++ b/packages/integrations/netlify/src/functions.ts
@@ -0,0 +1,6 @@
+import netlifyIntegration, { type NetlifyIntegrationConfig } from "./index.js"
+
+export default function functionsIntegration(config: NetlifyIntegrationConfig) {
+ console.warn("The @astrojs/netlify/functions import is deprecated and will be removed in a future release. Please use @astrojs/netlify instead.")
+ return netlifyIntegration(config)
+} \ No newline at end of file
diff --git a/packages/integrations/netlify/src/image-service.ts b/packages/integrations/netlify/src/image-service.ts
new file mode 100644
index 000000000..385f6996f
--- /dev/null
+++ b/packages/integrations/netlify/src/image-service.ts
@@ -0,0 +1,57 @@
+import type { ExternalImageService, ImageMetadata } from 'astro';
+import { AstroError } from 'astro/errors';
+import { baseService } from 'astro/assets'
+
+const SUPPORTED_FORMATS = ['avif', 'jpg', 'png', 'webp'];
+const QUALITY_NAMES: Record<string, number> = { low: 25, mid: 50, high: 90, max: 100 };
+
+export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
+ return typeof src === 'object';
+}
+
+function removeLeadingForwardSlash(path: string) {
+ return path.startsWith('/') ? path.substring(1) : path;
+}
+
+const service: ExternalImageService = {
+ getURL(options) {
+ const query = new URLSearchParams();
+
+ const fileSrc = isESMImportedImage(options.src)
+ ? removeLeadingForwardSlash(options.src.src)
+ : options.src;
+
+ query.set('url', fileSrc);
+
+ if (options.format) query.set('fm', options.format);
+ if (options.width) query.set('w', '' + options.width);
+ if (options.height) query.set('h', '' + options.height);
+ if (options.quality) query.set('q', '' + options.quality);
+
+ return '/.netlify/images?' + query;
+ },
+ getHTMLAttributes: baseService.getHTMLAttributes,
+ getSrcSet: baseService.getSrcSet,
+ validateOptions(options) {
+ if (options.format && !SUPPORTED_FORMATS.includes(options.format)) {
+ throw new AstroError(
+ `Unsupported image format "${options.format}"`,
+ `Use one of ${SUPPORTED_FORMATS.join(', ')} instead.`
+ );
+ }
+
+ if (options.quality) {
+ options.quality =
+ typeof options.quality === 'string' ? QUALITY_NAMES[options.quality] : options.quality;
+ if (options.quality < 1 || options.quality > 100) {
+ throw new AstroError(
+ `Invalid quality for picture "${options.src}"`,
+ `Quality needs to be between 1 and 100.`
+ );
+ }
+ }
+ return options;
+ },
+};
+
+export default service;
diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts
index a374020f9..09451a2da 100644
--- a/packages/integrations/netlify/src/index.ts
+++ b/packages/integrations/netlify/src/index.ts
@@ -1,2 +1,311 @@
-export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js';
-export { netlifyStatic } from './integration-static.js';
+import type { AstroConfig, AstroIntegration, RouteData } from 'astro';
+import { writeFile, mkdir, appendFile, rm } from 'fs/promises';
+import { fileURLToPath } from 'url';
+import { build } from 'esbuild';
+import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
+import { version as packageVersion } from '../package.json';
+import type { Context } from '@netlify/functions';
+import { AstroError } from 'astro/errors';
+import type { IncomingMessage } from 'http';
+
+export interface NetlifyLocals {
+ netlify: {
+ context: Context;
+ };
+}
+
+const isStaticRedirect = (route: RouteData) =>
+ route.type === 'redirect' && (route.redirect || route.redirectRoute);
+
+const clearDirectory = (dir: URL) => rm(dir, { recursive: true }).catch(() => {});
+
+export interface NetlifyIntegrationConfig {
+ /**
+ * If enabled, On-Demand-Rendered pages are cached for up to a year.
+ * This is useful for pages that are not updated often, like a blog post,
+ * but that you have too many of to pre-render at build time.
+ *
+ * You can override this behavior on a per-page basis
+ * by setting the `Cache-Control`, `CDN-Cache-Control` or `Netlify-CDN-Cache-Control` header
+ * from within the Page:
+ *
+ * ```astro
+ * // src/pages/cached-clock.astro
+ * Astro.response.headers.set('CDN-Cache-Control', "public, max-age=45, must-revalidate");
+ * ---
+ * <p>{Date.now()}</p>
+ * ```
+ */
+ cacheOnDemandPages?: boolean;
+
+ /**
+ * If disabled, Middleware is applied to prerendered pages at build-time, and to on-demand-rendered pages at runtime.
+ * Only disable when your Middleware does not need to run on prerendered pages.
+ * If you use Middleware to implement authentication, redirects or similar things, you should should likely enabled it.
+ *
+ * If enabled, Astro Middleware is deployed as an Edge Function and applies to all routes.
+ * Caveat: Locals set in Middleware are not applied to prerendered pages, because they've been rendered at build-time and are served from the CDN.
+ *
+ * @default disabled
+ */
+ edgeMiddleware?: boolean;
+}
+
+export default function netlifyIntegration(
+ integrationConfig?: NetlifyIntegrationConfig
+): AstroIntegration {
+ const isRunningInNetlify = Boolean(
+ process.env.NETLIFY || process.env.NETLIFY_LOCAL || process.env.NETLIFY_DEV
+ );
+
+ let _config: AstroConfig;
+ let outDir: URL;
+ let rootDir: URL;
+ let astroMiddlewareEntryPoint: URL | undefined = undefined;
+
+ const ssrOutputDir = () => new URL('./.netlify/functions-internal/ssr/', rootDir);
+ const middlewareOutputDir = () => new URL('.netlify/edge-functions/middleware/', rootDir);
+
+ const cleanFunctions = async () =>
+ await Promise.all([clearDirectory(middlewareOutputDir()), clearDirectory(ssrOutputDir())]);
+
+ async function writeRedirects(routes: RouteData[], dir: URL) {
+ const fallback = _config.output === 'static' ? '/.netlify/static' : '/.netlify/functions/ssr';
+ const redirects = createRedirectsFromAstroRoutes({
+ config: _config,
+ dir,
+ routeToDynamicTargetMap: new Map(
+ routes
+ .filter(isStaticRedirect) // all other routes are handled by SSR
+ .map((route) => {
+ // this is needed to support redirects to dynamic routes
+ // on static. not sure why this is needed, but it works.
+ route.distURL ??= route.redirectRoute?.distURL;
+
+ return [route, fallback];
+ })
+ ),
+ });
+
+ if (!redirects.empty()) {
+ await appendFile(new URL('_redirects', outDir), '\n' + redirects.print() + '\n');
+ }
+ }
+
+ async function writeSSRFunction() {
+ await writeFile(
+ new URL('./ssr.mjs', ssrOutputDir()),
+ `
+ import createSSRHandler from './entry.mjs';
+ export default createSSRHandler(${JSON.stringify({
+ cacheOnDemandPages: Boolean(integrationConfig?.cacheOnDemandPages),
+ })});
+ export const config = { name: "Astro SSR", generator: "@astrojs/netlify@${packageVersion}", path: "/*", preferStatic: true };
+ `
+ );
+ }
+
+ async function writeMiddleware(entrypoint: URL) {
+ await mkdir(middlewareOutputDir(), { recursive: true });
+ await writeFile(
+ new URL('./entry.mjs', middlewareOutputDir()),
+ `
+ import { onRequest } from "${fileURLToPath(entrypoint).replaceAll('\\', '/')}";
+ import { createContext, trySerializeLocals } from 'astro/middleware';
+
+ export default async (request, context) => {
+ const ctx = createContext({
+ request,
+ params: {}
+ });
+ ctx.locals = { netlify: { context } }
+ const next = () => {
+ const { netlify, ...otherLocals } = ctx.locals;
+ request.headers.set("x-astro-locals", trySerializeLocals(otherLocals));
+ return context.next();
+ };
+
+ return onRequest(ctx, next);
+ }
+
+ export const config = {
+ name: "Astro Middleware",
+ generator: "@astrojs/netlify@${packageVersion}",
+ path: "/*", excludedPath: ["/_astro/*", "/.netlify/images/*"]
+ };
+ `
+ );
+
+ // taking over bundling, because Netlify bundling trips over NPM modules
+ await build({
+ entryPoints: [fileURLToPath(new URL('./entry.mjs', middlewareOutputDir()))],
+ target: 'es2022',
+ platform: 'neutral',
+ outfile: fileURLToPath(new URL('./middleware.mjs', middlewareOutputDir())),
+ allowOverwrite: true,
+ format: 'esm',
+ bundle: true,
+ minify: false,
+ });
+ }
+
+ function getLocalDevNetlifyContext(req: IncomingMessage): Context {
+ const isHttps = req.headers['x-forwarded-proto'] === 'https';
+ const parseBase64JSON = <T = unknown>(header: string): T | undefined => {
+ if (typeof req.headers[header] === 'string') {
+ try {
+ return JSON.parse(Buffer.from(req.headers[header] as string, 'base64').toString('utf8'));
+ } catch {}
+ }
+ };
+
+ const context: Context = {
+ account: parseBase64JSON('x-nf-account-info') ?? {
+ id: 'mock-netlify-account-id',
+ },
+ deploy: {
+ id:
+ typeof req.headers['x-nf-deploy-id'] === 'string'
+ ? req.headers['x-nf-deploy-id']
+ : 'mock-netlify-deploy-id',
+ },
+ site: parseBase64JSON('x-nf-site-info') ?? {
+ id: 'mock-netlify-site-id',
+ name: 'mock-netlify-site.netlify.app',
+ url: `${isHttps ? 'https' : 'http'}://localhost:${isRunningInNetlify ? 8888 : 4321}`,
+ },
+ geo: parseBase64JSON('x-nf-geo') ?? {
+ city: 'Mock City',
+ country: { code: 'mock', name: 'Mock Country' },
+ subdivision: { code: 'SD', name: 'Mock Subdivision' },
+
+ // @ts-expect-error: these are smhw missing from the Netlify types - fix is on the way
+ timezone: 'UTC',
+ longitude: 0,
+ latitude: 0,
+ },
+ ip:
+ typeof req.headers['x-nf-client-connection-ip'] === 'string'
+ ? req.headers['x-nf-client-connection-ip']
+ : req.socket.remoteAddress ?? '127.0.0.1',
+ server: {
+ region: 'local-dev',
+ },
+ requestId:
+ typeof req.headers['x-nf-request-id'] === 'string'
+ ? req.headers['x-nf-request-id']
+ : 'mock-netlify-request-id',
+ get cookies(): never {
+ throw new Error('Please use Astro.cookies instead.');
+ },
+ json: (input) => Response.json(input),
+ log: console.log,
+ next: () => {
+ throw new Error('`context.next` is not implemented for serverless functions');
+ },
+ get params(): never {
+ throw new Error("context.params don't contain any usable content in Astro.");
+ },
+ rewrite() {
+ throw new Error('context.rewrite is not available in Astro.');
+ },
+ };
+
+ return context;
+ }
+
+ return {
+ name: '@astrojs/netlify',
+ hooks: {
+ 'astro:config:setup': async ({ config, updateConfig }) => {
+ rootDir = config.root;
+ await cleanFunctions();
+
+ outDir = new URL('./dist/', rootDir);
+
+ updateConfig({
+ outDir,
+ build: {
+ redirects: false,
+ client: outDir,
+ server: ssrOutputDir(),
+ },
+ vite: {
+ server: {
+ watch: {
+ ignored: [fileURLToPath(new URL('./.netlify/**', rootDir))],
+ },
+ },
+ },
+ image: {
+ service: {
+ entrypoint: isRunningInNetlify ? '@astrojs/netlify/image-service.js' : undefined,
+ },
+ },
+ });
+ },
+ 'astro:config:done': ({ config, setAdapter }) => {
+ rootDir = config.root;
+ _config = config;
+
+ if (config.image.domains.length || config.image.remotePatterns.length) {
+ throw new AstroError(
+ "config.image.domains and config.image.remotePatterns aren't supported by the Netlify adapter.",
+ 'See https://github.com/withastro/adapters/tree/main/packages/netlify#image-cdn for more.'
+ );
+ }
+
+ setAdapter({
+ name: '@astrojs/netlify',
+ serverEntrypoint: '@astrojs/netlify/ssr-function.js',
+ exports: ['default'],
+ adapterFeatures: {
+ functionPerRoute: false,
+ edgeMiddleware: integrationConfig?.edgeMiddleware ?? false,
+ },
+ supportedAstroFeatures: {
+ hybridOutput: 'stable',
+ staticOutput: 'stable',
+ serverOutput: 'stable',
+ assets: {
+ // keeping this as experimental at least until Netlify Image CDN is out of beta
+ supportKind: 'experimental',
+ // still using Netlify Image CDN instead
+ isSharpCompatible: true,
+ isSquooshCompatible: true,
+ },
+ },
+ });
+ },
+ 'astro:build:ssr': async ({ middlewareEntryPoint }) => {
+ astroMiddlewareEntryPoint = middlewareEntryPoint;
+ },
+ 'astro:build:done': async ({ routes, dir, logger }) => {
+ await writeRedirects(routes, dir);
+ logger.info('Emitted _redirects');
+
+ if (_config.output !== 'static') {
+ await writeSSRFunction();
+ logger.info('Generated SSR Function');
+ }
+
+ if (astroMiddlewareEntryPoint) {
+ await writeMiddleware(astroMiddlewareEntryPoint);
+ logger.info('Generated Middleware Edge Function');
+ }
+ },
+
+ // local dev
+ 'astro:server:setup': async ({ server }) => {
+ server.middlewares.use((req, res, next) => {
+ const locals = Symbol.for('astro.locals');
+ Reflect.set(req, locals, {
+ ...Reflect.get(req, locals),
+ netlify: { context: getLocalDevNetlifyContext(req) },
+ });
+ next();
+ });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts
deleted file mode 100644
index 8812a8e89..000000000
--- a/packages/integrations/netlify/src/integration-functions.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { writeFile } from 'node:fs/promises';
-import { extname, join } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
-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') {
- // 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';
-
- 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 9c9d8ad3a..000000000
--- a/packages/integrations/netlify/src/netlify-functions.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import { type Handler, builder } 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 fca3d5f0c..000000000
--- a/packages/integrations/netlify/src/shared.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-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';
-
-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 {}
- }
-}
diff --git a/packages/integrations/netlify/src/ssr-function.ts b/packages/integrations/netlify/src/ssr-function.ts
new file mode 100644
index 000000000..c2b6ed14c
--- /dev/null
+++ b/packages/integrations/netlify/src/ssr-function.ts
@@ -0,0 +1,56 @@
+import type { Context } from '@netlify/functions';
+import type { SSRManifest } from 'astro';
+import { App } from 'astro/app';
+import { applyPolyfills } from 'astro/app/node';
+
+applyPolyfills();
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface Args {}
+
+const clientAddressSymbol = Symbol.for('astro.clientAddress');
+
+export const createExports = (manifest: SSRManifest, _args: Args) => {
+ const app = new App(manifest);
+
+ function createHandler(integrationConfig: { cacheOnDemandPages: boolean }) {
+ return async function handler(request: Request, context: Context) {
+ const routeData = app.match(request);
+ Reflect.set(request, clientAddressSymbol, context.ip);
+
+ let locals: Record<string, unknown> = {};
+
+ if (request.headers.has('x-astro-locals')) {
+ locals = JSON.parse(request.headers.get('x-astro-locals')!);
+ }
+
+ locals.netlify = { context };
+
+ const response = await app.render(request, routeData, locals);
+
+ if (app.setCookieHeaders) {
+ for (const setCookieHeader of app.setCookieHeaders(response)) {
+ response.headers.append('Set-Cookie', setCookieHeader);
+ }
+ }
+
+ if (integrationConfig.cacheOnDemandPages) {
+ // any user-provided Cache-Control headers take precedence
+ const hasCacheControl = [
+ 'Cache-Control',
+ 'CDN-Cache-Control',
+ 'Netlify-CDN-Cache-Control',
+ ].some((header) => response.headers.has(header));
+
+ if (!hasCacheControl) {
+ // caches this page for up to a year
+ response.headers.append('CDN-Cache-Control', 'public, max-age=31536000, must-revalidate');
+ }
+ }
+
+ return response;
+ };
+ }
+
+ return { default: createHandler };
+};
diff --git a/packages/integrations/netlify/src/static.ts b/packages/integrations/netlify/src/static.ts
new file mode 100644
index 000000000..4748f384a
--- /dev/null
+++ b/packages/integrations/netlify/src/static.ts
@@ -0,0 +1,6 @@
+import netlifyIntegration from "./index.js"
+
+export default function staticIntegration() {
+ console.warn("The @astrojs/netlify/static import is deprecated and will be removed in a future release. Please use @astrojs/netlify instead.")
+ return netlifyIntegration()
+} \ No newline at end of file
diff --git a/packages/integrations/netlify/src/types.d.ts b/packages/integrations/netlify/src/types.d.ts
new file mode 100644
index 000000000..0df35e9e9
--- /dev/null
+++ b/packages/integrations/netlify/src/types.d.ts
@@ -0,0 +1 @@
+declare module "*.json"; \ No newline at end of file