next.config.ts
Rama main · réplica de lectura del repositorio original.
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;