@fleek-platform/next-on-fleek
Version:
`@fleek-platform/next-on-fleek` is a CLI tool that you can use to build and develop [Next.js](https://nextjs.org/) applications so that they can run on [Fleek Functions](https://fleek.xyz/docs/platform/fleek-functions/).
178 lines (150 loc) • 5.57 kB
text/typescript
import { applyHeaders, createMutableResponse } from './http';
/**
* Checks whether the given URL matches the given remote pattern from the Vercel build output
* images configuration.
*
* https://vercel.com/docs/build-output-api/v3/configuration#images
*
* @param url URL to check.
* @param pattern Remote pattern to match against.
* @returns Whether the URL matches the remote pattern.
*/
export function isRemotePatternMatch(
url: URL,
{ protocol, hostname, port, pathname }: VercelImageRemotePattern,
): boolean {
// Protocol must match if defined.
if (protocol && url.protocol.replace(/:$/, '') !== protocol) return false;
// Hostname must match regexp.
if (!new RegExp(hostname).test(url.hostname)) return false;
// Port must match regexp if defined.
if (port && !new RegExp(port).test(url.port)) return false;
// Pathname must match regexp if defined.
if (pathname && !new RegExp(pathname).test(url.pathname)) return false;
// All checks passed.
return true;
}
type ResizingProperties = {
isRelative: boolean;
imageUrl: URL;
options: RequestInitCfPropertiesImage;
};
/**
* Derives the properties to use for image resizing from the incoming request, respecting the
* images configuration spec from the Vercel build output config.
*
* https://vercel.com/docs/build-output-api/v3/configuration#images
*
* @param request Incoming request.
* @param config Images configuration from the Vercel build output.
* @returns Resizing properties if the request is valid, otherwise undefined.
*/
export function getResizingProperties(
request: Request,
config?: VercelImagesConfig,
): ResizingProperties | undefined {
if (request.method !== 'GET') return undefined;
const { origin, searchParams } = new URL(request.url);
const rawUrl = searchParams.get('url');
const width = Number.parseInt(searchParams.get('w') ?? '', 10);
// 75 is the default quality - https://nextjs.org/docs/app/api-reference/components/image#quality
const quality = Number.parseInt(searchParams.get('q') ?? '75', 10);
if (!rawUrl || Number.isNaN(width) || Number.isNaN(quality)) return undefined;
if (!config?.sizes?.includes(width)) return undefined;
if (quality < 0 || quality > 100) return undefined;
const url = new URL(rawUrl, origin);
// SVGs must be allowed by the config.
if (url.pathname.endsWith('.svg') && !config?.dangerouslyAllowSVG) {
return undefined;
}
const isProtocolRelative = rawUrl.startsWith('//');
const isRelative = rawUrl.startsWith('/') && !isProtocolRelative;
if (
// Relative URL means same origin as deployment and is allowed.
!isRelative &&
// External image URL must be allowed by domains or remote patterns.
!config?.domains?.includes(url.hostname) &&
!config?.remotePatterns?.find(pattern => isRemotePatternMatch(url, pattern))
) {
return undefined;
}
const acceptHeader = request.headers.get('Accept') ?? '';
const format = config?.formats
?.find(format => acceptHeader.includes(format))
?.replace('image/', '') as VercelImageFormatWithoutPrefix | undefined;
return {
isRelative,
imageUrl: url,
options: { width, quality, format },
};
}
/**
* Formats the given response to match the images configuration spec from the Vercel build output
* config.
*
* Applies headers for `Content-Security-Policy` and `Content-Disposition`, if defined in the config.
*
* https://vercel.com/docs/build-output-api/v3/configuration#images
*
* @param resp Response to format.
* @param imageUrl Image URL that was resized.
* @param config Images configuration from the Vercel build output.
* @returns Formatted response.
*/
export function formatResp(
resp: Response,
imageUrl: URL,
config?: VercelImagesConfig,
): Response {
const newHeaders = new Headers();
if (config?.contentSecurityPolicy) {
newHeaders.set('Content-Security-Policy', config.contentSecurityPolicy);
}
if (config?.contentDispositionType) {
const fileName = imageUrl.pathname.split('/').pop();
const contentDisposition = fileName
? `${config.contentDispositionType}; filename="${fileName}"`
: config.contentDispositionType;
newHeaders.set('Content-Disposition', contentDisposition);
}
if (!resp.headers.has('Cache-Control')) {
// Fall back to the minimumCacheTTL value if there is no Cache-Control header.
// https://vercel.com/docs/concepts/image-optimization#caching
newHeaders.set(
'Cache-Control',
`public, max-age=${config?.minimumCacheTTL ?? 60}`,
);
}
const mutableResponse = createMutableResponse(resp);
applyHeaders(mutableResponse.headers, newHeaders);
return mutableResponse;
}
/**
* Handles image resizing requests.
*
* @param request Incoming request.
* @param config Images configuration from the Vercel build output.
* @returns Resized image response if the request is valid, otherwise a 400 response.
*/
export async function handleImageResizingRequest(
request: Request,
{ buildOutput, assetsFetcher, imagesConfig }: ImageResizingOpts,
): Promise<Response> {
const opts = getResizingProperties(request, imagesConfig);
if (!opts) {
return new Response('Invalid image resizing request', { status: 400 });
}
const { isRelative, imageUrl } = opts;
// TODO: implement proper image resizing
const imgFetch =
isRelative && imageUrl.pathname in buildOutput
? assetsFetcher.fetch.bind(assetsFetcher)
: fetch;
const imageResp = await imgFetch(imageUrl);
return formatResp(imageResp, imageUrl, imagesConfig);
}
type ImageResizingOpts = {
buildOutput: VercelBuildOutput;
assetsFetcher: Fetcher;
imagesConfig?: VercelImagesConfig;
};