next.config.ts
Rama main · réplica de lectura del repositorio original.
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import createNextIntlPlugin from 'next-intl/plugin'
import type { NextConfig } from 'next'
import { requestedEdition } from './scripts/edition.mjs'
import { billingAdaptersPath, catalogPath } from './scripts/edition-aliases.mjs'
/**
* Which edition this build is. Read from the build environment here and in
* scripts/build-collab.mjs, which also records it for migrate and provision --
* see scripts/edition.mjs for why it is never a runtime switch.
*/
const edition = requestedEdition()
/**
* The website's German addresses, from before English became the unprefixed
* language (src/cloud/site/routes.ts explains the change).
*
* Permanent, and that is the whole point: these five are in sent mails, in the
* legal footer of every page § 5 ECG requires, and in whatever a search engine
* has already indexed. A 301 keeps every one of those links working and hands
* what it is worth to the new address; a 404 would throw both away.
*
* `/faq` is deliberately absent. It is English now, and it never existed as a
* German address, so there is nothing to redirect and a rule here would break
* the page it points away from.
*
* `/en` is the mirror case: English has no prefix, so the address is not a
* page -- but it is the first thing somebody types who has seen `/de` or
* `/fr`, and sending them to the front page is kinder than a 404.
*/
const websiteRedirects = [
{ source: '/preise', destination: '/de/preise', permanent: true },
{ source: '/impressum', destination: '/de/impressum', permanent: true },
{ source: '/agb', destination: '/de/agb', permanent: true },
{ source: '/datenschutz', destination: '/de/datenschutz', permanent: true },
{ source: '/avv', destination: '/de/avv', permanent: true },
{ source: '/en', destination: '/', permanent: true },
{ source: '/en/:path*', destination: '/:path*', permanent: true },
]
const nextConfig: NextConfig = {
// Single self-contained artifact for the on-prem Docker image.
output: 'standalone',
// Pinned rather than inferred. Next guesses the workspace root from the
// nearest lockfile, and in a git worktree (or any checkout under another
// project) it guesses wrong -- the traced bundle then lands somewhere else
// and `.next/standalone` comes out holding nothing but a package.json.
// Inside the Docker image there is only one lockfile, so the failure is
// invisible exactly where it would hurt most to discover it late.
outputFileTracingRoot: dirname(fileURLToPath(import.meta.url)),
/**
* One copy of Yjs in the process, not one per chunk.
*
* The editor bundle and the MCP route both reach for it, and two bundled
* copies mean two different Y.Doc classes: y-protocols checks instances, so
* a document created by one copy silently fails to sync through the other.
* Yjs prints "Yjs was already imported" when this happens -- a warning that
* describes a real, subtle breakage rather than noise. `ws` is here because
* it is a native-ish server library with no business in a bundle.
*/
serverExternalPackages: ['yjs', 'y-protocols', 'lib0', 'ws'],
reactStrictMode: true,
poweredByHeader: false,
turbopack: {
resolveAlias: {
'@gw/edition': `./src/server/edition/${edition}.ts`,
// The private cloud build points this at the real accounting and payment
// adapters; every other build refuses to bill.
'@gw/billing-adapters': billingAdaptersPath(
process.env,
'./src/cloud/billing/adapters/unavailable.ts',
),
'@gw/home':
edition === 'cloud'
? './src/cloud/site/home.tsx'
: './src/components/home/community-home.tsx',
// The Discover catalogue. The private cloud build points this at the real
// one; every other build gets a catalogue with nothing in it -- which is
// a state the screens have to render anyway, so it is not an error path.
'@gw/catalog': catalogPath(process.env, './src/cloud/catalog/unavailable.ts'),
},
},
// The cloud's website -- pricing, legal texts, registration -- lives in
// `page.cloud.tsx` files. Only a cloud build counts them as routes; in a
// community build they are ordinary modules nothing imports, and the routes
// do not exist.
pageExtensions: edition === 'cloud' ? ['cloud.tsx', 'cloud.ts', 'tsx', 'ts'] : ['tsx', 'ts'],
// Read from disk at request time (src/cloud/legal/documents.ts), so they have
// to be named for the standalone output to carry them.
...(edition === 'cloud'
? { outputFileTracingIncludes: { '/*': ['./src/cloud/legal/*.md'] } }
: {}),
/**
* The two settings pages were renamed from what they are made of to what they
* are for: "Passkeys" became Security, "Token" became AI Connection. The old
* addresses live on in bookmarks, in the README of older releases and in
* OAuth metadata that a client may have cached, so they keep working.
*/
async redirects() {
return [
{ source: '/settings/passkeys', destination: '/settings/security', permanent: true },
{ source: '/settings/tokens', destination: '/settings/ai-connection', permanent: true },
...(edition === 'cloud' ? websiteRedirects : []),
]
},
typedRoutes: true,
}
/**
* Required even though this application has no locale routing.
*
* The plugin's job here is to alias `next-intl/config` to the request file
* below; without it `getTranslations()` and `getLocale()` in a Server Component
* have no configuration and throw. What "without i18n routing" removes is the
* middleware and the [locale] segment -- not this.
*
* `createMessagesDeclaration` types every message key off the German catalog,
* which turns a typo into a compile error instead of a string that renders as
* its own key in production.
*/
const withNextIntl = createNextIntlPlugin({
requestConfig: './src/i18n/request.ts',
experimental: { createMessagesDeclaration: './src/messages/de.json' },
})
export default withNextIntl(nextConfig)