diff options
Diffstat (limited to 'packages/integrations/vercel/src')
-rw-r--r-- | packages/integrations/vercel/src/image/build-service.ts | 64 | ||||
-rw-r--r-- | packages/integrations/vercel/src/image/dev-service.ts | 31 | ||||
-rw-r--r-- | packages/integrations/vercel/src/image/shared-dev-service.ts | 35 | ||||
-rw-r--r-- | packages/integrations/vercel/src/image/shared.ts | 163 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/nft.ts | 85 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/prerender.ts | 5 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/redirects.ts | 144 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/speed-insights.ts | 29 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/web-analytics.ts | 30 | ||||
-rw-r--r-- | packages/integrations/vercel/src/serverless/adapter.ts | 562 | ||||
-rw-r--r-- | packages/integrations/vercel/src/serverless/entrypoint.ts | 58 | ||||
-rw-r--r-- | packages/integrations/vercel/src/serverless/middleware.ts | 124 | ||||
-rw-r--r-- | packages/integrations/vercel/src/speed-insights.ts | 65 | ||||
-rw-r--r-- | packages/integrations/vercel/src/static/adapter.ts | 155 | ||||
-rw-r--r-- | packages/integrations/vercel/src/types.d.ts | 3 |
15 files changed, 0 insertions, 1553 deletions
diff --git a/packages/integrations/vercel/src/image/build-service.ts b/packages/integrations/vercel/src/image/build-service.ts deleted file mode 100644 index e793b896e..000000000 --- a/packages/integrations/vercel/src/image/build-service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { ExternalImageService } from 'astro'; -import { baseService } from 'astro/assets'; -import { isESMImportedImage, sharedValidateOptions } from './shared.js'; - -const service: ExternalImageService = { - ...baseService, - validateOptions: (options, serviceOptions) => - sharedValidateOptions(options, serviceOptions.service.config, 'production'), - getHTMLAttributes(options) { - const { inputtedWidth, ...props } = options; - - // If `validateOptions` returned a different width than the one of the image, use it for attributes - if (inputtedWidth) { - props.width = inputtedWidth; - } - - let targetWidth = props.width; - let targetHeight = props.height; - if (isESMImportedImage(props.src)) { - const aspectRatio = props.src.width / props.src.height; - if (targetHeight && !targetWidth) { - // If we have a height but no width, use height to calculate the width - targetWidth = Math.round(targetHeight * aspectRatio); - } else if (targetWidth && !targetHeight) { - // If we have a width but no height, use width to calculate the height - targetHeight = Math.round(targetWidth / aspectRatio); - } else if (!targetWidth && !targetHeight) { - // If we have neither width or height, use the original image's dimensions - targetWidth = props.src.width; - targetHeight = props.src.height; - } - } - - const { src, width, height, format, quality, densities, widths, formats, ...attributes } = - options; - - return { - ...attributes, - width: targetWidth, - height: targetHeight, - loading: attributes.loading ?? 'lazy', - decoding: attributes.decoding ?? 'async', - }; - }, - getURL(options) { - const fileSrc = isESMImportedImage(options.src) - ? removeLeadingForwardSlash(options.src.src) - : options.src; - - const searchParams = new URLSearchParams(); - searchParams.append('url', fileSrc); - - options.width && searchParams.append('w', options.width.toString()); - options.quality && searchParams.append('q', options.quality.toString()); - - return '/_vercel/image?' + searchParams; - }, -}; - -function removeLeadingForwardSlash(path: string) { - return path.startsWith('/') ? path.substring(1) : path; -} - -export default service; diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts deleted file mode 100644 index c9702cff9..000000000 --- a/packages/integrations/vercel/src/image/dev-service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { LocalImageService } from 'astro'; -import sharpService from 'astro/assets/services/sharp'; -import { baseDevService } from './shared-dev-service.js'; - -const service: LocalImageService = { - ...baseDevService, - getHTMLAttributes(options, serviceOptions) { - const { inputtedWidth, ...props } = options; - - // If `validateOptions` returned a different width than the one of the image, use it for attributes - if (inputtedWidth) { - props.width = inputtedWidth; - } - - return sharpService.getHTMLAttributes - ? sharpService.getHTMLAttributes(props, serviceOptions) - : {}; - }, - transform(inputBuffer, transform, serviceOptions) { - // NOTE: Hardcoding webp here isn't accurate to how the Vercel Image Optimization API works, normally what we should - // do is setup a custom endpoint that sniff the user's accept-content header and serve the proper format based on the - // user's Vercel config. However, that's: a lot of work for: not much. The dev service is inaccurate to the prod service - // in many more ways, this is one of the less offending cases and is, imo, okay, erika - 2023-04-27 - transform.format = transform.src.endsWith('svg') ? 'svg' : 'webp'; - - // The base sharp service works the same way as the Vercel Image Optimization API, so it's a safe fallback in local - return sharpService.transform(inputBuffer, transform, serviceOptions); - }, -}; - -export default service; diff --git a/packages/integrations/vercel/src/image/shared-dev-service.ts b/packages/integrations/vercel/src/image/shared-dev-service.ts deleted file mode 100644 index 8ca87e99a..000000000 --- a/packages/integrations/vercel/src/image/shared-dev-service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { LocalImageService } from 'astro'; -import { baseService } from 'astro/assets'; -import { sharedValidateOptions } from './shared.js'; - -export const baseDevService: Omit<LocalImageService, 'transform'> = { - ...baseService, - validateOptions: (options, serviceOptions) => - sharedValidateOptions(options, serviceOptions.service.config, 'development'), - getURL(options) { - const fileSrc = typeof options.src === 'string' ? options.src : options.src.src; - - const searchParams = new URLSearchParams(); - searchParams.append('href', fileSrc); - - options.width && searchParams.append('w', options.width.toString()); - options.quality && searchParams.append('q', options.quality.toString()); - - return '/_image?' + searchParams; - }, - parseURL(url) { - const params = url.searchParams; - - if (!params.has('href')) { - return undefined; - } - - const transform = { - src: params.get('href')!, - width: params.has('w') ? parseInt(params.get('w')!) : undefined, - quality: params.get('q'), - }; - - return transform; - }, -}; diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts deleted file mode 100644 index 0a2575985..000000000 --- a/packages/integrations/vercel/src/image/shared.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { AstroConfig, ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro'; - -export function getDefaultImageConfig(astroImageConfig: AstroConfig['image']): VercelImageConfig { - return { - sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], - domains: astroImageConfig.domains ?? [], - // Cast is necessary here because Vercel's types are slightly different from ours regarding allowed protocols. Behavior should be the same, however. - remotePatterns: (astroImageConfig.remotePatterns as VercelImageConfig['remotePatterns']) ?? [], - }; -} - -export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata { - return typeof src === 'object'; -} - -export type DevImageService = 'sharp' | (string & {}); - -// https://vercel.com/docs/build-output-api/v3/configuration#images -type ImageFormat = 'image/avif' | 'image/webp'; - -type RemotePattern = { - protocol?: 'http' | 'https'; - hostname: string; - port?: string; - pathname?: string; -}; - -export type VercelImageConfig = { - /** - * Supported image widths. - */ - sizes: number[]; - /** - * Allowed external domains that can use Image Optimization. Leave empty for only allowing the deployment domain to use Image Optimization. - */ - domains: string[]; - /** - * Allowed external patterns that can use Image Optimization. Similar to `domains` but provides more control with RegExp. - */ - remotePatterns?: RemotePattern[]; - /** - * Cache duration (in seconds) for the optimized images. - */ - minimumCacheTTL?: number; - /** - * Supported output image formats - */ - formats?: ImageFormat[]; - /** - * Allow SVG input image URLs. This is disabled by default for security purposes. - */ - dangerouslyAllowSVG?: boolean; - /** - * Change the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) of the optimized images. - */ - contentSecurityPolicy?: string; -}; - -export const qualityTable: Record<ImageQualityPreset, number> = { - low: 25, - mid: 50, - high: 80, - max: 100, -}; - -export function getAstroImageConfig( - images: boolean | undefined, - imagesConfig: VercelImageConfig | undefined, - command: string, - devImageService: DevImageService, - astroImageConfig: AstroConfig['image'], -) { - let devService = '@astrojs/vercel/dev-image-service'; - - switch (devImageService) { - case 'sharp': - devService = '@astrojs/vercel/dev-image-service'; - break; - default: - if (typeof devImageService === 'string') { - devService = devImageService; - } else { - devService = '@astrojs/vercel/dev-image-service'; - } - break; - } - - if (images) { - return { - image: { - service: { - entrypoint: command === 'dev' ? devService : '@astrojs/vercel/build-image-service', - config: imagesConfig ? imagesConfig : getDefaultImageConfig(astroImageConfig), - }, - }, - }; - } - - return {}; -} - -export function sharedValidateOptions( - options: ImageTransform, - serviceConfig: Record<string, any>, - mode: 'development' | 'production', -) { - const vercelImageOptions = serviceConfig as VercelImageConfig; - - if ( - mode === 'development' && - (!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0) - ) { - throw new Error('Vercel Image Optimization requires at least one size to be configured.'); - } - - const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b); - - // The logic for finding the perfect width is a bit confusing, here it goes: - // For images where no width has been specified: - // - For local, imported images, fallback to nearest width we can find in our configured - // - For remote images, that's an error, width is always required. - // For images where a width has been specified: - // - If the width that the user asked for isn't in `sizes`, then fallback to the nearest one, but save the width - // the user asked for so we can put it on the `img` tag later. - // - Otherwise, just use as-is. - // The end goal is: - // - The size on the page is always the one the user asked for or the base image's size - // - The actual size of the image file is always one of `sizes`, either the one the user asked for or the nearest to it - if (!options.width) { - const src = options.src; - if (isESMImportedImage(src)) { - const nearestWidth = configuredWidths.reduce((prev, curr) => { - return Math.abs(curr - src.width) < Math.abs(prev - src.width) ? curr : prev; - }); - - // Use the image's base width to inform the `width` and `height` on the `img` tag - options.inputtedWidth = src.width; - options.width = nearestWidth; - } else { - throw new Error(`Missing \`width\` parameter for remote image ${options.src}`); - } - } else { - if (!configuredWidths.includes(options.width)) { - const nearestWidth = configuredWidths.reduce((prev, curr) => { - return Math.abs(curr - options.width!) < Math.abs(prev - options.width!) ? curr : prev; - }); - - // Save the width the user asked for to inform the `width` and `height` on the `img` tag - options.inputtedWidth = options.width; - options.width = nearestWidth; - } - } - - if (options.quality && typeof options.quality === 'string') { - options.quality = options.quality in qualityTable ? qualityTable[options.quality] : undefined; - } - - if (!options.quality) { - options.quality = 100; - } - - return options; -} diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts deleted file mode 100644 index 7f21f3f27..000000000 --- a/packages/integrations/vercel/src/lib/nft.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { relative as relativePath } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { copyFilesToFolder } from '@astrojs/internal-helpers/fs'; -import type { AstroIntegrationLogger } from 'astro'; - -export async function copyDependenciesToFunction( - { - entry, - outDir, - includeFiles, - excludeFiles, - logger, - }: { - entry: URL; - outDir: URL; - includeFiles: URL[]; - excludeFiles: URL[]; - logger: AstroIntegrationLogger; - }, - // we want to pass the caching by reference, and not by value - cache: object, -): Promise<{ handler: string }> { - const entryPath = fileURLToPath(entry); - logger.info(`Bundling function ${relativePath(fileURLToPath(outDir), entryPath)}`); - - // Get root of folder of the system (like C:\ on Windows or / on Linux) - let base = entry; - while (fileURLToPath(base) !== fileURLToPath(new URL('../', base))) { - base = new URL('../', base); - } - - // The Vite bundle includes an import to `@vercel/nft` for some reason, - // and that trips up `@vercel/nft` itself during the adapter build. Using a - // dynamic import helps prevent the issue. - // TODO: investigate why - const { nodeFileTrace } = await import('@vercel/nft'); - const result = await nodeFileTrace([entryPath], { - base: fileURLToPath(base), - // If you have a route of /dev this appears in source and NFT will try to - // scan your local /dev :8 - ignore: ['/dev/**'], - cache, - }); - - for (const error of result.warnings) { - if (error.message.startsWith('Failed to resolve dependency')) { - const [, module, file] = /Cannot find module '(.+?)' loaded from (.+)/.exec(error.message)!; - - // The import(astroRemark) sometimes fails to resolve, but it's not a problem - if (module === '@astrojs/') continue; - - // Sharp is always external and won't be able to be resolved, but that's also not a problem - if (module === 'sharp') continue; - - if (entryPath === file) { - logger.debug( - `[@astrojs/vercel] The module "${module}" couldn't be resolved. This may not be a problem, but it's worth checking.`, - ); - } else { - logger.debug( - `[@astrojs/vercel] The module "${module}" inside the file "${file}" couldn't be resolved. This may not be a problem, but it's worth checking.`, - ); - } - } - // parse errors are likely not js and can safely be ignored, - // such as this html file in "main" meant for nw instead of node: - // https://github.com/vercel/nft/issues/311 - else if (error.message.startsWith('Failed to parse')) { - continue; - } else { - throw error; - } - } - - const commonAncestor = await copyFilesToFolder( - [...result.fileList].map((file) => new URL(file, base)).concat(includeFiles), - outDir, - excludeFiles, - ); - - return { - // serverEntry location inside the outDir - handler: relativePath(commonAncestor, entryPath), - }; -} diff --git a/packages/integrations/vercel/src/lib/prerender.ts b/packages/integrations/vercel/src/lib/prerender.ts deleted file mode 100644 index f69f3b5d4..000000000 --- a/packages/integrations/vercel/src/lib/prerender.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { AstroConfig } from 'astro'; - -export function isServerLikeOutput(config: AstroConfig) { - return config.output === 'server' || config.output === 'hybrid'; -} diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts deleted file mode 100644 index 1e476cb1f..000000000 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ /dev/null @@ -1,144 +0,0 @@ -import nodePath from 'node:path'; -import { appendForwardSlash, removeLeadingForwardSlash } from '@astrojs/internal-helpers/path'; -import type { AstroConfig, RouteData, RoutePart } from 'astro'; - -const pathJoin = nodePath.posix.join; - -// https://vercel.com/docs/project-configuration#legacy/routes -interface VercelRoute { - src: string; - methods?: string[]; - dest?: string; - headers?: Record<string, string>; - status?: number; - continue?: boolean; -} - -// Copied from astro/packages/astro/src/core/routing/manifest/create.ts -// Disable eslint as we're not sure how to improve this regex yet -// eslint-disable-next-line regexp/no-super-linear-backtracking -const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/; -const ROUTE_SPREAD = /^\.{3}.+$/; -function getParts(part: string, file: string) { - const result: RoutePart[] = []; - part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => { - if (!str) return; - const dynamic = i % 2 === 1; - - const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str]; - - if (!content || (dynamic && !/^(?:\.\.\.)?[\w$]+$/.test(content))) { - throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`); - } - - result.push({ - content, - dynamic, - spread: dynamic && ROUTE_SPREAD.test(content), - }); - }); - - return result; -} - -// Copied from /home/juanm04/dev/misc/astro/packages/astro/src/core/routing/manifest/create.ts -// 2022-04-26 -function getMatchPattern(segments: RoutePart[][]) { - return segments - .map((segment) => { - return segment[0].spread - ? '(?:\\/(.*?))?' - : segment - .map((part) => { - if (part) - return part.dynamic - ? '([^/]+?)' - : part.content - .normalize() - .replace(/\?/g, '%3F') - .replace(/#/g, '%23') - .replace(/%5B/g, '[') - .replace(/%5D/g, ']') - .replace(/[*+?^${}()|[\]\\]/g, '\\$&'); - }) - .join(''); - }) - .join('/'); -} - -function getReplacePattern(segments: RoutePart[][]) { - let n = 0; - let result = ''; - - for (const segment of segments) { - for (const part of segment) { - if (part.dynamic) result += '$' + ++n; - else result += part.content; - } - result += '/'; - } - - // Remove trailing slash - result = result.slice(0, -1); - - return result; -} - -function getRedirectLocation(route: RouteData, config: AstroConfig): string { - if (route.redirectRoute) { - const pattern = getReplacePattern(route.redirectRoute.segments); - const path = config.trailingSlash === 'always' ? appendForwardSlash(pattern) : pattern; - return pathJoin(config.base, path); - } else if (typeof route.redirect === 'object') { - return pathJoin(config.base, route.redirect.destination); - } else { - return pathJoin(config.base, route.redirect || ''); - } -} - -function getRedirectStatus(route: RouteData): number { - if (typeof route.redirect === 'object') { - return route.redirect.status; - } - return 301; -} - -export function escapeRegex(content: string) { - const segments = removeLeadingForwardSlash(content) - .split(nodePath.posix.sep) - .filter(Boolean) - .map((s: string) => { - return getParts(s, content); - }); - return `^/${getMatchPattern(segments)}$`; -} - -export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] { - let redirects: VercelRoute[] = []; - - for (const route of routes) { - if (route.type === 'redirect') { - redirects.push({ - src: config.base + getMatchPattern(route.segments), - headers: { Location: getRedirectLocation(route, config) }, - status: getRedirectStatus(route), - }); - } else if (route.type === 'page' && route.route !== '/') { - if (config.trailingSlash === 'always') { - redirects.push({ - src: config.base + getMatchPattern(route.segments), - headers: { Location: config.base + getReplacePattern(route.segments) + '/' }, - status: 308, - }); - } else if (config.trailingSlash === 'never') { - redirects.push({ - src: config.base + getMatchPattern(route.segments) + '/', - headers: { Location: config.base + getReplacePattern(route.segments) }, - status: 308, - }); - } - } - } - - return redirects; -} diff --git a/packages/integrations/vercel/src/lib/speed-insights.ts b/packages/integrations/vercel/src/lib/speed-insights.ts deleted file mode 100644 index 8e3639536..000000000 --- a/packages/integrations/vercel/src/lib/speed-insights.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type VercelSpeedInsightsConfig = { - enabled: boolean; -}; - -export function getSpeedInsightsViteConfig(enabled?: boolean) { - if (enabled) { - return { - define: exposeEnv(['VERCEL_ANALYTICS_ID']), - }; - } - - return {}; -} - -/** - * While Vercel adds the `PUBLIC_` prefix for their `VERCEL_` env vars by default, some env vars - * like `VERCEL_ANALYTICS_ID` aren't, so handle them here so that it works correctly in runtime. - */ -export function exposeEnv(envs: string[]): Record<string, unknown> { - const mapped: Record<string, unknown> = {}; - - envs - .filter((env) => process.env[env]) - .forEach((env) => { - mapped[`import.meta.env.PUBLIC_${env}`] = JSON.stringify(process.env[env]); - }); - - return mapped; -} diff --git a/packages/integrations/vercel/src/lib/web-analytics.ts b/packages/integrations/vercel/src/lib/web-analytics.ts deleted file mode 100644 index d6ee4d78d..000000000 --- a/packages/integrations/vercel/src/lib/web-analytics.ts +++ /dev/null @@ -1,30 +0,0 @@ -export type VercelWebAnalyticsConfig = { - enabled: boolean; -}; - -export async function getInjectableWebAnalyticsContent({ - mode, -}: { - mode: 'development' | 'production'; -}) { - const base = `window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };`; - - if (mode === 'development') { - return ` - ${base} - var script = document.createElement('script'); - script.defer = true; - script.src = 'https://cdn.vercel-insights.com/v1/script.debug.js'; - var head = document.querySelector('head'); - head.appendChild(script); - `; - } - - return `${base} - var script = document.createElement('script'); - script.defer = true; - script.src = '/_vercel/insights/script.js'; - var head = document.querySelector('head'); - head.appendChild(script); - `; -} diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts deleted file mode 100644 index 3b86cec0e..000000000 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ /dev/null @@ -1,562 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { basename } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { removeDir, writeJson } from '@astrojs/internal-helpers/fs'; -import type { - AstroAdapter, - AstroConfig, - AstroIntegration, - AstroIntegrationLogger, - RouteData, -} from 'astro'; -import { AstroError } from 'astro/errors'; -import glob from 'fast-glob'; -import { - type DevImageService, - type VercelImageConfig, - getAstroImageConfig, - getDefaultImageConfig, -} from '../image/shared.js'; -import { copyDependenciesToFunction } from '../lib/nft.js'; -import { escapeRegex, getRedirects } from '../lib/redirects.js'; -import { - type VercelSpeedInsightsConfig, - getSpeedInsightsViteConfig, -} from '../lib/speed-insights.js'; -import { - type VercelWebAnalyticsConfig, - getInjectableWebAnalyticsContent, -} from '../lib/web-analytics.js'; -import { generateEdgeMiddleware } from './middleware.js'; - -const PACKAGE_NAME = '@astrojs/vercel/serverless'; - -/** - * The edge function calls the node server at /_render, - * with the original path as the value of this header. - */ -export const ASTRO_PATH_HEADER = 'x-astro-path'; -export const ASTRO_PATH_PARAM = 'x_astro_path'; - -/** - * The edge function calls the node server at /_render, - * with the locals serialized into this header. - */ -export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; -export const ASTRO_MIDDLEWARE_SECRET_HEADER = 'x-astro-middleware-secret'; -export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware'; - -// Vercel routes the folder names to a path on the deployed website. -// We attempt to avoid interfering by prefixing with an underscore. -export const NODE_PATH = '_render'; -const MIDDLEWARE_PATH = '_middleware'; - -// This isn't documented by vercel anywhere, but unlike serverless -// and edge functions, isr functions are not passed the original path. -// Instead, we have to use $0 to refer to the regex match from "src". -const ISR_PATH = `/_isr?${ASTRO_PATH_PARAM}=$0`; - -// https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version -const SUPPORTED_NODE_VERSIONS: Record< - string, - | { status: 'default' } - | { status: 'beta' } - | { status: 'retiring'; removal: Date | string; warnDate: Date } - | { status: 'deprecated'; removal: Date } -> = { - 18: { status: 'retiring', removal: 'Early 2025', warnDate: new Date('October 1 2024') }, - 20: { status: 'default' }, -}; - -function getAdapter({ - edgeMiddleware, - middlewareSecret, - skewProtection, -}: { - edgeMiddleware: boolean; - middlewareSecret: string; - skewProtection: boolean; -}): AstroAdapter { - return { - name: PACKAGE_NAME, - serverEntrypoint: `${PACKAGE_NAME}/entrypoint`, - exports: ['default'], - args: { middlewareSecret, skewProtection }, - adapterFeatures: { - edgeMiddleware, - }, - supportedAstroFeatures: { - hybridOutput: 'stable', - staticOutput: 'stable', - serverOutput: 'stable', - assets: { - supportKind: 'stable', - isSharpCompatible: true, - }, - i18nDomains: 'experimental', - envGetSecret: 'stable', - }, - }; -} - -export interface VercelServerlessConfig { - /** Configuration for [Vercel Web Analytics](https://vercel.com/docs/concepts/analytics). */ - webAnalytics?: VercelWebAnalyticsConfig; - - /** - * @deprecated This option lets you configure the legacy speed insights API which is now deprecated by Vercel. - * - * See [Vercel Speed Insights Quickstart](https://vercel.com/docs/speed-insights/quickstart) for instructions on how to use the library instead. - * - * https://vercel.com/docs/speed-insights/quickstart - */ - speedInsights?: VercelSpeedInsightsConfig; - - /** Force files to be bundled with your function. This is helpful when you notice missing files. */ - includeFiles?: string[]; - - /** Exclude any files from the bundling process that would otherwise be included. */ - excludeFiles?: string[]; - - /** When enabled, an Image Service powered by the Vercel Image Optimization API will be automatically configured and used in production. In development, the image service specified by devImageService will be used instead. */ - imageService?: boolean; - - /** Configuration options for [Vercel’s Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel’s image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters. */ - imagesConfig?: VercelImageConfig; - - /** Allows you to configure which image service to use in development when imageService is enabled. */ - devImageService?: DevImageService; - - /** Whether to create the Vercel Edge middleware from an Astro middleware in your code base. */ - edgeMiddleware?: boolean; - - /** The maximum duration (in seconds) that Serverless Functions can run before timing out. See the [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) for the default and maximum limit for your account plan. */ - maxDuration?: number; - - /** Whether to cache on-demand rendered pages in the same way as static files. */ - isr?: boolean | VercelISRConfig; - /** - * It enables Vercel skew protection: https://vercel.com/docs/deployments/skew-protection - */ - skewProtection?: boolean; -} - -interface VercelISRConfig { - /** - * A secret random string that you create. - * Its presence in the `__prerender_bypass` cookie will result in fresh responses being served, bypassing the cache. See Vercel’s documentation on [Draft Mode](https://vercel.com/docs/build-output-api/v3/features#draft-mode) for more information. - * Its presence in the `x-prerender-revalidate` header will result in a fresh response which will then be cached for all future requests to be used. See Vercel’s documentation on [On-Demand Incremental Static Regeneration (ISR)](https://vercel.com/docs/build-output-api/v3/features#on-demand-incremental-static-regeneration-isr) for more information. - * - * @default `undefined` - */ - bypassToken?: string; - - /** - * Expiration time (in seconds) before the pages will be re-generated. - * - * Setting to `false` means that the page will stay cached as long as the current deployment is in production. - * - * @default `false` - */ - expiration?: number | false; - - /** - * Paths that will always be served by a serverless function instead of an ISR function. - * - * @default `[]` - */ - exclude?: string[]; -} - -export default function vercelServerless({ - webAnalytics, - speedInsights, - includeFiles: _includeFiles = [], - excludeFiles: _excludeFiles = [], - imageService, - imagesConfig, - devImageService = 'sharp', - edgeMiddleware = false, - maxDuration, - isr = false, - skewProtection = false, -}: VercelServerlessConfig = {}): AstroIntegration { - if (maxDuration) { - if (typeof maxDuration !== 'number') { - throw new TypeError(`maxDuration must be a number`, { cause: maxDuration }); - } - if (maxDuration <= 0) { - throw new TypeError(`maxDuration must be a positive number`, { cause: maxDuration }); - } - } - - let _config: AstroConfig; - let _buildTempFolder: URL; - let _serverEntry: string; - let _entryPoints: Map<RouteData, URL>; - let _middlewareEntryPoint: URL | undefined; - // Extra files to be merged with `includeFiles` during build - const extraFilesToInclude: URL[] = []; - // Secret used to verify that the caller is the astro-generated edge middleware and not a third-party - const middlewareSecret = crypto.randomUUID(); - - return { - name: PACKAGE_NAME, - hooks: { - 'astro:config:setup': async ({ command, config, updateConfig, injectScript, logger }) => { - if (maxDuration && maxDuration > 900) { - logger.warn( - `maxDuration is set to ${maxDuration} seconds, which is longer than the maximum allowed duration of 900 seconds.`, - ); - logger.warn( - `Please make sure that your plan allows for this duration. See https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration for more information.`, - ); - } - - if (webAnalytics?.enabled) { - injectScript( - 'head-inline', - await getInjectableWebAnalyticsContent({ - mode: command === 'dev' ? 'development' : 'production', - }), - ); - } - if (command === 'build' && speedInsights?.enabled) { - injectScript('page', 'import "@astrojs/vercel/speed-insights"'); - } - - const vercelConfigPath = new URL('vercel.json', config.root); - if (existsSync(vercelConfigPath)) { - try { - const vercelConfig = JSON.parse(readFileSync(vercelConfigPath, 'utf-8')); - if (vercelConfig.trailingSlash === true && config.trailingSlash === 'always') { - logger.warn( - '\n' + - `\tYour "vercel.json" \`trailingSlash\` configuration (set to \`true\`) will conflict with your Astro \`trailinglSlash\` configuration (set to \`"always"\`).\n` + - `\tThis would cause infinite redirects under certain conditions and throw an \`ERR_TOO_MANY_REDIRECTS\` error.\n` + - `\tTo prevent this, your Astro configuration is updated to \`"ignore"\` during builds.\n`, - ); - updateConfig({ - trailingSlash: 'ignore', - }); - } - } catch (_err) { - logger.warn(`Your "vercel.json" config is not a valid json file.`); - } - } - - updateConfig({ - outDir: new URL('./.vercel/output/', config.root), - build: { - client: new URL('./.vercel/output/static/', config.root), - server: new URL('./.vercel/output/_functions/', config.root), - redirects: false, - }, - vite: { - ...getSpeedInsightsViteConfig(speedInsights?.enabled), - ssr: { - external: [ - '@vercel/nft', - ...((await shouldExternalizeAstroEnvSetup()) ? ['astro/env/setup'] : []), - ], - }, - }, - ...getAstroImageConfig( - imageService, - imagesConfig, - command, - devImageService, - config.image, - ), - }); - }, - 'astro:config:done': ({ setAdapter, config }) => { - setAdapter(getAdapter({ edgeMiddleware, middlewareSecret, skewProtection })); - - _config = config; - _buildTempFolder = config.build.server; - _serverEntry = config.build.serverEntry; - - if (config.output === 'static') { - throw new AstroError( - '`output: "server"` or `output: "hybrid"` is required to use the serverless adapter.', - ); - } - }, - 'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => { - _entryPoints = new Map( - Array.from(entryPoints).filter(([routeData]) => !routeData.prerender), - ); - _middlewareEntryPoint = middlewareEntryPoint; - }, - 'astro:build:done': async ({ routes, logger }) => { - // Merge any includes from `vite.assetsInclude - if (_config.vite.assetsInclude) { - const mergeGlobbedIncludes = (globPattern: unknown) => { - if (typeof globPattern === 'string') { - const entries = glob.sync(globPattern).map((p) => pathToFileURL(p)); - extraFilesToInclude.push(...entries); - } else if (Array.isArray(globPattern)) { - for (const pattern of globPattern) { - mergeGlobbedIncludes(pattern); - } - } - }; - - mergeGlobbedIncludes(_config.vite.assetsInclude); - } - - const routeDefinitions: Array<{ - src: string; - dest: string; - middlewarePath?: string; - }> = []; - - const includeFiles = _includeFiles - .map((file) => new URL(file, _config.root)) - .concat(extraFilesToInclude); - const excludeFiles = _excludeFiles.map((file) => new URL(file, _config.root)); - - const builder = new VercelBuilder(_config, excludeFiles, includeFiles, logger, maxDuration); - - // Multiple entrypoint support - if (_entryPoints.size) { - const getRouteFuncName = (route: RouteData) => route.component.replace('src/pages/', ''); - - const getFallbackFuncName = (entryFile: URL) => - basename(entryFile.toString()) - .replace('entry.', '') - .replace(/\.mjs$/, ''); - - for (const [route, entryFile] of _entryPoints) { - const func = route.component.startsWith('src/pages/') - ? getRouteFuncName(route) - : getFallbackFuncName(entryFile); - - await builder.buildServerlessFolder(entryFile, func); - - routeDefinitions.push({ - src: route.pattern.source, - dest: func, - }); - } - } else { - const entryFile = new URL(_serverEntry, _buildTempFolder); - if (isr) { - const isrConfig = typeof isr === 'object' ? isr : {}; - await builder.buildServerlessFolder(entryFile, NODE_PATH); - if (isrConfig.exclude?.length) { - const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; - for (const route of isrConfig.exclude) { - // vercel interprets src as a regex pattern, so we need to escape it - routeDefinitions.push({ src: escapeRegex(route), dest }); - } - } - await builder.buildISRFolder(entryFile, '_isr', isrConfig); - for (const route of routes) { - const src = route.pattern.source; - const dest = src.startsWith('^\\/_image') ? NODE_PATH : ISR_PATH; - if (!route.prerender) routeDefinitions.push({ src, dest }); - } - } else { - await builder.buildServerlessFolder(entryFile, NODE_PATH); - const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; - for (const route of routes) { - if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest }); - } - } - } - if (_middlewareEntryPoint) { - await builder.buildMiddlewareFolder( - _middlewareEntryPoint, - MIDDLEWARE_PATH, - middlewareSecret, - ); - } - const fourOhFourRoute = routes.find((route) => route.pathname === '/404'); - // Output configuration - // https://vercel.com/docs/build-output-api/v3#build-output-configuration - await writeJson(new URL(`./config.json`, _config.outDir), { - version: 3, - routes: [ - ...getRedirects(routes, _config), - { - src: `^/${_config.build.assets}/(.*)$`, - headers: { 'cache-control': 'public, max-age=31536000, immutable' }, - continue: true, - }, - { handle: 'filesystem' }, - ...routeDefinitions, - ...(fourOhFourRoute - ? [ - { - src: '/.*', - dest: fourOhFourRoute.prerender - ? '/404.html' - : _middlewareEntryPoint - ? MIDDLEWARE_PATH - : NODE_PATH, - status: 404, - }, - ] - : []), - ], - ...(imageService || imagesConfig - ? { - images: imagesConfig - ? { - ...imagesConfig, - domains: [...imagesConfig.domains, ..._config.image.domains], - remotePatterns: [ - ...(imagesConfig.remotePatterns ?? []), - ..._config.image.remotePatterns, - ], - } - : getDefaultImageConfig(_config.image), - } - : {}), - }); - - // Remove temporary folder - await removeDir(_buildTempFolder); - }, - }, - }; -} - -type Runtime = `nodejs${string}.x`; - -// TODO: remove once we don't use a TLA anymore -async function shouldExternalizeAstroEnvSetup() { - try { - await import('astro/env/setup'); - return false; - } catch { - return true; - } -} - -class VercelBuilder { - readonly NTF_CACHE = {}; - - constructor( - readonly config: AstroConfig, - readonly excludeFiles: URL[], - readonly includeFiles: URL[], - readonly logger: AstroIntegrationLogger, - readonly maxDuration?: number, - readonly runtime = getRuntime(process, logger), - ) {} - - async buildServerlessFolder(entry: URL, functionName: string) { - const { config, includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this; - // .vercel/output/functions/<name>.func/ - const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir); - const packageJson = new URL(`./functions/${functionName}.func/package.json`, config.outDir); - const vcConfig = new URL(`./functions/${functionName}.func/.vc-config.json`, config.outDir); - - // Copy necessary files (e.g. node_modules/) - const { handler } = await copyDependenciesToFunction( - { - entry, - outDir: functionFolder, - includeFiles, - excludeFiles, - logger, - }, - NTF_CACHE, - ); - - // Enable ESM - // https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/ - await writeJson(packageJson, { type: 'module' }); - - // Serverless function config - // https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration - await writeJson(vcConfig, { - runtime, - handler: handler.replaceAll('\\', '/'), - launcherType: 'Nodejs', - maxDuration, - supportsResponseStreaming: true, - }); - } - - async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig) { - await this.buildServerlessFolder(entry, functionName); - const prerenderConfig = new URL( - `./functions/${functionName}.prerender-config.json`, - this.config.outDir, - ); - // https://vercel.com/docs/build-output-api/v3/primitives#prerender-configuration-file - await writeJson(prerenderConfig, { - expiration: isr.expiration ?? false, - bypassToken: isr.bypassToken, - allowQuery: [ASTRO_PATH_PARAM], - passQuery: true, - }); - } - - async buildMiddlewareFolder(entry: URL, functionName: string, middlewareSecret: string) { - const functionFolder = new URL(`./functions/${functionName}.func/`, this.config.outDir); - - await generateEdgeMiddleware( - entry, - this.config.root, - new URL(VERCEL_EDGE_MIDDLEWARE_FILE, this.config.srcDir), - new URL('./middleware.mjs', functionFolder), - middlewareSecret, - this.logger, - ); - - await writeJson(new URL(`./.vc-config.json`, functionFolder), { - runtime: 'edge', - entrypoint: 'middleware.mjs', - }); - } -} - -function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Runtime { - const version = process.version.slice(1); // 'v18.19.0' --> '18.19.0' - const major = version.split('.')[0]; // '18.19.0' --> '18' - const support = SUPPORTED_NODE_VERSIONS[major]; - if (support === undefined) { - logger.warn( - `\n` + - `\tThe local Node.js version (${major}) is not supported by Vercel Serverless Functions.\n` + - `\tYour project will use Node.js 18 as the runtime instead.\n` + - `\tConsider switching your local version to 18.\n`, - ); - return 'nodejs18.x'; - } - if (support.status === 'default') { - return `nodejs${major}.x`; - } - if (support.status === 'retiring') { - if (support.warnDate && new Date() >= support.warnDate) { - logger.warn( - `Your project is being built for Node.js ${major} as the runtime, which is retiring by ${support.removal}.`, - ); - } - return `nodejs${major}.x`; - } - if (support.status === 'beta') { - logger.warn( - `Your project is being built for Node.js ${major} as the runtime, which is currently in beta for Vercel Serverless Functions.`, - ); - return `nodejs${major}.x`; - } - if (support.status === 'deprecated') { - const removeDate = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format( - support.removal, - ); - logger.warn( - `\n` + - `\tYour project is being built for Node.js ${major} as the runtime.\n` + - `\tThis version is deprecated by Vercel Serverless Functions, and scheduled to be disabled on ${removeDate}.\n` + - `\tConsider upgrading your local version to 18.\n`, - ); - return `nodejs${major}.x`; - } - return 'nodejs18.x'; -} diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts deleted file mode 100644 index 222722dd8..000000000 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'node:http'; -import type { SSRManifest } from 'astro'; -import { NodeApp, applyPolyfills } from 'astro/app/node'; -import { setGetEnv } from 'astro/env/setup'; -import { - ASTRO_LOCALS_HEADER, - ASTRO_MIDDLEWARE_SECRET_HEADER, - ASTRO_PATH_HEADER, - ASTRO_PATH_PARAM, -} from './adapter.js'; - -// Run polyfills immediately so any dependent code can use the globals -applyPolyfills(); -setGetEnv((key) => process.env[key]); - -export const createExports = ( - manifest: SSRManifest, - { middlewareSecret, skewProtection }: { middlewareSecret: string; skewProtection: boolean }, -) => { - const app = new NodeApp(manifest); - const handler = async (req: IncomingMessage, res: ServerResponse) => { - const url = new URL(`https://example.com${req.url}`); - const clientAddress = req.headers['x-forwarded-for'] as string | undefined; - const localsHeader = req.headers[ASTRO_LOCALS_HEADER]; - const middlewareSecretHeader = req.headers[ASTRO_MIDDLEWARE_SECRET_HEADER]; - const realPath = req.headers[ASTRO_PATH_HEADER] ?? url.searchParams.get(ASTRO_PATH_PARAM); - if (typeof realPath === 'string') { - req.url = realPath; - } - - let locals = {}; - if (localsHeader) { - if (middlewareSecretHeader !== middlewareSecret) { - res.statusCode = 403; - res.end('Forbidden'); - return; - } - locals = - typeof localsHeader === 'string' ? JSON.parse(localsHeader) : JSON.parse(localsHeader[0]); - } - // hide the secret from the rest of user code - delete req.headers[ASTRO_MIDDLEWARE_SECRET_HEADER]; - - // https://vercel.com/docs/deployments/skew-protection#supported-frameworks - if (skewProtection && process.env.VERCEL_SKEW_PROTECTION_ENABLED === '1') { - req.headers['x-deployment-id'] = process.env.VERCEL_DEPLOYMENT_ID; - } - - const webResponse = await app.render(req, { addCookieHeader: true, clientAddress, locals }); - await NodeApp.writeResponse(webResponse, res); - }; - - return { default: handler }; -}; - -// HACK: prevent warning -// @astrojs-ssr-virtual-entry (22:23) "start" is not exported by "dist/serverless/entrypoint.js", imported by "@astrojs-ssr-virtual-entry". -export function start() {} diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts deleted file mode 100644 index 07d0843bf..000000000 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { existsSync } from 'node:fs'; -import { builtinModules } from 'node:module'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import type { AstroIntegrationLogger } from 'astro'; -import { - ASTRO_LOCALS_HEADER, - ASTRO_MIDDLEWARE_SECRET_HEADER, - ASTRO_PATH_HEADER, - NODE_PATH, -} from './adapter.js'; - -/** - * It generates the Vercel Edge Middleware file. - * - * It creates a temporary file, the edge middleware, with some dynamic info. - * - * Then this file gets bundled with esbuild. The bundle phase will inline the Astro middleware code. - * - * @param astroMiddlewareEntryPoint - * @param outPath - * @returns {Promise<URL>} The path to the bundled file - */ -export async function generateEdgeMiddleware( - astroMiddlewareEntryPointPath: URL, - root: URL, - vercelEdgeMiddlewareHandlerPath: URL, - outPath: URL, - middlewareSecret: string, - logger: AstroIntegrationLogger, -): Promise<URL> { - const code = edgeMiddlewareTemplate( - astroMiddlewareEntryPointPath, - vercelEdgeMiddlewareHandlerPath, - middlewareSecret, - logger, - ); - // https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware - const bundledFilePath = fileURLToPath(outPath); - const esbuild = await import('esbuild'); - await esbuild.build({ - stdin: { - contents: code, - resolveDir: fileURLToPath(root), - }, - target: 'es2020', - platform: 'browser', - // https://runtime-keys.proposal.wintercg.org/#edge-light - conditions: ['edge-light', 'worker', 'browser'], - outfile: bundledFilePath, - allowOverwrite: true, - format: 'esm', - bundle: true, - minify: false, - // ensure node built-in modules are namespaced with `node:` - plugins: [ - { - name: 'esbuild-namespace-node-built-in-modules', - setup(build) { - const filter = new RegExp(builtinModules.map((mod) => `(^${mod}$)`).join('|')); - build.onResolve({ filter }, (args) => ({ path: 'node:' + args.path, external: true })); - }, - }, - ], - }); - return pathToFileURL(bundledFilePath); -} - -function edgeMiddlewareTemplate( - astroMiddlewareEntryPointPath: URL, - vercelEdgeMiddlewareHandlerPath: URL, - middlewareSecret: string, - logger: AstroIntegrationLogger, -) { - const middlewarePath = JSON.stringify( - fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/'), - ); - const filePathEdgeMiddleware = fileURLToPath(vercelEdgeMiddlewareHandlerPath); - let handlerTemplateImport = ''; - let handlerTemplateCall = '{}'; - if (existsSync(filePathEdgeMiddleware + '.js') || existsSync(filePathEdgeMiddleware + '.ts')) { - logger.warn( - 'Usage of `vercel-edge-middleware.js` is deprecated. You can now use the `waitUntil(promise)` function directly as `ctx.locals.waitUntil(promise)`.', - ); - const stringified = JSON.stringify(filePathEdgeMiddleware.replace(/\\/g, '/')); - handlerTemplateImport = `import handler from ${stringified}`; - handlerTemplateCall = `await handler({ request, context })`; - } else { - } - return ` - ${handlerTemplateImport} -import { onRequest } from ${middlewarePath}; -import { createContext, trySerializeLocals } from 'astro/middleware'; -export default async function middleware(request, context) { - const ctx = createContext({ - request, - params: {} - }); - ctx.locals = { vercel: { edge: context }, ...${handlerTemplateCall} }; - const { origin } = new URL(request.url); - const next = async () => { - const { vercel, ...locals } = ctx.locals; - const response = await fetch(new URL('/${NODE_PATH}', request.url), { - headers: { - ...Object.fromEntries(request.headers.entries()), - '${ASTRO_MIDDLEWARE_SECRET_HEADER}': '${middlewareSecret}', - '${ASTRO_PATH_HEADER}': request.url.replace(origin, ''), - '${ASTRO_LOCALS_HEADER}': trySerializeLocals(locals) - } - }); - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); - }; - - const response = await onRequest(ctx, next); - // Append cookies from Astro.cookies - for(const setCookieHeaderValue of ctx.cookies.headers()) { - response.headers.append('set-cookie', setCookieHeaderValue); - } - return response; -}`; -} diff --git a/packages/integrations/vercel/src/speed-insights.ts b/packages/integrations/vercel/src/speed-insights.ts deleted file mode 100644 index cd2ae7fe8..000000000 --- a/packages/integrations/vercel/src/speed-insights.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Metric } from 'web-vitals'; -import { onCLS, onFCP, onFID, onLCP, onTTFB } from 'web-vitals'; - -const SPEED_INSIGHTS_INTAKE = 'https://vitals.vercel-analytics.com/v1/vitals'; - -type Options = { path: string; analyticsId: string }; - -const getConnectionSpeed = () => { - return 'connection' in navigator && - navigator['connection'] && - 'effectiveType' in (navigator['connection'] as unknown as { effectiveType: string }) - ? (navigator['connection'] as unknown as { effectiveType: string })['effectiveType'] - : ''; -}; - -const sendToSpeedInsights = (metric: Metric, options: Options) => { - const body = { - dsn: options.analyticsId, - id: metric.id, - page: options.path, - href: location.href, - event_name: metric.name, - value: metric.value.toString(), - speed: getConnectionSpeed(), - }; - const blob = new Blob([new URLSearchParams(body).toString()], { - type: 'application/x-www-form-urlencoded', - }); - if (navigator.sendBeacon) { - navigator.sendBeacon(SPEED_INSIGHTS_INTAKE, blob); - } else - fetch(SPEED_INSIGHTS_INTAKE, { - body: blob, - method: 'POST', - credentials: 'omit', - keepalive: true, - }); -}; - -function collectWebVitals() { - const analyticsId = (import.meta as any).env.PUBLIC_VERCEL_ANALYTICS_ID; - - if (!analyticsId) { - console.error('[Speed Insights] VERCEL_ANALYTICS_ID not found'); - return; - } - - const options: Options = { path: window.location.pathname, analyticsId }; - - try { - onFID((metric) => sendToSpeedInsights(metric, options)); - onTTFB((metric) => sendToSpeedInsights(metric, options)); - onLCP((metric) => sendToSpeedInsights(metric, options)); - onCLS((metric) => sendToSpeedInsights(metric, options)); - onFCP((metric) => sendToSpeedInsights(metric, options)); - } catch (err) { - console.error('[Speed Insights]', err); - } -} - -const mode = (import.meta as any).env.MODE as 'development' | 'production'; - -if (mode === 'production') { - collectWebVitals(); -} diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts deleted file mode 100644 index 4969d55d1..000000000 --- a/packages/integrations/vercel/src/static/adapter.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; - -import { emptyDir, writeJson } from '@astrojs/internal-helpers/fs'; -import { - type DevImageService, - type VercelImageConfig, - getAstroImageConfig, - getDefaultImageConfig, -} from '../image/shared.js'; -import { isServerLikeOutput } from '../lib/prerender.js'; -import { getRedirects } from '../lib/redirects.js'; -import { - type VercelSpeedInsightsConfig, - getSpeedInsightsViteConfig, -} from '../lib/speed-insights.js'; -import { - type VercelWebAnalyticsConfig, - getInjectableWebAnalyticsContent, -} from '../lib/web-analytics.js'; - -const PACKAGE_NAME = '@astrojs/vercel/static'; - -function getAdapter(): AstroAdapter { - return { - name: PACKAGE_NAME, - supportedAstroFeatures: { - assets: { - supportKind: 'stable', - isSharpCompatible: true, - }, - staticOutput: 'stable', - serverOutput: 'unsupported', - hybridOutput: 'unsupported', - envGetSecret: 'unsupported', - }, - adapterFeatures: { - edgeMiddleware: false, - }, - }; -} - -export interface VercelStaticConfig { - webAnalytics?: VercelWebAnalyticsConfig; - /** - * @deprecated This option lets you configure the legacy speed insights API which is now deprecated by Vercel. - * - * See [Vercel Speed Insights Quickstart](https://vercel.com/docs/speed-insights/quickstart) for instructions on how to use the library instead. - * - * https://vercel.com/docs/speed-insights/quickstart - */ - speedInsights?: VercelSpeedInsightsConfig; - imageService?: boolean; - imagesConfig?: VercelImageConfig; - devImageService?: DevImageService; -} - -export default function vercelStatic({ - webAnalytics, - speedInsights, - imageService, - imagesConfig, - devImageService = 'sharp', -}: VercelStaticConfig = {}): AstroIntegration { - let _config: AstroConfig; - - return { - name: '@astrojs/vercel', - hooks: { - 'astro:config:setup': async ({ command, config, injectScript, updateConfig }) => { - if (webAnalytics?.enabled) { - injectScript( - 'head-inline', - await getInjectableWebAnalyticsContent({ - mode: command === 'dev' ? 'development' : 'production', - }), - ); - } - if (command === 'build' && speedInsights?.enabled) { - injectScript('page', 'import "@astrojs/vercel/speed-insights"'); - } - const outDir = new URL('./.vercel/output/static/', config.root); - updateConfig({ - outDir, - build: { - format: 'directory', - redirects: false, - }, - vite: { - ...getSpeedInsightsViteConfig(speedInsights?.enabled), - }, - ...getAstroImageConfig( - imageService, - imagesConfig, - command, - devImageService, - config.image, - ), - }); - }, - 'astro:config:done': ({ setAdapter, config }) => { - setAdapter(getAdapter()); - _config = config; - - if (isServerLikeOutput(config)) { - throw new Error(`${PACKAGE_NAME} should be used with output: 'static'`); - } - }, - 'astro:build:start': async () => { - // Ensure to have `.vercel/output` empty. - // This is because, when building to static, outDir = .vercel/output/static/, - // so .vercel/output itself won't get cleaned. - await emptyDir(new URL('./.vercel/output/', _config.root)); - }, - 'astro:build:done': async ({ routes }) => { - // Output configuration - // https://vercel.com/docs/build-output-api/v3#build-output-configuration - await writeJson(new URL('./.vercel/output/config.json', _config.root), { - version: 3, - routes: [ - ...getRedirects(routes, _config), - { - src: `^/${_config.build.assets}/(.*)$`, - headers: { 'cache-control': 'public, max-age=31536000, immutable' }, - continue: true, - }, - { handle: 'filesystem' }, - ...(routes.find((route) => route.pathname === '/404') - ? [ - { - src: `/.*`, - dest: `/404.html`, - status: 404, - }, - ] - : []), - ], - ...(imageService || imagesConfig - ? { - images: imagesConfig - ? { - ...imagesConfig, - domains: [...imagesConfig.domains, ..._config.image.domains], - remotePatterns: [ - ...(imagesConfig.remotePatterns ?? []), - ..._config.image.remotePatterns, - ], - } - : getDefaultImageConfig(_config.image), - } - : {}), - }); - }, - }, - }; -} diff --git a/packages/integrations/vercel/src/types.d.ts b/packages/integrations/vercel/src/types.d.ts deleted file mode 100644 index 1c5b8d2db..000000000 --- a/packages/integrations/vercel/src/types.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { AnalyticsProps } from '@vercel/analytics'; - -export type VercelWebAnalyticsBeforeSend = AnalyticsProps['beforeSend']; |