Ctrl K
ComunidadGuías…

next.config.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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;