import path from 'node:path'; import type { NextConfig } from 'next'; // 修正:worktree 根目錄需明確指定,避免 Next.js 誤用父目錄的 package-lock.json const PROJECT_ROOT = process.cwd(); const nextConfig: NextConfig = { output: 'standalone', outputFileTracingRoot: PROJECT_ROOT, // sharp 為原生套件(含平台專屬 .node 二進位),交由 Node 於 runtime require, // 不要讓 webpack/Turbopack 打包;standalone 由 Dockerfile 手動複製其 node_modules。 serverExternalPackages: ['sharp'], allowedDevOrigins: ['127.0.0.1', 'localhost'], distDir: 'build', // ASCII-only path // Next 16 still checks for the old typescript/lib/typescript.js path, which // TS 7 no longer ships. `npm run build` runs `npm run typecheck` first; // @typescript/native-preview keeps Next from auto-installing legacy TS. typescript: { ignoreBuildErrors: true }, experimental: { optimizePackageImports: ['lucide-react', 'date-fns', 'decimal.js'], }, // Next.js 15 起 instrumentation.js 為穩定 API,無需 experimental.instrumentationHook // instrumentation.ts 會被編譯給 nodejs 與 edge 兩個 runtime; // edge 環境不提供 path/fs/crypto,設 fallback:false 讓 webpack 不拋錯。 // 實際執行時這些模組只在 NEXT_RUNTIME==='nodejs' 時才被呼叫。 webpack(config, { nextRuntime }) { // 強制 webpack 優先使用 worktree 的 node_modules,避免 Next.js workspace root 偵測錯誤 // 導致 client bundle 與 RSC server 使用不同的 next 路徑,造成 module ID 不一致 config.resolve = { ...config.resolve, // Next 16 cannot load tsconfig paths through TS 7's new package shape, // so keep the existing @/* alias explicit here. alias: { ...(config.resolve?.alias ?? {}), '@': PROJECT_ROOT, }, modules: [ path.resolve(PROJECT_ROOT, 'node_modules'), 'node_modules', ], }; if (nextRuntime !== 'nodejs') { config.resolve.fallback = { ...config.resolve.fallback, path: false, fs: false, crypto: false, }; } return config; }, async headers() { const isDev = process.env.NODE_ENV !== 'production'; const scriptSrc = isDev ? "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://accounts.google.com https://challenges.cloudflare.com" : "script-src 'self' 'unsafe-inline' https://accounts.google.com https://challenges.cloudflare.com"; return [ // apple-app-site-association:Universal Links 的驗證檔案。 // // Apple 的要求(https://developer.apple.com/documentation/xcode/supporting-associated-domains): // 1. 必須以 `Content-Type: application/json` 提供(不能是 // `text/plain`,也不能靠副檔名推導 —— 此檔沒有副檔名)。 // 2. **不得**轉址(301/302 會讓驗證失敗),因此以 exact path 規則 // 讓它直接命中靜態檔。 // 3. 不可長期快取:內容變更(換 Team ID、增加路徑)後需立刻生效, // 否則 Apple 會持續使用舊的關聯資料。 { source: '/.well-known/apple-app-site-association', headers: [ { key: 'Content-Type', value: 'application/json' }, { key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' }, ], }, // 靜態資源長期快取(僅 production;dev mode 下 app-pages-internals.js 等無 hash 的 chunk 不可 immutable) ...(!isDev ? [ { source: '/_next/static/(.*)', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }, ], }, { source: '/public/(.*)', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }, ], }, // PWA 圖示與 manifest 由 Service Worker pre-cache(見 public/sw.js); // 帶雜湊語意但路徑固定,故給短快取並允許重新驗證,避免更新圖示後 // 使用者長時間拿到舊版。sw.js 本身一律不快取,否則新版無法生效。 { source: '/icons/(.*)', headers: [ { key: 'Cache-Control', value: 'public, max-age=86400, stale-while-revalidate=604800' }, ], }, { source: '/manifest.webmanifest', headers: [ { key: 'Cache-Control', value: 'public, max-age=3600, must-revalidate' }, { key: 'Content-Type', value: 'application/manifest+json' }, ], }, { source: '/sw.js', headers: [ { key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' }, { key: 'Service-Worker-Allowed', value: '/' }, ], }, ] : []), // 全域安全標頭 { source: '/(.*)', headers: [ // 僅 production 送出 HSTS:本機/dev 常見 http://localhost,強制 HTTPS // 會破壞開發體驗。正式環境的邊緣代理需確實終止 TLS 並將 HTTP 導向 // HTTPS(見 nginx.conf),HSTS 才能保護首次造訪與後續降級攻擊。 ...(!isDev ? [ { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains', }, ] : []), { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-XSS-Protection', value: '1; mode=block' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, { key: 'Content-Security-Policy', value: [ "default-src 'self'", scriptSrc, // Service Worker 註冊需要明確的 worker-src;manifest 需要 manifest-src, // 否則會被 default-src 'self' 以外的嚴格政策擋下(PWA 無法註冊/安裝)。 "worker-src 'self'", "manifest-src 'self'", "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com", "font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com", "img-src 'self' data: https:", "connect-src 'self' https://accounts.google.com https://challenges.cloudflare.com", "frame-src https://accounts.google.com https://challenges.cloudflare.com", "object-src 'none'", "base-uri 'self'", "form-action 'self'", ].join('; '), }, ], }, ]; }, }; export default nextConfig;