Skip to content

Commit

Permalink
Populate sourcemap ignoreList to collapse stackframes from 3rd part…
Browse files Browse the repository at this point in the history
…y dependencies

Webpack only. Replays from RSC will follow.
  • Loading branch information
eps1lon committed Oct 24, 2024
1 parent b800ff1 commit 91bac8c
Show file tree
Hide file tree
Showing 15 changed files with 521 additions and 135 deletions.
30 changes: 29 additions & 1 deletion packages/next/src/build/webpack/config/blocks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ import curry from 'next/dist/compiled/lodash.curry'
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { COMPILER_NAMES } from '../../../../shared/lib/constants'
import type { ConfigurationContext } from '../utils'
import DevToolsIgnorePlugin from '../../plugins/devtools-ignore-list-plugin'
import EvalSourceMapDevToolPlugin from '../../plugins/eval-source-map-dev-tool-plugin'

function shouldIgnorePath(modulePath: string): boolean {
// TODO: How to ignore list 'webpack:///../../../src/shared/lib/is-thenable.ts'
return (
modulePath.includes('node_modules') ||
// would filter 'webpack://_N_E/./app/page.tsx'
// modulePath.startsWith('webpack://_N_E/') ||
// e.g. 'webpack:///external commonjs "next/dist/compiled/next-server/app-page.runtime.dev.js"'
modulePath.includes('next/dist')
)
}

export const base = curry(function base(
ctx: ConfigurationContext,
Expand Down Expand Up @@ -29,7 +42,14 @@ export const base = curry(function base(
// original source, including columns and original variable names.
// This is desirable so the in-browser debugger can correctly pause
// and show scoped variables with their original names.
config.devtool = 'eval-source-map'
// We're using a fork of `eval-source-map`
config.devtool = false
config.plugins ??= []
config.plugins.push(
new EvalSourceMapDevToolPlugin({
shouldIgnorePath,
})
)
}
} else {
if (
Expand All @@ -39,6 +59,14 @@ export const base = curry(function base(
(ctx.productionBrowserSourceMaps && ctx.isClient)
) {
config.devtool = 'source-map'
config.plugins ??= []
config.plugins.push(
new DevToolsIgnorePlugin({
// TODO: eval-source-map has different module paths than source-map.
// We're currently not actually ignore listing anything.
shouldIgnorePath,
})
)
} else {
config.devtool = false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Source: https://github.com/mondaychen/devtools-ignore-webpack-plugin/blob/e35ce41d9606a92a455ef247f509a1c2ccab5778/src/index.ts

import { webpack } from 'next/dist/compiled/webpack/webpack'

// Following the naming conventions from
// https://tc39.es/source-map/#source-map-format
const IGNORE_LIST = 'ignoreList'

const PLUGIN_NAME = 'devtools-ignore-plugin'

interface SourceMap {
sources: string[]
[IGNORE_LIST]: number[]
}

interface PluginOptions {
shouldIgnorePath?: (path: string) => boolean
isSourceMapAsset?: (name: string) => boolean
}

interface ValidatedOptions extends PluginOptions {
shouldIgnorePath: Required<PluginOptions>['shouldIgnorePath']
isSourceMapAsset: Required<PluginOptions>['isSourceMapAsset']
}

function defaultShouldIgnorePath(path: string): boolean {
return path.includes('/node_modules/') || path.includes('/webpack/')
}

function defaultIsSourceMapAsset(name: string): boolean {
return name.endsWith('.map')
}

/**
* This plugin adds a field to source maps that identifies which sources are
* vendored or runtime-injected (aka third-party) sources. These are consumed by
* Chrome DevTools to automatically ignore-list sources.
*/
export default class DevToolsIgnorePlugin {
options: ValidatedOptions

constructor(options: PluginOptions = {}) {
this.options = {
shouldIgnorePath: options.shouldIgnorePath ?? defaultShouldIgnorePath,
isSourceMapAsset: options.isSourceMapAsset ?? defaultIsSourceMapAsset,
}
}

apply(compiler: webpack.Compiler) {
const { RawSource, } = compiler.webpack.sources

compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
additionalAssets: true,
},
(assets) => {
for (const [name, asset] of Object.entries(assets)) {
// TODO: Does not work for eval-source-map.
// We can use `asset.map()` but it's apparently slow and unclear how to write back to it.

// Instead of using `asset.map()` to fetch the source maps from
// SourceMapSource assets, process them directly as a RawSource.
// This is because `.map()` is slow and can take several seconds.
if (!this.options.isSourceMapAsset(name)) {
// Ignore non source map files.
continue
}

const mapContent = asset.source().toString()
if (!mapContent) {
continue
}

const sourcemap = JSON.parse(mapContent) as SourceMap

const ignoreList = []
for (const [index, path] of sourcemap.sources.entries()) {
if (this.options.shouldIgnorePath(path)) {
ignoreList.push(index)
}
}

;(sourcemap as unknown as SourceMap)[IGNORE_LIST] = ignoreList
compilation.updateAsset(
name,
new RawSource(JSON.stringify(sourcemap))
)
}
}
)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
import {
type webpack,
type SourceMapDevToolPluginOptions,
ConcatenatedModule,
makePathsAbsolute,
ModuleFilenameHelpers,
NormalModule,
RuntimeGlobals,
SourceMapDevToolModuleOptionsPlugin,
} from 'next/dist/compiled/webpack/webpack'
import type { RawSourceMap } from 'next/dist/compiled/source-map'

const cache = new WeakMap<webpack.sources.Source, webpack.sources.Source>()

const devtoolWarningMessage = `/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
`

// @ts-expect-error -- can't compare `string` with `number` in `version`Ï
interface SourceMap extends RawSourceMap {
ignoreList?: number[]
version: number
}

export interface EvalSourceMapDevToolPluginOptions
extends SourceMapDevToolPluginOptions {
// Fork
shouldIgnorePath?: (modulePath: string) => boolean
}

// Fork of webpack's EvalSourceMapDevToolPlugin with support for adding `ignoreList`.
// https://github.com/webpack/webpack/blob/e237b580e2bda705c5ab39973f786f7c5a7026bc/lib/EvalSourceMapDevToolPlugin.js#L37
export default class EvalSourceMapDevToolPlugin {
sourceMapComment: string
moduleFilenameTemplate: NonNullable<
EvalSourceMapDevToolPluginOptions['moduleFilenameTemplate']
>
namespace: NonNullable<EvalSourceMapDevToolPluginOptions['namespace']>
options: EvalSourceMapDevToolPluginOptions
shouldIgnorePath: (modulePath: string) => boolean

/**
* @param {SourceMapDevToolPluginOptions|string} inputOptions Options object
*/
constructor(inputOptions: EvalSourceMapDevToolPluginOptions) {
let options: EvalSourceMapDevToolPluginOptions
if (typeof inputOptions === 'string') {
options = {
append: inputOptions,
}
} else {
options = inputOptions
}
this.sourceMapComment =
options.append && typeof options.append !== 'function'
? options.append
: '//# sourceURL=[module]\n//# sourceMappingURL=[url]'
this.moduleFilenameTemplate =
options.moduleFilenameTemplate ||
'webpack://[namespace]/[resource-path]?[hash]'
this.namespace = options.namespace || ''
this.options = options

// fork
this.shouldIgnorePath = options.shouldIgnorePath ?? (() => false)
}

/**
* Apply the plugin
* @param compiler the compiler instance
*/
apply(compiler: webpack.Compiler): void {
const options = this.options
compiler.hooks.compilation.tap(
'NextJSEvalSourceMapDevToolPlugin',
(compilation) => {
const { JavascriptModulesPlugin } = compiler.webpack.javascript
const { RawSource, ConcatSource } = compiler.webpack.sources

const devtoolWarning = new RawSource(devtoolWarningMessage)

const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation)

new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation)
const matchModule = ModuleFilenameHelpers.matchObject.bind(
ModuleFilenameHelpers,
options
)

hooks.renderModuleContent.tap(
'NextJSEvalSourceMapDevToolPlugin',
(source, m, { chunk, runtimeTemplate, chunkGraph }) => {
const cachedSource = cache.get(source)
if (cachedSource !== undefined) {
return cachedSource
}

const result = (
r: webpack.sources.Source
): webpack.sources.Source => {
cache.set(source, r)
return r
}

if (m instanceof NormalModule) {
const module = m
if (!matchModule(module.resource)) {
return result(source)
}
} else if (m instanceof ConcatenatedModule) {
const concatModule = m
if (concatModule.rootModule instanceof NormalModule) {
const module = concatModule.rootModule
if (!matchModule(module.resource)) {
return result(source)
}
} else {
return result(source)
}
} else {
return result(source)
}

const namespace = compilation.getPath(this.namespace, {
chunk,
})
let sourceMap: SourceMap
let content
if (source.sourceAndMap) {
const sourceAndMap = source.sourceAndMap(options)
sourceMap = sourceAndMap.map as SourceMap
content = sourceAndMap.source
} else {
sourceMap = source.map(options) as SourceMap
content = source.source()
}
if (!sourceMap) {
return result(source)
}

// Clone (flat) the sourcemap to ensure that the mutations below do not persist.
sourceMap = { ...sourceMap }
const context = compiler.options.context!
const root = compiler.root
const modules = sourceMap.sources.map((sourceMapSource) => {
if (!sourceMapSource.startsWith('webpack://'))
return sourceMapSource
sourceMapSource = makePathsAbsolute(
context,
sourceMapSource.slice(10),
root
)
const module = compilation.findModule(sourceMapSource)
return module || sourceMapSource
})
let moduleFilenames = modules.map((module) =>
ModuleFilenameHelpers.createFilename(
module,
{
moduleFilenameTemplate: this.moduleFilenameTemplate,
namespace,
},
{
requestShortener: runtimeTemplate.requestShortener,
chunkGraph,
// @ts-expect-error -- Original code
hashFunction: compilation.outputOptions.hashFunction,
}
)
)
moduleFilenames = ModuleFilenameHelpers.replaceDuplicates(
moduleFilenames,
(filename, _i, n) => {
for (let j = 0; j < n; j++) filename += '*'
return filename
}
)
sourceMap.sources = moduleFilenames
sourceMap.ignoreList = []
for (let index = 0; index < moduleFilenames.length; index++) {
if (this.shouldIgnorePath(moduleFilenames[index])) {
sourceMap.ignoreList.push(index)
}
}
if (options.noSources) {
sourceMap.sourcesContent = undefined
}
sourceMap.sourceRoot = options.sourceRoot || ''
const moduleId =
/** @type {ModuleId} */
chunkGraph.getModuleId(m)
sourceMap.file =
typeof moduleId === 'number' ? `${moduleId}.js` : moduleId

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

return result(
new RawSource(
`eval(${
compilation.outputOptions.trustedTypes
? `${RuntimeGlobals.createScript}(${JSON.stringify(
content + footer
)})`
: JSON.stringify(content + footer)
});`
)
)
}
)
hooks.inlineInRuntimeBailout.tap(
'EvalDevToolModulePlugin',
() => 'the eval-source-map devtool is used.'
)
hooks.render.tap(
'EvalSourceMapDevToolPlugin',
(source) => new ConcatSource(devtoolWarning, source)
)
hooks.chunkHash.tap('EvalSourceMapDevToolPlugin', (_chunk, hash) => {
hash.update('EvalSourceMapDevToolPlugin')
hash.update('2')
})
if (compilation.outputOptions.trustedTypes) {
compilation.hooks.additionalModuleRuntimeRequirements.tap(
'EvalSourceMapDevToolPlugin',
(_module, set, _context) => {
set.add(RuntimeGlobals.createScript)
}
)
}
}
)
}
}
Loading

0 comments on commit 91bac8c

Please sign in to comment.