spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { NextConfig } from 'next';2import { existsSync } from 'node:fs';3import path from 'node:path';45// Monorepo: the single `.env` lives at the repository root; Next only reads the app directory.6for (const candidate of [path.resolve(process.cwd(), '../../.env'), path.resolve(process.cwd(), '.env')]) {7 if (existsSync(candidate)) {8 try {9 process.loadEnvFile(candidate);10 } catch {11 /* ignore malformed env */12 }13 }14}1516const nextConfig: NextConfig = {17 reactStrictMode: true,18 poweredByHeader: false,19 agentRules: false,20 // Dev only: Next blocks client assets/HMR for origins other than the printed host (localhost).21 // Allow 127.0.0.1 so curl/Playwright smoke tests hydrate in `next dev` as well.22 allowedDevOrigins: ['127.0.0.1', 'localhost'],23 // Workspace packages are consumed as TypeScript sources.24 transpilePackages: ['@cancerindex/shared', '@cancerindex/database', '@cancerindex/ontology', '@cancerindex/ranking'],25 serverExternalPackages: ['postgres', 'pino'],26 // Public API: /api/v1/* is proxied to the Fastify service (apps/api) so one public host serves web + API.27 async rewrites() {28 const api = process.env.CI_API_URL ?? `http://127.0.0.1:${process.env.API_PORT ?? 8251}`;29 return [{ source: '/api/v1/:path*', destination: `${api}/v1/:path*` }];30 },31 experimental: {32 optimizePackageImports: ['lucide-react'],33 // Workspace packages use NodeNext-style `./file.js` imports that resolve to .ts sources.34 // Turbopack does not rewrite these outside the app root, so the app is built with webpack35 // (`next dev/build --webpack`) where extensionAlias handles the mapping.36 extensionAlias: { '.js': ['.ts', '.tsx', '.js'], '.mjs': ['.mts', '.mjs'] },37 },38 outputFileTracingRoot: path.resolve(__dirname, '../..'),39 async headers() {40 // Static-ish public routes: let ngrok/CDN/browser caches share a rendered page for 10 min and serve41 // stale for an hour while revalidating. Never for /admin, /search, /api (user- or query-specific).42 const cacheable = ['/cancer/:path*', '/gene/:path*', '/variant/:path*', '/drug/:path*', '/trial/:path*', '/publication/:path*', '/source/:path*', '/rankings', '/rankings/:path*'];43 const PUBLIC_CACHE = { key: 'Cache-Control', value: 'public, s-maxage=600, stale-while-revalidate=3600' };44 const NO_STORE = { key: 'Cache-Control', value: 'private, no-store' };45 return [46 {47 source: '/(.*)',48 headers: [49 { key: 'X-Content-Type-Options', value: 'nosniff' },50 { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },51 { key: 'X-Frame-Options', value: 'SAMEORIGIN' },52 { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },53 ],54 },55 ...cacheable.map((source) => ({ source, headers: [PUBLIC_CACHE] })),56 { source: '/admin/:path*', headers: [NO_STORE] },57 { source: '/search', headers: [NO_STORE] },58 { source: '/api/:path*', headers: [NO_STORE] },59 ];60 },61};6263export default nextConfig;64