Files
readest/apps/readest-app/next.config.mjs
T
Huang Xin 6514d4aa58 build(web): standalone Docker image + drop Turbopack build cache (#4619)
The Docker production-stage opts into Next.js `output: 'standalone'` via a
BUILD_STANDALONE env flag, so it ships only the traced runtime (server.js +
hoisted node_modules + static/public) and runs `node server.js` instead of
pnpm over the full source tree. The flag — and `outputFileTracingRoot`,
which traces from the monorepo root so workspace packages are included — is
set only in the Dockerfile build stage. Every other path keeps its original
output: Tauri `export`, local `build-web`, dev, and the Cloudflare/OpenNext
deploy (which forces standalone itself via NEXT_PRIVATE_STANDALONE).

Disable the experimental `turbopackFileSystemCacheForBuild`: a build
interrupted mid-compile leaves a partial cache that the next build
mishandles, fanning out workers until it exhausts host RAM. Remove the
pull-request CI step that cached `.next/cache` for it, now unused.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:21:24 +02:00

156 lines
5.1 KiB
JavaScript

import withSerwistInit from '@serwist/next';
import withBundleAnalyzer from '@next/bundle-analyzer';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isDev = process.env['NODE_ENV'] === 'development';
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
if (isDev) {
const { initOpenNextCloudflareForDev } = await import('@opennextjs/cloudflare');
initOpenNextCloudflareForDev();
}
const exportOutput = appPlatform !== 'web' && !isDev;
// Opt-in standalone output, set only by the Docker production build
// (Dockerfile). Every other path keeps the original behavior: Tauri `export`,
// local `build-web` (output undefined), dev, and the Cloudflare/OpenNext
// deploy — which forces standalone itself via NEXT_PRIVATE_STANDALONE.
const standaloneOutput = !exportOutput && process.env['BUILD_STANDALONE'] === 'true';
/** @type {import('next').NextConfig} */
const nextConfig = {
// Ensure Next.js uses SSG instead of SSR
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
// The Docker production image opts into a self-contained `.next/standalone`
// tree (see Dockerfile) so it can ship only the traced runtime; all other
// web builds fall back to the default server output.
output: exportOutput ? 'export' : standaloneOutput ? 'standalone' : undefined,
// Monorepo: trace from the repo root so workspace packages land in the
// standalone tree. Only relevant to — and only set for — the Docker build.
outputFileTracingRoot: standaloneOutput ? path.join(__dirname, '../../') : undefined,
pageExtensions: exportOutput ? ['jsx', 'tsx'] : ['js', 'jsx', 'ts', 'tsx'],
// Note: This feature is required to use the Next.js Image component in SSG mode.
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
images: {
unoptimized: true,
},
devIndicators: false,
experimental: {
// Dev caching is on by default since Next 16.1. We deliberately do NOT
// enable Turbopack's build cache (turbopackFileSystemCacheForBuild, beta):
// a build interrupted mid-compile leaves a partial cache that the next
// build mishandles, fanning out workers until it exhausts RAM.
turbopackFileSystemCacheForDev: true,
},
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: '',
reactStrictMode: true,
serverExternalPackages: ['isows'],
allowedDevOrigins: ['192.168.2.120'],
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
nunjucks: 'nunjucks/browser/nunjucks.js',
// `js-mdict` is consumed as TS source via tsconfig paths from
// `packages/js-mdict/src/`; its sources `import 'fflate'` directly.
// Without an alias, webpack walks up from that source location and
// can't find fflate (only installed in this app's node_modules).
fflate: path.resolve(__dirname, 'node_modules/fflate'),
...(appPlatform !== 'web' ? { '@tursodatabase/database-wasm': false } : {}),
};
return config;
},
turbopack: {
resolveAlias: {
nunjucks: 'nunjucks/browser/nunjucks.js',
// Turbopack rejects absolute paths in resolveAlias ("server relative
// imports not implemented") — use a project-relative path.
fflate: './node_modules/fflate',
...(appPlatform !== 'web' ? { '@tursodatabase/database-wasm': './src/utils/stub.ts' } : {}),
},
},
transpilePackages: [
'ai',
'ai-sdk-ollama',
'@ai-sdk/react',
'@assistant-ui/react',
'@assistant-ui/react-ai-sdk',
'@assistant-ui/react-markdown',
'streamdown',
...(isDev
? []
: [
'i18next-browser-languagedetector',
'react-i18next',
'i18next',
'@tauri-apps',
'highlight.js',
'foliate-js',
'marked',
]),
],
async rewrites() {
return [
{
source: '/reader/:ids',
destination: '/reader?ids=:ids',
},
{
source: '/o/book/:hash/annotation/:id',
destination: '/o?book=:hash&note=:id',
},
{
source: '/s/:token',
destination: '/s?token=:token',
},
];
},
async headers() {
return [
{
source: '/.well-known/apple-app-site-association',
headers: [
{
key: 'Content-Type',
value: 'application/json',
},
],
},
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: isDev
? 'public, max-age=0, must-revalidate'
: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
const pwaDisabled = isDev || appPlatform !== 'web';
const withPWA = pwaDisabled
? (config) => config
: withSerwistInit({
swSrc: 'src/sw.ts',
swDest: 'public/sw.js',
cacheOnNavigation: true,
reloadOnOnline: true,
disable: false,
register: true,
scope: '/',
});
const withAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withPWA(withAnalyzer(nextConfig));