Skip to content

Commit 275f3e9

Browse files
jon-bellclaude
andcommitted
next.config: emit server sourcemaps in coverage builds
Server Components like \`app/(auth-pages)/sign-in/page.tsx\` execute on the Node server, so their coverage lands in NODE_V8_COVERAGE (not in Playwright's V8 dump). But Next 15 omits sourcemaps from server bundles in production by default, so \`c8 report\` had no way to map \`.next/server/app/(auth-pages)/sign-in/page.js\` back to \`.tsx\` — explaining why server.lcov was only 5KB with 4 utility files instead of ~200 page sources. Force \`devtool = "source-map"\` for the server build when COVERAGE=1. Apply only to isServer (client gets sourcemaps via \`productionBrowserSourceMaps: true\` above; setting both produces an empty \`.next/static/\`). Verified locally: \`.next/server\` now contains 320 .js.map files including \`(auth-pages)/sign-in/page.js.map\` whose \`sources\` array points at \`webpack://@pawtograder/webapp/./app/(auth-pages)/sign-in/page.tsx\`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b0af86b commit 275f3e9

1 file changed

Lines changed: 110 additions & 91 deletions

File tree

next.config.ts

Lines changed: 110 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -65,112 +65,131 @@ const nextConfig: NextConfig = {
6565
static: 300
6666
}
6767
},
68-
...(useLegacyWebpackTweaks
68+
// Coverage builds need full source maps on BOTH client and server
69+
// bundles. `productionBrowserSourceMaps: true` (above) handles client;
70+
// for the server bundle Next 15 omits sourcemaps in production by
71+
// default, so we force `devtool = source-map` via a webpack hook.
72+
// Without this, `c8 report` over NODE_V8_COVERAGE has no way to map
73+
// compiled `.next/server/app/.../page.js` back to its `.tsx` source
74+
// and silently produces near-empty server coverage.
75+
...(coverageBuild
6976
? {
70-
// Keep legacy memory-optimized webpack behavior available via NEXT_BUNDLING_PROFILE=legacy.
71-
webpack: (config, { isServer, dev }) => {
72-
if (config.cache && !dev) {
73-
config.cache = {
74-
...config.cache,
75-
maxMemoryGenerations: 1
76-
};
77-
}
78-
79-
if (!isServer) {
80-
config.optimization = {
81-
...config.optimization,
82-
moduleIds: "deterministic",
83-
splitChunks: {
84-
chunks: "all",
85-
maxInitialRequests: 25,
86-
maxAsyncRequests: 30,
87-
cacheGroups: {
88-
default: false,
89-
monaco: {
90-
name: "monaco-editor",
91-
test: /[\\/]node_modules[\\/](@monaco-editor|monaco-editor|monaco-yaml)[\\/]/,
92-
priority: 20,
93-
reuseExistingChunk: true,
94-
enforce: true
95-
},
96-
chakra: {
97-
name: "chakra-ui",
98-
test: /[\\/]node_modules[\\/]@chakra-ui[\\/]/,
99-
priority: 15,
100-
reuseExistingChunk: true,
101-
enforce: true
102-
},
103-
charts: {
104-
name: "charts",
105-
test: /[\\/]node_modules[\\/](recharts|@chakra-ui\/charts)[\\/]/,
106-
priority: 10,
107-
reuseExistingChunk: true,
108-
enforce: true
109-
},
110-
mdEditor: {
111-
name: "md-editor",
112-
test: /[\\/]node_modules[\\/]@uiw[\\/]react-md-editor[\\/]/,
113-
priority: 10,
114-
reuseExistingChunk: true,
115-
enforce: true
116-
},
117-
mathjs: {
118-
name: "mathjs",
119-
test: /[\\/]node_modules[\\/]mathjs[\\/]/,
120-
priority: 10,
121-
reuseExistingChunk: true,
122-
enforce: true
77+
webpack: (config: { devtool?: string }, ctx: { isServer: boolean }) => {
78+
// Only override server bundle. Client bundles get
79+
// sourcemaps via `productionBrowserSourceMaps: true`
80+
// above; setting devtool twice (here + the option)
81+
// confuses Next's webpack pipeline and produces an
82+
// empty `.next/static` directory.
83+
if (ctx.isServer) config.devtool = "source-map";
84+
return config;
85+
}
86+
}
87+
: useLegacyWebpackTweaks
88+
? {
89+
// Keep legacy memory-optimized webpack behavior available via NEXT_BUNDLING_PROFILE=legacy.
90+
webpack: (config, { isServer, dev }) => {
91+
if (config.cache && !dev) {
92+
config.cache = {
93+
...config.cache,
94+
maxMemoryGenerations: 1
95+
};
96+
}
97+
98+
if (!isServer) {
99+
config.optimization = {
100+
...config.optimization,
101+
moduleIds: "deterministic",
102+
splitChunks: {
103+
chunks: "all",
104+
maxInitialRequests: 25,
105+
maxAsyncRequests: 30,
106+
cacheGroups: {
107+
default: false,
108+
monaco: {
109+
name: "monaco-editor",
110+
test: /[\\/]node_modules[\\/](@monaco-editor|monaco-editor|monaco-yaml)[\\/]/,
111+
priority: 20,
112+
reuseExistingChunk: true,
113+
enforce: true
114+
},
115+
chakra: {
116+
name: "chakra-ui",
117+
test: /[\\/]node_modules[\\/]@chakra-ui[\\/]/,
118+
priority: 15,
119+
reuseExistingChunk: true,
120+
enforce: true
121+
},
122+
charts: {
123+
name: "charts",
124+
test: /[\\/]node_modules[\\/](recharts|@chakra-ui\/charts)[\\/]/,
125+
priority: 10,
126+
reuseExistingChunk: true,
127+
enforce: true
128+
},
129+
mdEditor: {
130+
name: "md-editor",
131+
test: /[\\/]node_modules[\\/]@uiw[\\/]react-md-editor[\\/]/,
132+
priority: 10,
133+
reuseExistingChunk: true,
134+
enforce: true
135+
},
136+
mathjs: {
137+
name: "mathjs",
138+
test: /[\\/]node_modules[\\/]mathjs[\\/]/,
139+
priority: 10,
140+
reuseExistingChunk: true,
141+
enforce: true
142+
}
123143
}
124144
}
125-
}
126-
};
127-
}
145+
};
146+
}
128147

129-
if (config.optimization?.minimizer) {
130-
config.optimization.minimizer = config.optimization.minimizer.map((plugin: unknown) => {
131-
if (!plugin || typeof plugin !== "object" || !("constructor" in plugin)) {
132-
return plugin;
133-
}
148+
if (config.optimization?.minimizer) {
149+
config.optimization.minimizer = config.optimization.minimizer.map((plugin: unknown) => {
150+
if (!plugin || typeof plugin !== "object" || !("constructor" in plugin)) {
151+
return plugin;
152+
}
134153

135-
const pluginName = plugin.constructor.name;
154+
const pluginName = plugin.constructor.name;
136155

137-
if (pluginName === "SwcMinify") {
138-
return plugin;
139-
}
140-
141-
if (pluginName === "TerserPlugin") {
142-
const terserPlugin = plugin as {
143-
options?: { parallel?: boolean; terserOptions?: { compress?: { passes?: number } } };
144-
};
145-
if (terserPlugin.options) {
146-
terserPlugin.options.parallel = false;
147-
if (terserPlugin.options.terserOptions?.compress) {
148-
terserPlugin.options.terserOptions.compress.passes = 1;
156+
if (pluginName === "SwcMinify") {
157+
return plugin;
158+
}
159+
160+
if (pluginName === "TerserPlugin") {
161+
const terserPlugin = plugin as {
162+
options?: { parallel?: boolean; terserOptions?: { compress?: { passes?: number } } };
163+
};
164+
if (terserPlugin.options) {
165+
terserPlugin.options.parallel = false;
166+
if (terserPlugin.options.terserOptions?.compress) {
167+
terserPlugin.options.terserOptions.compress.passes = 1;
168+
}
149169
}
170+
return plugin;
150171
}
151-
return plugin;
152-
}
153172

154-
if (pluginName === "CssMinimizerPlugin") {
155-
const cssPlugin = plugin as { options?: { parallel?: boolean } };
156-
if (cssPlugin.options) {
157-
cssPlugin.options.parallel = false;
173+
if (pluginName === "CssMinimizerPlugin") {
174+
const cssPlugin = plugin as { options?: { parallel?: boolean } };
175+
if (cssPlugin.options) {
176+
cssPlugin.options.parallel = false;
177+
}
178+
return plugin;
158179
}
180+
159181
return plugin;
160-
}
182+
});
183+
}
161184

162-
return plugin;
163-
});
164-
}
185+
if (config.resolve) {
186+
config.resolve.cache = false;
187+
}
165188

166-
if (config.resolve) {
167-
config.resolve.cache = false;
189+
return config;
168190
}
169-
170-
return config;
171191
}
172-
}
173-
: {})
192+
: {})
174193
};
175194

176195
// Skip Sentry webpack integration when DSN is unset (local dev) or explicitly disabled (CI speed).

0 commit comments

Comments
 (0)