|
| 1 | +/** |
| 2 | + * Webpack/Rspack config for bundling dev server startup modules. |
| 3 | + * |
| 4 | + * This bundles start-server.ts, router-server.ts, and their dependencies |
| 5 | + * into a single file to reduce module loading overhead during dev server boot. |
| 6 | + */ |
| 7 | + |
| 8 | +/* eslint-disable import/no-extraneous-dependencies */ |
| 9 | +const rspack = require('@rspack/core') |
| 10 | +const path = require('path') |
| 11 | +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') |
| 12 | +const DevToolsIgnorePlugin = |
| 13 | + require('./dist/build/webpack/plugins/devtools-ignore-list-plugin').default |
| 14 | +/* eslint-enable import/no-extraneous-dependencies */ |
| 15 | + |
| 16 | +// Shared externals with the server runtime bundle |
| 17 | +const sharedExternals = [ |
| 18 | + 'styled-jsx', |
| 19 | + 'styled-jsx/style', |
| 20 | + '@opentelemetry/api', |
| 21 | + 'next/dist/compiled/@ampproject/toolbox-optimizer', |
| 22 | + 'next/dist/compiled/edge-runtime', |
| 23 | + 'next/dist/compiled/@edge-runtime/ponyfill', |
| 24 | + 'next/dist/compiled/undici', |
| 25 | + 'next/dist/compiled/raw-body', |
| 26 | + 'next/dist/server/capsize-font-metrics.json', |
| 27 | + 'critters', |
| 28 | + 'next/dist/compiled/node-html-parser', |
| 29 | + 'next/dist/compiled/compression', |
| 30 | + 'next/dist/compiled/jsonwebtoken', |
| 31 | + 'next/dist/compiled/@opentelemetry/api', |
| 32 | + 'next/dist/compiled/@mswjs/interceptors/ClientRequest', |
| 33 | + 'next/dist/compiled/ws', |
| 34 | + 'next/dist/compiled/tar', |
| 35 | + 'next/dist/compiled/@modelcontextprotocol/sdk/server/mcp', |
| 36 | +] |
| 37 | + |
| 38 | +// Dev server specific externals |
| 39 | +const devServerExternals = [ |
| 40 | + // Native bindings - cannot be bundled |
| 41 | + '@next/swc', |
| 42 | + // User-facing packages |
| 43 | + '@next/env', |
| 44 | + // Optional/native image dependencies |
| 45 | + 'sharp', |
| 46 | +] |
| 47 | + |
| 48 | +// Patterns to externalize |
| 49 | +const externalPatterns = [ |
| 50 | + // Pre-compiled dependencies |
| 51 | + /^next\/dist\/compiled\//, |
| 52 | + // Telemetry - lazy loaded |
| 53 | + /telemetry\/storage/, |
| 54 | + // Hot reloaders - dynamically loaded based on bundler choice |
| 55 | + /hot-reloader-turbopack/, |
| 56 | + /hot-reloader-webpack/, |
| 57 | + /hot-reloader-rspack/, |
| 58 | + // MCP server - only needed when clients connect |
| 59 | + /\/mcp\//, |
| 60 | + // Turbopack internals - native bindings |
| 61 | + /turbopack/, |
| 62 | + // SWC bindings - native (matches ./swc, build/swc, etc.) |
| 63 | + /\/swc($|\/)/, |
| 64 | + // Download SWC - only needed for fallback, loads tar |
| 65 | + /download-swc/, |
| 66 | + // Sandbox - manipulates require.cache |
| 67 | + /web\/sandbox/, |
| 68 | + // Require hook - uses require.resolve with user-facing packages |
| 69 | + /require-hook/, |
| 70 | + // Config utils - uses require.resolve with dynamic webpack paths |
| 71 | + /config-utils/, |
| 72 | + // Config loading - uses dynamic import() for user config files |
| 73 | + /\/config$/, |
| 74 | + // Node environment extensions - sets up global handlers, avoid duplication |
| 75 | + /node-environment-extensions/, |
| 76 | + // TypeScript setup verification - uses dynamic require.resolve |
| 77 | + /verify-typescript-setup/, |
| 78 | + // Sharp image processing |
| 79 | + /@img\/sharp/, |
| 80 | +] |
| 81 | + |
| 82 | +// Map relative paths to proper external paths |
| 83 | +// Note: Most mappings are now handled dynamically by toNextDistPath() in the externalHandler |
| 84 | +const externalsMap = { |
| 85 | + // render-server pulls in next-server which has heavy dependencies (react-dom/server) |
| 86 | + // It's lazy-loaded at runtime when handling requests, not at startup |
| 87 | + './render-server': 'next/dist/server/lib/render-server', |
| 88 | + '../render-server': 'next/dist/server/lib/render-server', |
| 89 | + // next/dist/server/next (the main Next.js server) - heavy, only needed for request handling |
| 90 | + '../next': 'next/dist/server/next', |
| 91 | +} |
| 92 | + |
| 93 | +// Regex-based externals mapping |
| 94 | +const externalsRegexMap = { |
| 95 | + '(.*)trace/tracer$': 'next/dist/server/lib/trace/tracer', |
| 96 | +} |
| 97 | + |
| 98 | +/** |
| 99 | + * Convert an absolute path within dist/ to a next/dist/... style import |
| 100 | + * @param {string} absolutePath |
| 101 | + * @returns {string|null} |
| 102 | + */ |
| 103 | +function toNextDistPath(absolutePath) { |
| 104 | + const normalizedPath = absolutePath.replace(/\\/g, '/') |
| 105 | + const distIndex = normalizedPath.indexOf('/dist/') |
| 106 | + if (distIndex === -1) return null |
| 107 | + |
| 108 | + // Get the path after /dist/ and convert to next/dist/... |
| 109 | + const relativePath = normalizedPath.substring(distIndex + 1) // removes leading / |
| 110 | + return `next/${relativePath.replace(/\.js$/, '')}` |
| 111 | +} |
| 112 | + |
| 113 | +/** |
| 114 | + * @param {Object} options |
| 115 | + * @param {boolean} options.dev - Development mode |
| 116 | + * @returns {import('@rspack/core').Configuration} |
| 117 | + */ |
| 118 | +module.exports = ({ dev }) => { |
| 119 | + const externalHandler = ({ context, request, getResolve }, callback) => { |
| 120 | + ;(async () => { |
| 121 | + // Handle compiled dependencies with complex paths |
| 122 | + if ( |
| 123 | + request.match( |
| 124 | + /next[/\\]dist[/\\]compiled[/\\](babel|webpack|source-map|semver|jest-worker|stacktrace-parser|@ampproject\/toolbox-optimizer)/ |
| 125 | + ) |
| 126 | + ) { |
| 127 | + callback(null, 'commonjs ' + request) |
| 128 | + return |
| 129 | + } |
| 130 | + |
| 131 | + // Handle image optimizer and test mode |
| 132 | + if (request.match(/(server\/image-optimizer|experimental\/testmode)/)) { |
| 133 | + callback(null, 'commonjs ' + request) |
| 134 | + return |
| 135 | + } |
| 136 | + |
| 137 | + // Handle .external.js files |
| 138 | + if (request.match(/\.external(\.js)?$/)) { |
| 139 | + try { |
| 140 | + const resolve = getResolve() |
| 141 | + const resolved = await resolve(context, request) |
| 142 | + const nextDistPath = toNextDistPath(resolved) |
| 143 | + if (nextDistPath) { |
| 144 | + callback(null, `commonjs ${nextDistPath}`) |
| 145 | + return |
| 146 | + } |
| 147 | + } catch { |
| 148 | + // Resolution failed, use request as-is |
| 149 | + } |
| 150 | + callback(null, `commonjs ${request}`) |
| 151 | + return |
| 152 | + } |
| 153 | + |
| 154 | + // Handle pattern-based externals - resolve and convert to next/dist/... paths |
| 155 | + for (const pattern of externalPatterns) { |
| 156 | + if (pattern.test(request)) { |
| 157 | + // Try to resolve and get proper next/dist/... path |
| 158 | + try { |
| 159 | + const resolve = getResolve() |
| 160 | + const resolved = await resolve(context, request) |
| 161 | + const nextDistPath = toNextDistPath(resolved) |
| 162 | + if (nextDistPath) { |
| 163 | + callback(null, `commonjs ${nextDistPath}`) |
| 164 | + return |
| 165 | + } |
| 166 | + } catch { |
| 167 | + // Resolution failed, use request as-is |
| 168 | + } |
| 169 | + callback(null, 'commonjs ' + request) |
| 170 | + return |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + // Handle regex map externals |
| 175 | + const regexMatch = Object.keys(externalsRegexMap).find((regex) => |
| 176 | + new RegExp(regex).test(request) |
| 177 | + ) |
| 178 | + if (regexMatch) { |
| 179 | + callback(null, 'commonjs ' + externalsRegexMap[regexMatch]) |
| 180 | + return |
| 181 | + } |
| 182 | + |
| 183 | + callback() |
| 184 | + })() |
| 185 | + } |
| 186 | + |
| 187 | + return { |
| 188 | + entry: { |
| 189 | + 'start-server': path.join(__dirname, 'dist/server/lib/start-server.js'), |
| 190 | + }, |
| 191 | + target: 'node', |
| 192 | + mode: dev ? 'development' : 'production', |
| 193 | + output: { |
| 194 | + path: path.join(__dirname, 'dist/compiled/dev-server'), |
| 195 | + filename: '[name].js', |
| 196 | + libraryTarget: 'commonjs2', |
| 197 | + }, |
| 198 | + devtool: 'source-map', |
| 199 | + optimization: { |
| 200 | + moduleIds: 'named', |
| 201 | + minimize: !dev, |
| 202 | + ...(dev |
| 203 | + ? {} |
| 204 | + : { |
| 205 | + minimizer: [ |
| 206 | + new rspack.SwcJsMinimizerRspackPlugin({ |
| 207 | + minimizerOptions: { |
| 208 | + mangle: false, // Keep readable for debugging |
| 209 | + }, |
| 210 | + }), |
| 211 | + ], |
| 212 | + }), |
| 213 | + }, |
| 214 | + plugins: [ |
| 215 | + new rspack.DefinePlugin({ |
| 216 | + 'typeof window': JSON.stringify('undefined'), |
| 217 | + // Replace process.env.NODE_ENV with 'development' since this bundle |
| 218 | + // is only used by the dev server. This ensures loadEnvConfig gets |
| 219 | + // the correct value for loading .env.development files. |
| 220 | + 'process.env.NODE_ENV': JSON.stringify('development'), |
| 221 | + }), |
| 222 | + new rspack.BannerPlugin({ |
| 223 | + banner: '/* Bundled dev server - reduces module loading overhead */', |
| 224 | + raw: true, |
| 225 | + }), |
| 226 | + process.env.ANALYZE && |
| 227 | + new BundleAnalyzerPlugin({ |
| 228 | + analyzerMode: 'static', |
| 229 | + reportFilename: path.join( |
| 230 | + __dirname, |
| 231 | + 'dist/compiled/dev-server/bundle-report.html' |
| 232 | + ), |
| 233 | + openAnalyzer: false, |
| 234 | + }), |
| 235 | + // Add ignoreList to source maps so internal frames are hidden in error stacks |
| 236 | + new DevToolsIgnorePlugin({ |
| 237 | + shouldIgnorePath: () => true, // All sources in this bundle are internal |
| 238 | + }), |
| 239 | + ].filter(Boolean), |
| 240 | + resolve: { |
| 241 | + extensions: ['.js', '.json'], |
| 242 | + }, |
| 243 | + externals: [ |
| 244 | + ...sharedExternals, |
| 245 | + ...devServerExternals, |
| 246 | + externalsMap, |
| 247 | + externalHandler, |
| 248 | + ], |
| 249 | + externalsPresets: { |
| 250 | + node: true, |
| 251 | + }, |
| 252 | + stats: process.env.ANALYZE_REASONS |
| 253 | + ? { |
| 254 | + preset: 'verbose', |
| 255 | + reasons: true, |
| 256 | + modulesSpace: Infinity, |
| 257 | + } |
| 258 | + : { |
| 259 | + preset: 'errors-warnings', |
| 260 | + assets: true, |
| 261 | + }, |
| 262 | + } |
| 263 | +} |
0 commit comments