mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
f108d51493
`Prisma` imported from `@prisma/client` was bundled into the app layer while
`dbRead`/`dbWrite` reached a second copy through the transpiled
`@civitai/db-schema`. `$queryRaw` identifies its tagged-template argument with
`instanceof Sql`, so a `Prisma.join()` built by the other copy failed that check
and was bound as a plain value, producing:
42883 operator does not exist: integer = jsonb
This 500'd every `homeBlock.getHomeBlock` on the homepage feed, and was latent
for the ~20 other server modules that use `Prisma.join`. `require.resolve`
reports a single physical path from every workspace dir, so the duplication is
only visible at the bundler layer.
`@prisma/client` is already in Next's built-in serverExternalPackages defaults;
listing it explicitly is what stops `transpilePackages` from pulling it back in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
436 lines
18 KiB
JavaScript
436 lines
18 KiB
JavaScript
// @ts-check
|
|
import { withAxiom } from '@civitai/next-axiom';
|
|
import bundlAnalyzer from '@next/bundle-analyzer';
|
|
import CircularDependencyPlugin from 'circular-dependency-plugin';
|
|
import { createRequire } from 'module';
|
|
const require = createRequire(import.meta.url);
|
|
const packageJson = require('./package.json');
|
|
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
const analyze = process.env.ANALYZE === 'true';
|
|
const includeCircularDependencyPlugin = process.env.CIRCULAR_DEPENDENCY_PLUGIN === 'true';
|
|
const shouldOptimizeImports = (isDev && analyze) || isProd;
|
|
|
|
const withBundleAnalyzer = bundlAnalyzer({
|
|
enabled: analyze,
|
|
});
|
|
|
|
/**
|
|
* Don't be scared of the generics here.
|
|
* All they do is to give us autocompletion when using this.
|
|
*
|
|
* @template {import('next').NextConfig} T
|
|
* @param {T} config - A generic parameter that flows through to the return type
|
|
* @constraint {{import('next').NextConfig}}
|
|
*/
|
|
function defineNextConfig(config) {
|
|
return withBundleAnalyzer(config);
|
|
}
|
|
|
|
export default defineNextConfig(
|
|
withAxiom({
|
|
env: {
|
|
version: packageJson.version,
|
|
// The client login helpers need the hub origin. Reuse AUTH_JWT_ISSUER (the server's single hub-URL source)
|
|
// by exposing it to the client bundle as NEXT_PUBLIC_AUTH_HUB_URL — so there's no separate var to set. An
|
|
// explicit NEXT_PUBLIC_AUTH_HUB_URL still wins if provided. (AUTH_JWT_ISSUER is public: the JWT `iss` /
|
|
// JWKS origin.)
|
|
NEXT_PUBLIC_AUTH_HUB_URL:
|
|
process.env.NEXT_PUBLIC_AUTH_HUB_URL ?? process.env.AUTH_JWT_ISSUER,
|
|
},
|
|
// webpack: (config, options) => {
|
|
// if (isDev && !options.isServer) {
|
|
// config.plugins.push(
|
|
// new CircularDependencyPlugin({
|
|
// exclude: /node_modules|\.d\.ts/, // Ignore types and external modules
|
|
// failOnError: true, // Fail build on cycle
|
|
// allowAsyncCycles: false, // Disallow lazy cycles (recommended)
|
|
// cwd: process.cwd(), // Base path for clearer output
|
|
// // `onStart` is called before the cycle detection starts
|
|
// // onStart({ compilation }) {
|
|
// // console.log('start detecting webpack modules cycles');
|
|
// // },
|
|
// // `onDetected` is called for each module that is cyclical
|
|
// onDetected({ module: webpackModuleRecord, paths, compilation }) {
|
|
// // `paths` will be an Array of the relative module paths that make up the cycle
|
|
// // `module` will be the module record generated by webpack that caused the cycle
|
|
// compilation.errors.push(new Error(paths.join(' -> ')));
|
|
// },
|
|
// // `onEnd` is called before the cycle detection ends
|
|
// // onEnd({ compilation }) {
|
|
// // console.log('end detecting webpack modules cycles');
|
|
// // },
|
|
// })
|
|
// );
|
|
// }
|
|
|
|
// return config;
|
|
// },
|
|
// Turbopack is the default bundler as of Next 16. The OpenTelemetry packages
|
|
// that produced the `require-in-the-middle` webpack warnings are listed in
|
|
// `serverExternalPackages` below, so Turbopack externalizes them and never
|
|
// emits those warnings — an empty config just acknowledges we're on Turbopack
|
|
// and silences Next's "webpack config with no turbopack config" build error.
|
|
turbopack: {},
|
|
// Per-branch build dir. Turbopack's dev filesystem cache (~8GB) is invalidated
|
|
// wholesale by an in-place branch switch, so the dev daemon points each branch at
|
|
// its own dir and keeps them warm instead of purging. Unset -> stock `.next`.
|
|
distDir: process.env.NEXT_DIST_DIR || '.next',
|
|
allowedDevOrigins: ['civitai-dev.green', 'civitai-dev.blue', 'civitai-dev.red'],
|
|
// Retained for the `next build --webpack` fallback path; ignored under Turbopack.
|
|
webpack: (config) => {
|
|
config.ignoreWarnings = [
|
|
{ module: /require-in-the-middle/ },
|
|
{ module: /@opentelemetry\/instrumentation/ },
|
|
];
|
|
return config;
|
|
},
|
|
reactStrictMode: true,
|
|
// Source maps for prod CPU-profile de-minification.
|
|
//
|
|
// Under Turbopack (our prod bundler, Next 16), the ONLY source-map lever is the
|
|
// experimental `turbopackSourceMaps` flag, whose build-time default IS
|
|
// `productionBrowserSourceMaps`. So setting this `true` turns on map emission for
|
|
// BOTH client (`.next/static/**/*.js.map`) and server (`.next/server/**/*.js.map`)
|
|
// chunks. Turbopack ignores `experimental.serverSourceMaps` (webpack-only) — that
|
|
// flag below only matters for the `next build --webpack` fallback path.
|
|
//
|
|
// Maps are inert at runtime: the Node server never loads a `.js.map` unless an
|
|
// inspector / error-stack resolver reads it, so there is NO
|
|
// request-path perf cost.
|
|
// They are NOT served to browsers for server chunks (those live in `.next/server`,
|
|
// which is not a static-served directory). Cost is build time + image size only.
|
|
//
|
|
// IMPORTANT: `output:'standalone'` traces required files via @vercel/nft, which
|
|
// follows `import`/`require`/`fs` — it does NOT trace sibling `.js.map` files, so
|
|
// the server maps are emitted to `.next/server` but DROPPED from `.next/standalone`.
|
|
// The runtime image does NOT ship these maps (the RUNNER stage copies only
|
|
// standalone + static). The build instead publishes them as a separate
|
|
// `civitai-web-maps:<tag>` artifact (Dockerfile `maps` target + the pipeline),
|
|
// fetched on demand by `scripts/resolve-cpuprofile.mjs --image <tag>` to
|
|
// de-minify a captured profile — keeping the runtime image lean.
|
|
productionBrowserSourceMaps: true,
|
|
// Next.js i18n docs: https://nextjs.org/docs/advanced-features/i18n-routing
|
|
i18n: {
|
|
locales: ['en'],
|
|
defaultLocale: 'en',
|
|
},
|
|
generateEtags: false,
|
|
compress: false,
|
|
images: {
|
|
remotePatterns: [
|
|
{ hostname: 's3.us-west-1.wasabisys.com' },
|
|
{ hostname: 'model-share.s3.us-west-1.wasabisys.com' },
|
|
{ hostname: 'civitai-prod.s3.us-west-1.wasabisys.com' },
|
|
{ hostname: 'civitai-dev.s3.us-west-1.wasabisys.com' },
|
|
{ hostname: 'image.civitai.com' },
|
|
],
|
|
// domains: [
|
|
// 's3.us-west-1.wasabisys.com',
|
|
// 'model-share.s3.us-west-1.wasabisys.com',
|
|
// 'civitai-prod.s3.us-west-1.wasabisys.com',
|
|
// 'civitai-dev.s3.us-west-1.wasabisys.com',
|
|
// 'image.civitai.com',
|
|
// ],
|
|
},
|
|
compiler:
|
|
process.env.NODE_ENV === 'production'
|
|
? {
|
|
reactRemoveProperties: { properties: ['^data-testid$'] },
|
|
// removeConsole: true,
|
|
}
|
|
: {},
|
|
transpilePackages: [
|
|
'superjson',
|
|
'@civitai/db-schema',
|
|
'@civitai/db',
|
|
'@civitai/db-queries',
|
|
'@civitai/shared',
|
|
'@civitai/buzz',
|
|
'@civitai/redis',
|
|
'@civitai/clickhouse',
|
|
'@civitai/axiom',
|
|
'@civitai/telemetry',
|
|
'@civitai/auth',
|
|
'@civitai/notifications',
|
|
],
|
|
// Renamed from experimental.serverComponentsExternalPackages → top-level serverExternalPackages in Next 15
|
|
serverExternalPackages: [
|
|
'redis', '@redis/client', '@redis/bloom', '@redis/json', '@redis/search', '@redis/time-series',
|
|
'@opentelemetry/sdk-node', '@opentelemetry/instrumentation', '@opentelemetry/instrumentation-http',
|
|
'@opentelemetry/instrumentation-redis', '@prisma/instrumentation',
|
|
// Bundling this gives the app layer its own copy of the Prisma runtime while
|
|
// `dbRead`/`dbWrite` (reached through the transpiled `@civitai/db-schema`) hold a
|
|
// second one. `$queryRaw` identifies its template argument with `instanceof Sql`,
|
|
// so a `Prisma.join()` built by the other copy fails that check and is bound as a
|
|
// plain value -> `operator does not exist: integer = jsonb`.
|
|
'@prisma/client',
|
|
// NOTE: the logs-pipeline packages (@opentelemetry/api, /api-logs, /sdk-logs,
|
|
// /exporter-logs-otlp-proto) are deliberately NOT externalized here. Adding them
|
|
// fails the image build, so it needs to be its own change with a full build as its
|
|
// gate. The logs bridge does not depend on it: it binds to the Logs API lazily, so
|
|
// a second bundled module copy is survivable, and `no_provider` on its skip counter
|
|
// is the runtime signal if one ever appears.
|
|
'@pyroscope/nodejs', '@datadog/pprof',
|
|
],
|
|
// Several entry points read markdown from src/static-content at runtime via fs
|
|
// (dynamic string paths that @vercel/nft can't trace). With output:'standalone'
|
|
// the build only ships traced files, so without these explicit includes the
|
|
// markdown is missing in the deployed image and every read hits ENOENT ->
|
|
// 500/404 (works locally because the full source tree is present). Top-level as
|
|
// of Next 15 (lived under `experimental` on Next 14). Keyed by each read site.
|
|
outputFileTracingIncludes: {
|
|
'/safety': ['./src/static-content/**/*'],
|
|
'/region-blocked': ['./src/static-content/**/*'],
|
|
'/content/[[...slug]]': ['./src/static-content/**/*'],
|
|
'/api/trpc/[trpc]': ['./src/static-content/**/*'],
|
|
'/api/v1/content/[[...slug]]': ['./src/static-content/**/*'],
|
|
// /api/og uses next/og's `ImageResponse`, which on the nodejs runtime
|
|
// lazily require()s `next/dist/compiled/@vercel/og/index.node.js` (plus its
|
|
// resvg/yoga WASM + fonts). @vercel/nft cannot follow that dynamic require,
|
|
// so with output:'standalone' the file is DROPPED from the image and every
|
|
// origin (cache-miss) /api/og render throws `Cannot find module ...
|
|
// index.node.js` -> 500. This became the dominant app-500 source (~1.9/s)
|
|
// after the Next 16.2.7 upgrade; Cloudflare edge-caching of OG images masks
|
|
// it for popular entities. Force-include the whole compiled @vercel/og dir
|
|
// (entry + WASM + fonts) for this route. Two version-agnostic globs (no
|
|
// hardcoded next@<hash>): the symlinked path, plus the real pnpm path with a
|
|
// `next@*` wildcard in case globby doesn't follow the node_modules/next
|
|
// symlink. Whichever matches copies the files; a non-matching glob is a no-op.
|
|
'/api/og': [
|
|
'./node_modules/next/dist/compiled/@vercel/og/**/*',
|
|
'./node_modules/.pnpm/next@*/node_modules/next/dist/compiled/@vercel/og/**/*',
|
|
],
|
|
},
|
|
experimental: {
|
|
// scrollRestoration: true,
|
|
cpus: 8,
|
|
serverSourceMaps: true,
|
|
// instrumentationHook removed in Next 15 — instrumentation.ts is enabled by default now
|
|
largePageDataBytes: 512 * 100000,
|
|
// Nested async chunking for the SERVER build. Next's own defaults table
|
|
// (node_modules/next/dist/docs/01-app/03-api-reference/08-turbopack.md) has
|
|
// `turbopackClientSideNestedAsyncChunking` defaulting to TRUE in build mode but
|
|
// `turbopackServerSideNestedAsyncChunking` defaulting to FALSE in *both* dev and
|
|
// build — so the server build never got the async-chunk dedup the client build has,
|
|
// and every async chunk group re-emits its whole module graph.
|
|
//
|
|
// Trade: ~72% fewer emitted server chunks and roughly half the server chunk bytes,
|
|
// in exchange for ~+8% CI build time and ~+33% peak builder RSS. Measurements live
|
|
// in the PR rather than here, so they don't rot when Next's chunker changes.
|
|
//
|
|
// Build only. It buys nothing in dev — chunk size is irrelevant to a dev server — and
|
|
// the walk over dynamic-import paths is paid on a graph that `_app` already makes large.
|
|
turbopackServerSideNestedAsyncChunking: isProd,
|
|
optimizePackageImports: [
|
|
'@civitai/client',
|
|
'./src/libs/form',
|
|
'lodash-es',
|
|
'@tabler/icons-react',
|
|
'@headlessui/react',
|
|
],
|
|
},
|
|
headers: async () => {
|
|
// Add X-Robots-Tag header to all pages matching /sitemap.xml and /sitemap-models.xml /sitemap-articles.xml, etc
|
|
const headers = [
|
|
{
|
|
source: '/sitemap(-\\w+)?.xml',
|
|
headers: [
|
|
{ key: 'X-Robots-Tag', value: 'noindex' },
|
|
{ key: 'Content-Type', value: 'application/xml' },
|
|
{ key: 'Cache-Control', value: 'public, max-age=86400, must-revalidate' },
|
|
],
|
|
},
|
|
];
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
headers.push({
|
|
source: '/:path*',
|
|
headers: [
|
|
{
|
|
key: 'X-Robots-Tag',
|
|
value: 'noindex',
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// Allow Kinguin checkout iframe on gift cards page - NO X-Frame-Options header
|
|
// Minimal CSP that only restricts frame-src to allow Kinguin
|
|
headers.push({
|
|
source: '/gift-cards',
|
|
headers: [
|
|
{
|
|
key: 'Content-Security-Policy',
|
|
value: "frame-src 'self' https://www.kinguin.net https://sandbox.kinguin.net https://gateway.kinguin.net https://*.kinguin.net;"
|
|
}
|
|
// NOTE: Intentionally NO X-Frame-Options header as per Kinguin's documentation
|
|
// NOTE: Only setting frame-src, letting other resources use browser defaults
|
|
],
|
|
});
|
|
|
|
// Apply X-Frame-Options to all pages EXCEPT gift-cards
|
|
headers.push({
|
|
source: '/((?!gift-cards).*)',
|
|
headers: [{ key: 'X-Frame-Options', value: 'DENY' }],
|
|
});
|
|
|
|
return headers;
|
|
},
|
|
poweredByHeader: false,
|
|
redirects: async () => {
|
|
// Note: the .red-host support-portal bounce is implemented as a
|
|
// Cloudflare Redirect Rule on the civitai.red zone. Config lives at
|
|
// ops/cloudflare/civitai-red-redirects.json. A host-conditional rule
|
|
// here would be pruned to nothing at build time anyway since
|
|
// SERVER_DOMAIN_* env vars aren't exposed as Docker build ARGs.
|
|
return [
|
|
{
|
|
source: '/api/download/training-data/:modelVersionId',
|
|
destination: '/api/download/models/:modelVersionId?type=Training%20Data',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/github/:path*',
|
|
destination: 'https://github.com/civitai/civitai/:path*',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/discord',
|
|
destination: 'https://discord.gg/civitai',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/twitter',
|
|
destination: 'https://twitter.com/HelloCivitai',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/reddit',
|
|
destination: 'https://reddit.com/r/civitai',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/instagram',
|
|
destination: 'https://www.instagram.com/hellocivitai/',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/tiktok',
|
|
destination: 'https://www.tiktok.com/@hellocivitai',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/youtube',
|
|
destination: 'https://www.youtube.com/@civitai',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/twitch',
|
|
destination: 'https://www.twitch.tv/civitai',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/ideas',
|
|
destination: 'https://github.com/civitai/civitai/discussions/categories/ideas',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/v/civitai-link-intro',
|
|
destination: 'https://youtu.be/EHUjiDgh-MI',
|
|
permanent: false,
|
|
},
|
|
{
|
|
source: '/v/civitai-link-installation',
|
|
destination: 'https://youtu.be/fs-Zs-fvxb0',
|
|
permanent: false,
|
|
},
|
|
{
|
|
source: '/v/ally-parting-message',
|
|
destination: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
permanent: false,
|
|
},
|
|
{
|
|
source: '/gallery/:path*',
|
|
destination: '/images/:path*',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/canny/bugs',
|
|
destination:
|
|
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/bugs',
|
|
destination:
|
|
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/support-portal',
|
|
destination:
|
|
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/leaderboard',
|
|
destination: '/leaderboard/overall',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/forms/bounty-refund',
|
|
destination: 'https://forms.clickup.com/8459928/f/825mr-8331/R30FGV9JFHLF527GGN',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/air/confirm',
|
|
destination: '/studio/confirm',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/education',
|
|
destination: 'https://education.civitai.com',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/cosmetic-shop',
|
|
destination: '/shop',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/shop/cosmetic-shop',
|
|
destination: '/shop',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/projectodyssey_season2',
|
|
destination: '/collections/6503138',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/creators-program',
|
|
destination: '/creator-program',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/research/rater',
|
|
destination: '/games/knights-of-new-order',
|
|
permanent: true,
|
|
},
|
|
{
|
|
// Reserved-name redirect: the legacy 'civitai' user account moved to
|
|
// 'CivitaiOfficial'. Handled here (not in middleware) so it runs at
|
|
// framework/edge level before any middleware, with no JS execution
|
|
// per request.
|
|
source: '/user/civitai',
|
|
destination: '/user/CivitaiOfficial',
|
|
permanent: true,
|
|
},
|
|
];
|
|
},
|
|
output: 'standalone',
|
|
})
|
|
);
|