mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix (self-hosted Studio image packaging). ## What is the current behavior? Since the Next 16.3.5 bump, the self-hosted `next build` (`output: 'standalone'`) traces Next's optional `sharp` dependency into `.next/standalone`. Next 16.3.5 upgraded `@vercel/nft` (vercel/next.js#93979), so tracing now follows `module-sync` export conditions, and Next only excludes sharp from the trace when it detects Vercel. Effects: - The self-hosted standalone output carries the `sharp` and `@img/*` native binaries, roughly 10 to 20 percent of the image. - The slim-services Studio image fails: it loads every `.node` file eagerly, and sharp's binary segfaults on linux/amd64 without the matching libvips shared library. `apps/studio` does not depend on `sharp` directly. The last Studio image without sharp in the trace was `2026.09.14`. ## What is the new behavior? Single-file change to `apps/studio/next.config.ts`. When `NEXT_PUBLIC_IS_PLATFORM` is not `'true'`: - `images.unoptimized: true`, so `next/image` renders plain `<img>` and the server 404s `/_next/image` before sharp is ever loaded. This matches what the TanStack build already does via `compat/next/image.tsx`. - `outputFileTracingExcludes` drops `**/node_modules/sharp/**` and `**/node_modules/@img/**` from the standalone trace. Hosted Studio is unchanged: `unoptimized` stays `false`, the exclude key is omitted, and image optimization keeps running on Vercel. Self-hosted CSP is `frame-ancestors 'none'` only, so loading remote avatars directly instead of through `/_next/image` does not hit a CSP rule. Also hoists the existing `isPlatform` const from `redirects()` to module scope. **Why the globs start with `../../`.** Studio builds with Turbopack, which resolves `outputFileTracingExcludes` relative to the app directory. A plain `**/node_modules/sharp/**` becomes `apps/studio/**/node_modules/sharp/**` and never matches the pnpm store hoisted to the monorepo root. Each leading `../` moves the glob root up one level (`relativize_glob` in Next's `crates/next-core/src/util.rs`), so `../../` anchors the pattern at the repo root. The webpack path applies the same globs unprefixed for the server trace, so this is Turbopack-specific. ## Additional context Considered and rejected: a Next.js issue. Next intentionally ships sharp for self-hosted `next start` image optimization and intentionally excludes it only on Vercel. Opting out per app is the supported path. Test plan: - CI: typecheck, lint, Prettier, Studio unit tests, Studio Docker Build. - Verified on this PR with a temporary step in the Studio Docker Build workflow that ran `find` inside the production image for `node_modules/sharp*` and `node_modules/@img*` files. It failed on the first commit (globs rooted at `apps/studio`, image still contained `@img/colour`) and passed once the globs were anchored at the repo root. The step was removed before merge. - Hosted preview should still serve optimized images from `/_next/image`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01SZHcgqB2qNqCvWsFJ9Qvo8 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Improved configuration handling for platform and self-hosted deployments. * Self-hosted builds now avoid bundling unnecessary image-processing binaries, while platform deployments retain image optimization support. * Clarified configuration comments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
269 lines
8.2 KiB
TypeScript
269 lines
8.2 KiB
TypeScript
/* eslint-disable no-restricted-exports */
|
|
|
|
import bundleAnalyzer from '@next/bundle-analyzer'
|
|
import { withSentryConfig } from '@sentry/nextjs'
|
|
import type { NextConfig } from 'next'
|
|
|
|
import { getCSP } from './csp'
|
|
import {
|
|
getMaintenanceRedirects,
|
|
PLATFORM_REDIRECTS,
|
|
SELF_HOSTED_REDIRECTS,
|
|
SHARED_REDIRECTS,
|
|
} from './redirects.shared'
|
|
|
|
const withBundleAnalyzer = bundleAnalyzer({
|
|
enabled: process.env.ANALYZE === 'true',
|
|
})
|
|
|
|
function getAssetPrefix() {
|
|
// If not force enabled, but not production env, disable CDN
|
|
if (process.env.FORCE_ASSET_CDN !== '1' && process.env.VERCEL_ENV !== 'production') {
|
|
return undefined
|
|
}
|
|
|
|
// Force disable CDN
|
|
if (process.env.FORCE_ASSET_CDN === '-1') {
|
|
return undefined
|
|
}
|
|
|
|
const SUPABASE_ASSETS_URL =
|
|
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
|
? 'https://frontend-assets.supabase.green'
|
|
: 'https://frontend-assets.supabase.com'
|
|
|
|
return `${SUPABASE_ASSETS_URL}/${process.env.SITE_NAME}/${process.env.VERCEL_GIT_COMMIT_SHA?.substring(0, 12) ?? 'unknown'}`
|
|
}
|
|
|
|
const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
|
|
|
|
const marketplaceApiUrl = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
|
|
? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL)
|
|
: null
|
|
|
|
const marketplaceApiProtocol: 'http' | 'https' | null =
|
|
marketplaceApiUrl?.protocol === 'https:'
|
|
? 'https'
|
|
: marketplaceApiUrl?.protocol === 'http:'
|
|
? 'http'
|
|
: null
|
|
|
|
// Use `satisfies` instead of `: NextConfig` so TypeScript preserves narrow
|
|
// inferred types (e.g. async headers → Promise). This avoids TS2345 when
|
|
// wrapper functions (bundle-analyzer, sentry) resolve their `next` peer
|
|
// types to a different major version than studio's own next dependency.
|
|
const nextConfig = {
|
|
basePath: process.env.NEXT_PUBLIC_BASE_PATH,
|
|
assetPrefix: getAssetPrefix(),
|
|
output: 'standalone',
|
|
experimental: {
|
|
clientRouterFilter: false,
|
|
turbopackFileSystemCacheForBuild: false,
|
|
},
|
|
async rewrites() {
|
|
return [
|
|
{
|
|
source: `/.well-known/vercel/flags`,
|
|
destination: `https://supabase.com/.well-known/vercel/flags`,
|
|
basePath: false as const,
|
|
},
|
|
]
|
|
},
|
|
async redirects() {
|
|
// Rules live in `redirects.shared.ts` (shared with `vercel.ts`). Next
|
|
// auto-prepends `basePath` to source and destination on its own,
|
|
// except for the special `/` → basePath bounce below which opts out
|
|
// via `basePath: false`.
|
|
const maintenance = process.env.MAINTENANCE_MODE === 'true'
|
|
return [
|
|
...(isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS),
|
|
...SHARED_REDIRECTS,
|
|
...(process.env.NEXT_PUBLIC_BASE_PATH?.length
|
|
? [
|
|
{
|
|
source: '/',
|
|
destination: process.env.NEXT_PUBLIC_BASE_PATH,
|
|
basePath: false as const,
|
|
permanent: false,
|
|
},
|
|
]
|
|
: []),
|
|
...getMaintenanceRedirects(maintenance),
|
|
]
|
|
},
|
|
async headers() {
|
|
return [
|
|
{
|
|
source: '/(.*?)',
|
|
headers: [
|
|
{
|
|
key: 'X-Frame-Options',
|
|
value: 'DENY',
|
|
},
|
|
{
|
|
key: 'X-Content-Type-Options',
|
|
value: 'nosniff',
|
|
},
|
|
{
|
|
key: 'Strict-Transport-Security',
|
|
value:
|
|
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && process.env.VERCEL === '1'
|
|
? 'max-age=31536000; includeSubDomains; preload'
|
|
: '',
|
|
},
|
|
{
|
|
key: 'Content-Security-Policy',
|
|
value:
|
|
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' ? getCSP() : "frame-ancestors 'none';",
|
|
},
|
|
{
|
|
key: 'Referrer-Policy',
|
|
value: 'strict-origin-when-cross-origin',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
source: '/.well-known/vercel/flags',
|
|
headers: [
|
|
{
|
|
key: 'content-type',
|
|
value: 'application/json',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
source: '/img/:slug*',
|
|
headers: [{ key: 'cache-control', value: 'public, max-age=2592000' }],
|
|
},
|
|
{
|
|
source: '/favicon/:slug*',
|
|
headers: [{ key: 'cache-control', value: 'public, max-age=86400' }],
|
|
},
|
|
{
|
|
source: '/(.*).ts',
|
|
headers: [{ key: 'content-type', value: 'text/typescript' }],
|
|
},
|
|
]
|
|
},
|
|
images: {
|
|
// Self-hosted: serve plain <img> (as the TanStack shim does) so Next never
|
|
// loads sharp. Hosted Studio optimizes images on Vercel.
|
|
unoptimized: !isPlatform,
|
|
dangerouslyAllowSVG: false,
|
|
remotePatterns: [
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'github.com',
|
|
port: '',
|
|
pathname: '**',
|
|
},
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'avatars.githubusercontent.com',
|
|
port: '',
|
|
pathname: '/u/*',
|
|
},
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'api-frameworks.vercel.sh',
|
|
port: '',
|
|
pathname: '**',
|
|
},
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'vercel.com',
|
|
port: '',
|
|
pathname: '**',
|
|
},
|
|
...(marketplaceApiUrl
|
|
? [
|
|
{
|
|
...(marketplaceApiProtocol ? { protocol: marketplaceApiProtocol } : {}),
|
|
hostname: marketplaceApiUrl.hostname,
|
|
port: marketplaceApiUrl.port,
|
|
pathname: '**',
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
// Keep Next's optional sharp dependency out of the self-hosted standalone
|
|
// output. It is unused with `unoptimized` above, and its native binaries
|
|
// break the slim-services Studio image (sharp's .node segfaults without
|
|
// libvips). Globs resolve from `apps/studio`; `../../` anchors them at the
|
|
// repo root where pnpm hoists the store.
|
|
...(isPlatform
|
|
? {}
|
|
: {
|
|
outputFileTracingExcludes: {
|
|
'*': ['../../**/node_modules/sharp/**/*', '../../**/node_modules/@img/**/*'],
|
|
},
|
|
}),
|
|
transpilePackages: ['ui', 'ui-patterns', 'common', 'shared-data', 'api-types', 'icons'],
|
|
serverExternalPackages: ['libpg-query'],
|
|
turbopack: {
|
|
rules: {
|
|
'*.md': {
|
|
loaders: ['raw-loader'],
|
|
as: '*.js',
|
|
},
|
|
// special case for Deno libs to be loaded as a raw text. They're passed as raw text to the Monaco editor.
|
|
'edge-runtime.d.ts': {
|
|
loaders: ['raw-loader'],
|
|
as: '*.js',
|
|
},
|
|
'lib.deno.d.ts': {
|
|
loaders: ['raw-loader'],
|
|
as: '*.js',
|
|
},
|
|
},
|
|
},
|
|
onDemandEntries: {
|
|
maxInactiveAge: 24 * 60 * 60 * 1000,
|
|
pagesBufferLength: 100,
|
|
},
|
|
typescript: {
|
|
// Typechecking is run via GitHub Action only for efficiency
|
|
// For production, we run typechecks separate from the build command (pnpm typecheck && pnpm build)
|
|
ignoreBuildErrors: true,
|
|
},
|
|
} satisfies NextConfig
|
|
|
|
// Make sure adding Sentry options is the last code to run before exporting, to
|
|
// ensure that your source maps include changes from all other Webpack plugins
|
|
const platformConfig =
|
|
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' ? withBundleAnalyzer(nextConfig) : nextConfig
|
|
|
|
export default process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && process.env.VERCEL === '1'
|
|
? withSentryConfig(platformConfig, {
|
|
silent: false,
|
|
debug: true,
|
|
|
|
// For all available options, see:
|
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
|
|
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
|
widenClientFileUpload: true,
|
|
|
|
// Automatically annotate React components to show their full name in breadcrumbs and session replay
|
|
reactComponentAnnotation: {
|
|
enabled: true,
|
|
},
|
|
|
|
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
|
disableLogger: true,
|
|
|
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
|
// See the following for more information:
|
|
// https://docs.sentry.io/product/crons/
|
|
// https://vercel.com/docs/cron-jobs
|
|
automaticVercelMonitors: true,
|
|
|
|
// Annotate bundles at build time so thirdPartyErrorFilterIntegration can
|
|
// distinguish our code from browser extensions / injected scripts at runtime.
|
|
unstable_sentryWebpackPluginOptions: {
|
|
applicationKey: 'supabase-studio',
|
|
},
|
|
})
|
|
: platformConfig
|