Repository navigation
Expand file tree
/
Copy pathnext.config.ts
More file actions
166 lines (162 loc) · 6.36 KB
/
Copy pathnext.config.ts
File metadata and controls
166 lines (162 loc) · 6.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import type { NextConfig } from "next";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const projectRoot = dirname(fileURLToPath(import.meta.url));
const isolatedBuild = !!process.env.NEXT_DIST_DIR && process.env.NEXT_DIST_DIR !== ".next";
const devOriginHosts = Array.from(
new Set(
[process.env.AUTH_URL_DEV, process.env.NEXT_PUBLIC_APP_URL, process.env.LAN_IP]
.map((value) => {
if (!value) return null;
if (!value.includes("://")) return value.split(":")[0] || null;
try {
return new URL(value).hostname;
} catch {
return null;
}
})
.filter((value): value is string => Boolean(value)),
),
);
const config: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
// Multiple lockfiles may exist above a Codex worktree on developer
// machines. Keep Turbopack resolution and file watching scoped to Forge.
turbopack: {
root: projectRoot,
},
allowedDevOrigins: devOriginHosts.length > 0 ? devOriginHosts : undefined,
// `standalone` is for the prod Docker image. The E2E build (NEXT_DIST_DIR=
// .next-e2e) is served with plain `next start`, which doesn't support
// standalone — so skip it there to keep `next start` clean.
output: isolatedBuild ? undefined : "standalone",
// Allow an isolated build dir (E2E sets NEXT_DIST_DIR=.next-e2e) so a
// parallel `next dev` on the same checkout can't clobber the build output.
distDir: process.env.NEXT_DIST_DIR || ".next",
// `ws` (Codex app-server connector) must NOT be bundled/minified — the
// bundler mangles its frame-masking util, yielding "b.mask is not a
// function" when the client masks an outgoing frame. Load it unbundled.
serverExternalPackages: [
"ioredis",
"bullmq",
"ws",
"web-push",
"https-proxy-agent",
"agent-base",
],
eslint: {
// Lint in CI / pre-commit via `pnpm lint`; don't gate production
// build on warnings.
ignoreDuringBuilds: true,
},
webpack: (config, { nextRuntime }) => {
// Middleware forces an edge-runtime bundle pass even for server-only
// code. ioredis + bullmq can't be bundled for edge — mark as external
// there so the instrumentation hook (which boots the BullMQ workers
// in-process) doesn't drag node:* builtins into the edge graph.
// The function-form catches every `node:*` builtin since BullMQ pulls
// node:crypto / node:net / node:worker_threads transitively.
if (nextRuntime === "edge") {
const existing = config.externals ?? [];
config.externals = [
...(Array.isArray(existing) ? existing : [existing]),
"ioredis",
"bullmq",
"web-push",
"https-proxy-agent",
"agent-base",
(
{ request }: { request?: string },
callback: (err?: Error | null, result?: string) => void,
) => {
if (request && request.startsWith("node:")) {
return callback(null, `commonjs ${request}`);
}
callback();
},
];
}
return config;
},
// Prisma's generated client and engines are loaded dynamically so
// Next's trace doesn't pick them up. Include them explicitly so the
// standalone bundle runs without node_modules on the side.
outputFileTracingIncludes: {
"*": [
"./node_modules/.pnpm/@prisma+client*/**/*",
"./node_modules/.pnpm/prisma*/**/*",
"./node_modules/@prisma/client/**/*",
"./prisma/schema.prisma",
],
},
async rewrites() {
return [
// The in-app VitePress docs live under /public/docs/ as static
// files. With `cleanUrls: false` in vitepress config, the home
// page is `index.html` — but Next's default trailing-slash
// redirect rewrites `/docs/` → `/docs`, which then 404s because
// the public folder doesn't auto-extension-resolve. Map both
// `/docs` and `/docs/` to the index file so the iframe lands.
{ source: "/docs", destination: "/docs/index.html" },
{ source: "/docs/", destination: "/docs/index.html" },
];
},
async headers() {
return [
// Default: deny framing site-wide (clickjacking guard).
{
source: "/:path*",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
],
},
// Keep the service worker fresh. A stale SW is harder to reason
// about than stale app chunks because it controls future requests.
{
source: "/sw.js",
headers: [
{ key: "Content-Type", value: "application/javascript; charset=utf-8" },
{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" },
{ key: "Service-Worker-Allowed", value: "/" },
],
},
{
source: "/shared/artifacts/:path*",
headers: [
{ key: "Cache-Control", value: "private, no-store" },
{ key: "Referrer-Policy", value: "no-referrer" },
{ key: "X-Robots-Tag", value: "noindex, nofollow, noarchive" },
{
key: "Content-Security-Policy",
value:
"default-src 'self'; base-uri 'none'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self'",
},
],
},
// Override for the in-app VitePress docs at /docs/*. The workspace
// /docs route iframes this path; SAMEORIGIN keeps clickjacking
// guards in place while letting the Forge shell embed it.
// VitePress static assets (HTML, JS, CSS, images) all live under
// /docs/ in the public dir thanks to `base: "/docs/"` in
// docs/.vitepress/config.ts, so the two matchers below cover the
// whole site.
{
source: "/docs",
headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
},
{
source: "/docs/:path*",
headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
},
];
},
};
export default config;