Skip to content

Commit 776cd68

Browse files
authored
Ensure Next.js is ignore-listed when used as external (#72498)
Observable in `pnpm next dev test/development/app-dir/dynamic-io-dev-errors/` and `/no-accessed-data`. It only affects the bottom stacks in that particular stack so assertions are not viable yet until the full stack is properly sourcemapped.
1 parent c3467c7 commit 776cd68

File tree

5 files changed

+306
-2
lines changed

5 files changed

+306
-2
lines changed

packages/next/src/build/webpack/plugins/eval-source-map-dev-tool-plugin.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/*
22
MIT License http://www.opensource.org/licenses/mit-license.php
33
Author Tobias Koppers @sokra
4+
5+
Forked to add support for `ignoreList`.
6+
Keep in sync with packages/next/webpack-plugins/eval-source-map-dev-tool-plugin.js
47
*/
58
import {
69
type webpack,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Source: https://github.com/mondaychen/devtools-ignore-webpack-plugin/blob/e35ce41d9606a92a455ef247f509a1c2ccab5778/src/index.ts
2+
3+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
4+
const webpack = require('webpack')
5+
6+
// Following the naming conventions from
7+
// https://tc39.es/source-map/#source-map-format
8+
const IGNORE_LIST = 'ignoreList'
9+
const PLUGIN_NAME = 'devtools-ignore-plugin'
10+
function defaultShouldIgnorePath(path) {
11+
return path.includes('/node_modules/') || path.includes('/webpack/')
12+
}
13+
function defaultIsSourceMapAsset(name) {
14+
return name.endsWith('.map')
15+
}
16+
17+
/**
18+
* This plugin adds a field to source maps that identifies which sources are
19+
* vendored or runtime-injected (aka third-party) sources. These are consumed by
20+
* Chrome DevTools to automatically ignore-list sources.
21+
*/
22+
module.exports = class DevToolsIgnorePlugin {
23+
options
24+
constructor(options = {}) {
25+
this.options = {
26+
shouldIgnorePath: options.shouldIgnorePath ?? defaultShouldIgnorePath,
27+
isSourceMapAsset: options.isSourceMapAsset ?? defaultIsSourceMapAsset,
28+
}
29+
}
30+
apply(compiler) {
31+
const { RawSource } = compiler.webpack.sources
32+
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
33+
compilation.hooks.processAssets.tap(
34+
{
35+
name: PLUGIN_NAME,
36+
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
37+
additionalAssets: true,
38+
},
39+
(assets) => {
40+
for (const [name, asset] of Object.entries(assets)) {
41+
// Instead of using `asset.map()` to fetch the source maps from
42+
// SourceMapSource assets, process them directly as a RawSource.
43+
// This is because `.map()` is slow and can take several seconds.
44+
if (!this.options.isSourceMapAsset(name)) {
45+
// Ignore non source map files.
46+
continue
47+
}
48+
const mapContent = asset.source().toString()
49+
if (!mapContent) {
50+
continue
51+
}
52+
const sourcemap = JSON.parse(mapContent)
53+
const ignoreList = []
54+
for (const [index, path] of sourcemap.sources.entries()) {
55+
if (this.options.shouldIgnorePath(path)) {
56+
ignoreList.push(index)
57+
}
58+
}
59+
sourcemap[IGNORE_LIST] = ignoreList
60+
compilation.updateAsset(
61+
name,
62+
new RawSource(JSON.stringify(sourcemap))
63+
)
64+
}
65+
}
66+
)
67+
})
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Tobias Koppers @sokra
4+
5+
Forked to add support for `ignoreList`.
6+
Keep in sync with packages/next/webpack-plugins/eval-source-map-dev-tool-plugin.js
7+
*/
8+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
9+
const ConcatenatedModule = require('webpack/lib/optimize/ConcatenatedModule')
10+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
11+
const { makePathsAbsolute } = require('webpack/lib/util/identifier')
12+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
13+
const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers')
14+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
15+
const NormalModule = require('webpack/lib/NormalModule')
16+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
17+
const RuntimeGlobals = require('webpack/lib/RuntimeGlobals')
18+
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
19+
const SourceMapDevToolModuleOptionsPlugin = require('webpack/lib/SourceMapDevToolModuleOptionsPlugin')
20+
21+
const cache = new WeakMap()
22+
const devtoolWarningMessage = `/*
23+
* ATTENTION: An "eval-source-map" devtool has been used.
24+
* This devtool is neither made for production nor for readable output files.
25+
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
26+
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
27+
* or disable the default devtool with "devtool: false".
28+
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
29+
*/
30+
`
31+
32+
// @ts-expect-error -- can't compare `string` with `number` in `version`Ï
33+
34+
// Fork of webpack's EvalSourceMapDevToolPlugin with support for adding `ignoreList`.
35+
// https://github.com/webpack/webpack/blob/e237b580e2bda705c5ab39973f786f7c5a7026bc/lib/EvalSourceMapDevToolPlugin.js#L37
36+
module.exports = class EvalSourceMapDevToolPlugin {
37+
sourceMapComment
38+
moduleFilenameTemplate
39+
namespace
40+
options
41+
shouldIgnorePath
42+
43+
/**
44+
* @param {SourceMapDevToolPluginOptions|string} inputOptions Options object
45+
*/
46+
constructor(inputOptions) {
47+
let options
48+
if (typeof inputOptions === 'string') {
49+
options = {
50+
append: inputOptions,
51+
}
52+
} else {
53+
options = inputOptions
54+
}
55+
this.sourceMapComment =
56+
options.append && typeof options.append !== 'function'
57+
? options.append
58+
: '//# sourceURL=[module]\n//# sourceMappingURL=[url]'
59+
this.moduleFilenameTemplate =
60+
options.moduleFilenameTemplate ||
61+
'webpack://[namespace]/[resource-path]?[hash]'
62+
this.namespace = options.namespace || ''
63+
this.options = options
64+
65+
// fork
66+
this.shouldIgnorePath = options.shouldIgnorePath ?? (() => false)
67+
}
68+
69+
/**
70+
* Apply the plugin
71+
* @param compiler the compiler instance
72+
*/
73+
apply(compiler) {
74+
const options = this.options
75+
compiler.hooks.compilation.tap(
76+
'NextJSEvalSourceMapDevToolPlugin',
77+
(compilation) => {
78+
const { JavascriptModulesPlugin } = compiler.webpack.javascript
79+
const { RawSource, ConcatSource } = compiler.webpack.sources
80+
const devtoolWarning = new RawSource(devtoolWarningMessage)
81+
const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation)
82+
new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation)
83+
const matchModule = ModuleFilenameHelpers.matchObject.bind(
84+
ModuleFilenameHelpers,
85+
options
86+
)
87+
hooks.renderModuleContent.tap(
88+
'NextJSEvalSourceMapDevToolPlugin',
89+
(source, m, { chunk, runtimeTemplate, chunkGraph }) => {
90+
const cachedSource = cache.get(source)
91+
if (cachedSource !== undefined) {
92+
return cachedSource
93+
}
94+
const result = (r) => {
95+
cache.set(source, r)
96+
return r
97+
}
98+
if (m instanceof NormalModule) {
99+
const module = m
100+
if (!matchModule(module.resource)) {
101+
return result(source)
102+
}
103+
} else if (m instanceof ConcatenatedModule) {
104+
const concatModule = m
105+
if (concatModule.rootModule instanceof NormalModule) {
106+
const module = concatModule.rootModule
107+
if (!matchModule(module.resource)) {
108+
return result(source)
109+
}
110+
} else {
111+
return result(source)
112+
}
113+
} else {
114+
return result(source)
115+
}
116+
const namespace = compilation.getPath(this.namespace, {
117+
chunk,
118+
})
119+
let sourceMap
120+
let content
121+
if (source.sourceAndMap) {
122+
const sourceAndMap = source.sourceAndMap(options)
123+
sourceMap = sourceAndMap.map
124+
content = sourceAndMap.source
125+
} else {
126+
sourceMap = source.map(options)
127+
content = source.source()
128+
}
129+
if (!sourceMap) {
130+
return result(source)
131+
}
132+
133+
// Clone (flat) the sourcemap to ensure that the mutations below do not persist.
134+
sourceMap = {
135+
...sourceMap,
136+
}
137+
const context = compiler.options.context
138+
const root = compiler.root
139+
const modules = sourceMap.sources.map((sourceMapSource) => {
140+
if (!sourceMapSource.startsWith('webpack://'))
141+
return sourceMapSource
142+
sourceMapSource = makePathsAbsolute(
143+
context,
144+
sourceMapSource.slice(10),
145+
root
146+
)
147+
const module = compilation.findModule(sourceMapSource)
148+
return module || sourceMapSource
149+
})
150+
let moduleFilenames = modules.map((module) =>
151+
ModuleFilenameHelpers.createFilename(
152+
module,
153+
{
154+
moduleFilenameTemplate: this.moduleFilenameTemplate,
155+
namespace,
156+
},
157+
{
158+
requestShortener: runtimeTemplate.requestShortener,
159+
chunkGraph,
160+
hashFunction: compilation.outputOptions.hashFunction,
161+
}
162+
)
163+
)
164+
moduleFilenames = ModuleFilenameHelpers.replaceDuplicates(
165+
moduleFilenames,
166+
(filename, _i, n) => {
167+
for (let j = 0; j < n; j++) filename += '*'
168+
return filename
169+
}
170+
)
171+
sourceMap.sources = moduleFilenames
172+
sourceMap.ignoreList = []
173+
for (let index = 0; index < moduleFilenames.length; index++) {
174+
if (this.shouldIgnorePath(moduleFilenames[index])) {
175+
sourceMap.ignoreList.push(index)
176+
}
177+
}
178+
if (options.noSources) {
179+
sourceMap.sourcesContent = undefined
180+
}
181+
sourceMap.sourceRoot = options.sourceRoot || ''
182+
const moduleId = /** @type {ModuleId} */ chunkGraph.getModuleId(m)
183+
if (moduleId) {
184+
sourceMap.file =
185+
typeof moduleId === 'number' ? `${moduleId}.js` : moduleId
186+
}
187+
const footer = `${this.sourceMapComment.replace(/\[url\]/g, `data:application/json;charset=utf-8;base64,${Buffer.from(JSON.stringify(sourceMap), 'utf8').toString('base64')}`)}\n//# sourceURL=webpack-internal:///${moduleId}\n` // workaround for chrome bug
188+
189+
return result(
190+
new RawSource(
191+
`eval(${compilation.outputOptions.trustedTypes ? `${RuntimeGlobals.createScript}(${JSON.stringify(content + footer)})` : JSON.stringify(content + footer)});`
192+
)
193+
)
194+
}
195+
)
196+
hooks.inlineInRuntimeBailout.tap(
197+
'EvalDevToolModulePlugin',
198+
() => 'the eval-source-map devtool is used.'
199+
)
200+
hooks.render.tap(
201+
'EvalSourceMapDevToolPlugin',
202+
(source) => new ConcatSource(devtoolWarning, source)
203+
)
204+
hooks.chunkHash.tap('EvalSourceMapDevToolPlugin', (_chunk, hash) => {
205+
hash.update('EvalSourceMapDevToolPlugin')
206+
hash.update('2')
207+
})
208+
if (compilation.outputOptions.trustedTypes) {
209+
compilation.hooks.additionalModuleRuntimeRequirements.tap(
210+
'EvalSourceMapDevToolPlugin',
211+
(_module, set, _context) => {
212+
set.add(RuntimeGlobals.createScript)
213+
}
214+
)
215+
}
216+
}
217+
)
218+
}
219+
}

packages/next/webpack.config.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ const webpack = require('webpack')
22
const path = require('path')
33
const TerserPlugin = require('terser-webpack-plugin')
44
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
5+
const EvalSourceMapDevToolPlugin = require('./webpack-plugins/eval-source-map-dev-tool-plugin')
6+
const DevToolsIgnoreListPlugin = require('./webpack-plugins/devtools-ignore-list-plugin')
7+
8+
function shouldIgnorePath(modulePath) {
9+
// For consumers, everything will be considered 3rd party dependency if they use
10+
// the bundles we produce here.
11+
// In other words, this is all library code and should therefore be ignored.
12+
return true
13+
}
514

615
const pagesExternals = [
716
'react',
@@ -156,7 +165,8 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => {
156165
libraryTarget: 'commonjs2',
157166
},
158167
devtool: process.env.NEXT_SERVER_EVAL_SOURCE_MAPS
159-
? 'eval-source-map'
168+
? // We'll use a fork in plugins
169+
false
160170
: 'source-map',
161171
optimization: {
162172
moduleIds: 'named',
@@ -181,6 +191,9 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => {
181191
],
182192
},
183193
plugins: [
194+
process.env.NEXT_SERVER_EVAL_SOURCE_MAPS
195+
? new EvalSourceMapDevToolPlugin({ shouldIgnorePath })
196+
: new DevToolsIgnoreListPlugin({ shouldIgnorePath }),
184197
new webpack.DefinePlugin({
185198
'typeof window': JSON.stringify('undefined'),
186199
'process.env.NEXT_MINIMAL': JSON.stringify('true'),

test/integration/server-side-dev-errors/test/index.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe('server-side dev errors', () => {
126126
' ⨯ ReferenceError: missingVar is not defined' +
127127
'\n at getServerSideProps (./test/integration/server-side-dev-errors/pages/gssp.js:6:3)' +
128128
// TODO(veil): Should be sourcemapped
129-
'\n a'
129+
'\n at'
130130
)
131131
} else {
132132
expect(stderrOutput).toStartWith(

0 commit comments

Comments
 (0)