diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index abd2b01a08296b7..4dc5b5b3e7a8a4f 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -51,6 +51,7 @@ import { getRawPageExtensions } from './utils' import browserslist from 'next/dist/compiled/browserslist' import loadJsConfig from './load-jsconfig' import { shouldUseReactRoot } from '../server/config' +import { getMiddlewareSourceMapPlugins } from './webpack/plugins/middleware-source-maps-plugin' const watchOptions = Object.freeze({ aggregateTimeout: 5, @@ -1272,6 +1273,12 @@ export default async function getBaseWebpackConfig( ].filter(Boolean), }, plugins: [ + ...(!dev && + !isServer && + !!config.experimental.middlewareSourceMaps && + !config.productionBrowserSourceMaps + ? getMiddlewareSourceMapPlugins() + : []), hasReactRefresh && new ReactRefreshWebpackPlugin(webpack), // Makes sure `Buffer` and `process` are polyfilled in client and flight bundles (same behavior as webpack 4) targetWeb && diff --git a/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts b/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts new file mode 100644 index 000000000000000..8ec8e21b004dae5 --- /dev/null +++ b/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts @@ -0,0 +1,38 @@ +import { webpack } from 'next/dist/compiled/webpack/webpack' +import type { webpack5 } from 'next/dist/compiled/webpack/webpack' + +/** + * Produce source maps for middlewares. + * Currently we use the same compiler for browser and middlewares, + */ +export const getMiddlewareSourceMapPlugins = () => { + return [ + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', + include: [ + // Middlewares are the only ones who have `server/pages/[name]` as their filename + /^server\/pages\//, + // All middleware chunks + /^server\/middleware-chunks\//, + ], + }), + new MiddlewareSourceMapsPlugin(), + ] +} + +/** + * Produce source maps for middlewares. + * Currently we use the same compiler for browser and middlewares, + * so we can avoid having the custom plugins if the browser source maps + * are emitted. + */ +class MiddlewareSourceMapsPlugin { + apply(compiler: webpack5.Compiler): void { + const PLUGIN_NAME = 'NextJsMiddlewareSourceMapsPlugin' + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.buildModule.tap(PLUGIN_NAME, (module) => { + module.useSourceMap = module.layer === 'middleware' + }) + }) + } +} diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index a580ee537efbbee..b927e37ffdc8140 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -106,6 +106,7 @@ export interface ExperimentalConfig { urlImports?: NonNullable['buildHttp'] outputFileTracingRoot?: string outputStandalone?: boolean + middlewareSourceMaps?: boolean } /** diff --git a/test/production/generate-middleware-source-maps/index.test.ts b/test/production/generate-middleware-source-maps/index.test.ts new file mode 100644 index 000000000000000..c4c3cf5f87dc122 --- /dev/null +++ b/test/production/generate-middleware-source-maps/index.test.ts @@ -0,0 +1,63 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import fs from 'fs-extra' +import path from 'path' + +describe('experimental.middlewareSourceMaps: true', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + nextConfig: { + experimental: { + middlewareSourceMaps: true, + }, + }, + files: { + 'pages/_middleware.js': ` + export default function middleware() { + return new Response("Hello, world!"); + } + `, + }, + dependencies: {}, + }) + }) + afterAll(() => next.destroy()) + + it('generates a source map', async () => { + const middlewarePath = path.resolve( + next.testDir, + '.next/server/pages/_middleware.js' + ) + expect(await fs.pathExists(middlewarePath)).toEqual(true) + expect(await fs.pathExists(`${middlewarePath}.map`)).toEqual(true) + }) +}) + +describe('experimental.middlewareSourceMaps: false', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/_middleware.js': ` + export default function middleware() { + return new Response("Hello, world!"); + } + `, + }, + dependencies: {}, + }) + }) + afterAll(() => next.destroy()) + + it('does not generate a source map', async () => { + const middlewarePath = path.resolve( + next.testDir, + '.next/server/pages/_middleware.js' + ) + expect(await fs.pathExists(middlewarePath)).toEqual(true) + expect(await fs.pathExists(`${middlewarePath}.map`)).toEqual(false) + }) +})