diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index ce1ffb910..7ff9686dd 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -111,10 +111,10 @@ export default defineConfig({ #### `beforeSend` -Since functions can't be passed down via `astro.config.mjs`, you need to export the `beforeSend` function in a separate file inside your root called `vercel-web-analytics.ts`. +To define the `beforeSend` function, you need to create a separate file inside your root called `vercel-web-analytics.ts`. If you're not using TypeScript, you can define the function inside `vercel-web-analytics.js`. -```js +```ts // vercel-web-analytics.ts import type { VercelWebAnalyticsBeforeSend } from '@astrojs/vercel'; @@ -128,8 +128,6 @@ export const beforeSend: VercelWebAnalyticsBeforeSend = (event) => { } ``` -````js - ### Speed Insights You can enable [Vercel Speed Insights](https://vercel.com/docs/concepts/speed-insights) by setting `speedInsights: { enabled: true }`. This will collect and send Web Vital data to Vercel. diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index b822fcfe2..6de75d78d 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -52,7 +52,7 @@ }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", - "@vercel/analytics": "~1.0.0", + "@vercel/analytics": "^1.0.2", "@vercel/nft": "^0.23.1", "esbuild": "^0.19.2", "fast-glob": "^3.3.1", diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts deleted file mode 100644 index e919bad1a..000000000 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; - -import esbuild from 'esbuild'; -import { relative as relativePath } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { defaultImageConfig, getImageConfig, type VercelImageConfig } from '../image/shared.js'; -import { - copyFilesToFunction, - getFilesFromFolder, - getVercelOutput, - removeDir, - writeJson, -} from '../lib/fs.js'; -import { getRedirects } from '../lib/redirects.js'; -import { - getInjectableWebAnalyticsContent, - type VercelWebAnalyticsConfig, -} from '../lib/web-analytics.js'; -import { - getSpeedInsightsViteConfig, - type VercelSpeedInsightsConfig, -} from '../lib/speed-insights.js'; - -const PACKAGE_NAME = '@astrojs/vercel/edge'; - -function getAdapter(): AstroAdapter { - return { - name: PACKAGE_NAME, - serverEntrypoint: `${PACKAGE_NAME}/entrypoint`, - exports: ['default'], - }; -} - -export interface VercelEdgeConfig { - webAnalytics?: VercelWebAnalyticsConfig; - speedInsights?: VercelSpeedInsightsConfig; - includeFiles?: string[]; - imageService?: boolean; - imagesConfig?: VercelImageConfig; -} - -export default function vercelEdge({ - webAnalytics, - speedInsights, - includeFiles = [], - imageService, - imagesConfig, -}: VercelEdgeConfig = {}): AstroIntegration { - let _config: AstroConfig; - let buildTempFolder: URL; - let functionFolder: URL; - let serverEntry: string; - - return { - name: PACKAGE_NAME, - hooks: { - 'astro:config:setup': async ({ command, config, updateConfig, injectScript }) => { - if (webAnalytics?.enabled) { - injectScript( - 'page', - await getInjectableWebAnalyticsContent( - { - ...webAnalytics.config, - mode: command === 'dev' ? 'development' : 'production', - }, - config.root - ) - ); - } - if (command === 'build' && speedInsights?.enabled) { - injectScript('page', 'import "@astrojs/vercel/speed-insights"'); - } - const outDir = getVercelOutput(config.root); - updateConfig({ - outDir, - build: { - serverEntry: 'entry.mjs', - client: new URL('./static/', outDir), - server: new URL('./dist/', config.root), - }, - vite: { - ...getSpeedInsightsViteConfig(speedInsights?.enabled), - ssr: { - external: ['@vercel/nft'], - }, - }, - ...getImageConfig(imageService, imagesConfig, command), - }); - }, - 'astro:config:done': ({ setAdapter, config }) => { - setAdapter(getAdapter()); - _config = config; - buildTempFolder = config.build.server; - functionFolder = new URL('./functions/render.func/', config.outDir); - serverEntry = config.build.serverEntry; - - if (config.output === 'static') { - throw new Error(` - [@astrojs/vercel] \`output: "server"\` or \`output: "hybrid"\` is required to use the edge adapter. - - `); - } - }, - 'astro:build:setup': ({ vite, target }) => { - if (target === 'server') { - vite.resolve ||= {}; - vite.resolve.alias ||= {}; - - const aliases = [{ find: 'react-dom/server', replacement: 'react-dom/server.browser' }]; - - if (Array.isArray(vite.resolve.alias)) { - vite.resolve.alias = [...vite.resolve.alias, ...aliases]; - } else { - for (const alias of aliases) { - (vite.resolve.alias as Record)[alias.find] = alias.replacement; - } - } - - vite.ssr ||= {}; - vite.ssr.target = 'webworker'; - - // Vercel edge runtime is a special webworker-ish environment that supports process.env, - // but Vite would replace away `process.env` in webworkers, so we set a define here to prevent it - vite.define = { - 'process.env': 'process.env', - ...vite.define, - }; - } - }, - 'astro:build:done': async ({ routes }) => { - const entry = new URL(serverEntry, buildTempFolder); - const generatedFiles = await getFilesFromFolder(buildTempFolder); - const entryPath = fileURLToPath(entry); - - await esbuild.build({ - target: 'es2020', - platform: 'browser', - // https://runtime-keys.proposal.wintercg.org/#edge-light - conditions: ['edge-light', 'worker', 'browser'], - entryPoints: [entryPath], - outfile: entryPath, - allowOverwrite: true, - format: 'esm', - bundle: true, - minify: true, - }); - - // Copy entry and other server files - const commonAncestor = await copyFilesToFunction( - [...generatedFiles, ...includeFiles.map((file) => new URL(file, _config.root))], - functionFolder - ); - - // Remove temporary folder - await removeDir(buildTempFolder); - - // Edge function config - // https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions/configuration - await writeJson(new URL(`./.vc-config.json`, functionFolder), { - runtime: 'edge', - entrypoint: relativePath(commonAncestor, entryPath), - }); - - // 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' }, - { src: '/.*', dest: 'render' }, - ], - ...(imageService || imagesConfig - ? { images: imagesConfig ? imagesConfig : defaultImageConfig } - : {}), - }); - }, - }, - }; -} diff --git a/packages/integrations/vercel/src/lib/web-analytics.ts b/packages/integrations/vercel/src/lib/web-analytics.ts index 31cc22fbd..4874d41e5 100644 --- a/packages/integrations/vercel/src/lib/web-analytics.ts +++ b/packages/integrations/vercel/src/lib/web-analytics.ts @@ -1,51 +1,71 @@ +import { AstroError } from 'astro/errors'; import type { AnalyticsProps } from '@vercel/analytics'; import { fileURLToPath } from 'url'; +import { existsSync } from 'node:fs'; +import type { AstroIntegrationLogger } from 'astro'; export type VercelWebAnalyticsConfig = { enabled: boolean; config?: Omit; }; -async function getWebAnalyticsFunctions(root: URL) { - try { - const files = await Promise.all([ - import(/* @vite-ignore */ fileURLToPath(new URL('./vercel-web-analytics.ts', root))).catch( - () => undefined - ), - import(/* @vite-ignore */ fileURLToPath(new URL('./vercel-web-analytics.js', root))).catch( - () => undefined - ), - ]); +async function getWebAnalyticsFunctions({ + root, + logger, +}: { + root: URL; + logger: AstroIntegrationLogger; +}) { + const tsPath = fileURLToPath(new URL('./vercel-web-analytics.ts', root)); + const jsPath = fileURLToPath(new URL('./vercel-web-analytics.js', root)); - const functions = files[0] || files[1]; + const tsFileExists = existsSync(tsPath); + const jsFileExists = existsSync(jsPath); - if (functions?.default) { - if (typeof functions.default.beforeSend !== 'function') { - throw new Error( - `@astrojs/vercel: ./vercel-web-analytics.js should export a \`beforeSend\` function.` - ); - } + if (tsFileExists && jsFileExists) { + logger.warn( + `@astrojs/vercel: Both \`vercel-web-analytics.ts\` and \`vercel-web-analytics.js\` exist. Using \`vercel-web-analytics.ts\`.` + ); + } - return { - beforeSend: functions.default.beforeSend, - }; - } + if (!tsFileExists && !jsFileExists) { + logger.debug( + `@astrojs/vercel: \`vercel-web-analytics.ts\` or \`vercel-web-analytics.js\` not found.` + ); - return { - beforeSend: undefined, - }; - } catch (e) { return { beforeSend: undefined, }; } + + const functions = await import( + tsFileExists ? /* @vite-ignore */ tsPath : /* @vite-ignore */ jsPath + ); + + if (typeof functions.beforeSend !== 'function') { + throw new AstroError( + `@astrojs/vercel: \`vercel-web-analytics.${ + tsFileExists ? 'ts' : 'js' + }\` must export a \`beforeSend\` function.` + ); + } + + return { + beforeSend: functions.beforeSend, + }; } -export async function getInjectableWebAnalyticsContent( - config: Omit | undefined, - root: URL -) { - const { beforeSend } = await getWebAnalyticsFunctions(root); +export async function getInjectableWebAnalyticsContent({ + config, + astro, +}: { + config: Omit | undefined; + astro: { + root: URL; + logger: AstroIntegrationLogger; + }; +}) { + const { beforeSend } = await getWebAnalyticsFunctions(astro); return `import { inject } from '@vercel/analytics'; inject({ diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 2659e7703..93fd90c49 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -116,17 +116,20 @@ export default function vercelServerless({ return { name: PACKAGE_NAME, hooks: { - 'astro:config:setup': async ({ command, config, updateConfig, injectScript }) => { + 'astro:config:setup': async ({ command, config, updateConfig, injectScript, logger }) => { if (webAnalytics?.enabled) { injectScript( 'page', - await getInjectableWebAnalyticsContent( - { + await getInjectableWebAnalyticsContent({ + config: { ...webAnalytics.config, mode: command === 'dev' ? 'development' : 'production', }, - config.root - ) + astro: { + root: config.root, + logger, + }, + }) ); } if (command === 'build' && speedInsights?.enabled) { diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index d723063d5..c5d1d4cef 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -37,17 +37,20 @@ export default function vercelStatic({ return { name: '@astrojs/vercel', hooks: { - 'astro:config:setup': async ({ command, config, injectScript, updateConfig }) => { + 'astro:config:setup': async ({ command, config, injectScript, updateConfig, logger }) => { if (webAnalytics?.enabled) { injectScript( 'page', - await getInjectableWebAnalyticsContent( - { + await getInjectableWebAnalyticsContent({ + config: { ...webAnalytics.config, mode: command === 'dev' ? 'development' : 'production', }, - config.root - ) + astro: { + root: config.root, + logger, + }, + }) ); } if (command === 'build' && speedInsights?.enabled) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1f2be16a..c25dd31a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4679,7 +4679,7 @@ importers: specifier: workspace:* version: link:../../internal-helpers '@vercel/analytics': - specifier: ~1.0.0 + specifier: ^1.0.2 version: 1.0.2 '@vercel/nft': specifier: ^0.23.1