import { assertNodeVersion } from './scripts/lib/node-version.mjs'; import { cloudflare, type WorkerConfig } from '@cloudflare/vite-plugin'; import { flue, flueWorkerConfig } from '@flue/vite'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { defineConfig, type UserConfig } from 'vite'; import { fileURLToPath } from 'node:url'; import { buildIdentityDefines, readBuildIdentity } from './scripts/lib/build-identity.mjs'; import { bundleModuleReport } from './scripts/lib/bundle-module-report.mjs'; import { applyCloudflareDeploymentProfile } from './scripts/cloudflare-deployment-profile.mjs'; import { localWorkerViteSettings } from './scripts/lib/local-worker-lane.mjs'; import { PUBLIC_ASSET_PATHS } from './src/assets/public-assets.ts'; const projectRoot = fileURLToPath(new URL('.', import.meta.url)); /** * A host that builds this Worker from an unmodified checkout of this * repository, supplying its own entries and output. Without a host, the * config is this repository's own build and local Worker lanes. */ export interface ChickpeaWorkerHost { /** Absolute path of the host's app entry, used instead of src/app.ts. */ app: string; /** Absolute path of the host's non-HTTP handlers entry, used instead of src/cloudflare.ts. */ cloudflare: string; /** Absolute build output directory, outside this checkout. */ outDir: string; /** Adjusts the Worker config after Flue and Chickpea have built it. */ configureWorker?: (config: WorkerConfig) => void; } assertNodeVersion(); export function chickpeaWorkerViteConfig(command: 'build' | 'serve', host?: ChickpeaWorkerHost): UserConfig { const fluePlugins = flue({ providers: ['anthropic', 'openai', 'openrouter', 'google', 'cloudflare'], tracing: false, ...(host ? { app: host.app, cloudflare: host.cloudflare } : {}), }); // Capture the customizer from the same flue() instance during config // evaluation, then compose the optional deployment overlay after Flue has // added its generated entry and Durable Object bindings. Flue must run // first: its project and agent scan feeds the Cloudflare plugin's generated // Worker configuration during the same config pass. const configureFlueWorker = flueWorkerConfig(); // Local Worker lanes belong to this repository's own development. const local = command === 'serve' && !host ? localWorkerViteSettings() : undefined; return { // Explicit so a host running Vite from its own directory still resolves // this project, its source root and its Worker config. root: projectRoot, define: { ...buildIdentityDefines(projectRoot), // Explicit build-time mode: every Vite serve lane is local development, // where the committed build identity does not track working-tree schema // edits. Local serve must never attach to a persisted schema marker. __CHICKPEA_VITE_SERVE__: JSON.stringify(command === 'serve'), // How this Worker was built, so Admin can show matching redeploy steps. // Cloudflare Workers Builds sets WORKERS_CI=1; a local or generic-CI // `npm run deploy` does not. A serve lane is neither. __CHICKPEA_CLOUDFLARE_BUILD_SOURCE__: JSON.stringify( command === 'serve' ? 'unknown' : process.env.WORKERS_CI === '1' ? 'workers-builds' : 'command', ), }, // Public images are uploaded as Static Assets, not embedded in Worker code. publicDir: 'assets', plugins: [ fluePlugins, { name: 'chickpea-public-assets', apply: 'build', async generateBundle() { if (this.environment.name !== 'client') return; // Keep source/README artwork in the repository, but publish only the // images the application uses. publicDir still supports local dev. for (const fileName of PUBLIC_ASSET_PATHS) { this.emitFile({ type: 'asset', fileName, source: await readFile(path.resolve(this.environment.config.publicDir, fileName)), }); } }, }, bundleModuleReport(), cloudflare({ ...(local ? { persistState: { path: local.statePath }, tunnel: { name: local.tunnelName, autoStart: true }, } : {}), config(config) { configureFlueWorker(config); applyCloudflareDeploymentProfile(config); const identity = readBuildIdentity(projectRoot); config.vars = { ...(config.vars ?? {}), CHICKPEA_APP_VERSION: identity.version, CHICKPEA_SOURCE_COMMIT: identity.sourceCommit ?? 'unknown', }; if (local) { const authDb = config.d1_databases?.find((database) => database.binding === 'AUTH_DB'); if (!authDb) throw new Error('Local Worker development requires the AUTH_DB binding.'); authDb.database_id = local.d1DatabaseId; config.assets = { ...(config.assets ?? {}), // Static assets normally run before the Worker. Agent avatars // are generated from local state, so let the dynamic route own // only that namespace while keeping the static asset binding. run_worker_first: ['/assets/agents/*'], }; config.ai = { ...(config.ai ?? { binding: 'AI' }), remote: true }; config.vars = { ...(config.vars ?? {}), CHICKPEA_LOCAL_LANE: local.lane, CHICKPEA_LOCAL_RUNTIME: 'cloudflare-vite/workerd', CHICKPEA_LOCAL_TRANSPORT: 'slack-http-events', CHICKPEA_LOCAL_SOURCE_SHA: local.sourceSha, SLACK_TAG_PUBLIC_URL: local.publicUrl, CHICKPEA_TELEMETRY_ENVIRONMENT: 'development', }; } host?.configureWorker?.(config); } }), ], resolve: { alias: [ // Never reached on Chickpea's paths; see src/build-stubs/empty-module.ts. { find: /^mimetext$/, replacement: path.join(projectRoot, 'src/build-stubs/empty-module.ts') }, { find: /^pusher-js$/, replacement: path.join(projectRoot, 'src/build-stubs/empty-module.ts') }, ], }, build: { outDir: host?.outDir ?? 'dist-cf', // Vite empties only an output directory inside the root; a host's is outside it. ...(host ? { emptyOutDir: true } : {}), copyPublicDir: false, minify: 'oxc', // Better Auth dispatches on error.name, which jose derives from the class // name; without this, revoking an expired access token answers 500. rolldownOptions: { output: { keepNames: true } }, }, ...(local ? { optimizeDeps: { // This Worker graph is ESM. Discovery repeatedly invalidates pi-ai's // generated provider chunks during workerd boot, so run the source // graph directly instead of maintaining a disposable prebundle cache. noDiscovery: true, include: [], exclude: ['@earendil-works/pi-ai'], }, server: { host: '127.0.0.1', port: local.port, strictPort: true, allowedHosts: [ new URL(local.publicUrl).hostname, // read.cloudflare.ts uses this non-routable origin when it calls the // ASSETS binding from Durable Objects and other requestless paths. 'chickpea-assets.invalid', ], }, } : {}), }; } export default defineConfig(({ command }) => chickpeaWorkerViteConfig(command));