Skip to content

Commit

Permalink
Ensure Next.js is ignore-listed when used as external
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Nov 12, 2024
1 parent cf55399 commit a8f3203
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
Forked to add support for `ignoreList`.
Keep in sync with packages/next/webpack-plugins/eval-source-map-dev-tool-plugin.js
*/
import {
type webpack,
Expand Down
69 changes: 69 additions & 0 deletions packages/next/webpack-plugins/devtools-ignore-list-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Source: https://github.com/mondaychen/devtools-ignore-webpack-plugin/blob/e35ce41d9606a92a455ef247f509a1c2ccab5778/src/index.ts

// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const webpack = require('webpack')

// Following the naming conventions from
// https://tc39.es/source-map/#source-map-format
const IGNORE_LIST = 'ignoreList'
const PLUGIN_NAME = 'devtools-ignore-plugin'
function defaultShouldIgnorePath(path) {
return path.includes('/node_modules/') || path.includes('/webpack/')
}
function defaultIsSourceMapAsset(name) {
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.
*/
module.exports = class DevToolsIgnorePlugin {
options
constructor(options = {}) {
this.options = {
shouldIgnorePath: options.shouldIgnorePath ?? defaultShouldIgnorePath,
isSourceMapAsset: options.isSourceMapAsset ?? defaultIsSourceMapAsset,
}
}
apply(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)) {
// 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)
const ignoreList = []
for (const [index, path] of sourcemap.sources.entries()) {
if (this.options.shouldIgnorePath(path)) {
ignoreList.push(index)
}
}
sourcemap[IGNORE_LIST] = ignoreList
compilation.updateAsset(
name,
new RawSource(JSON.stringify(sourcemap))
)
}
}
)
})
}
}
219 changes: 219 additions & 0 deletions packages/next/webpack-plugins/eval-source-map-dev-tool-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
Forked to add support for `ignoreList`.
Keep in sync with packages/next/webpack-plugins/eval-source-map-dev-tool-plugin.js
*/
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const ConcatenatedModule = require('webpack/lib/optimize/ConcatenatedModule')
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const { makePathsAbsolute } = require('webpack/lib/util/identifier')
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers')
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const NormalModule = require('webpack/lib/NormalModule')
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const RuntimeGlobals = require('webpack/lib/RuntimeGlobals')
// eslint-disable-next-line import/no-extraneous-dependencies -- this is a dev-only file
const SourceMapDevToolModuleOptionsPlugin = require('webpack/lib/SourceMapDevToolModuleOptionsPlugin')

const cache = new WeakMap()
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`Ï

// Fork of webpack's EvalSourceMapDevToolPlugin with support for adding `ignoreList`.
// https://github.com/webpack/webpack/blob/e237b580e2bda705c5ab39973f786f7c5a7026bc/lib/EvalSourceMapDevToolPlugin.js#L37
module.exports = class EvalSourceMapDevToolPlugin {
sourceMapComment
moduleFilenameTemplate
namespace
options
shouldIgnorePath

/**
* @param {SourceMapDevToolPluginOptions|string} inputOptions Options object
*/
constructor(inputOptions) {
let options
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) {
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) => {
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
let content
if (source.sourceAndMap) {
const sourceAndMap = source.sourceAndMap(options)
sourceMap = sourceAndMap.map
content = sourceAndMap.source
} else {
sourceMap = source.map(options)
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,
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)
if (moduleId) {
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)
}
)
}
}
)
}
}
15 changes: 14 additions & 1 deletion packages/next/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ const webpack = require('webpack')
const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const EvalSourceMapDevToolPlugin = require('./webpack-plugins/eval-source-map-dev-tool-plugin')
const DevToolsIgnoreListPlugin = require('./webpack-plugins/devtools-ignore-list-plugin')

function shouldIgnorePath(modulePath) {
// For consumers, everything will be considered 3rd party dependency if they use
// the bundles we produce here.
// In other words, this is all library code and should therefore be ignored.
return true
}

const pagesExternals = [
'react',
Expand Down Expand Up @@ -156,7 +165,8 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => {
libraryTarget: 'commonjs2',
},
devtool: process.env.NEXT_SERVER_EVAL_SOURCE_MAPS
? 'eval-source-map'
? // We'll use a fork in plugins
false
: 'source-map',
optimization: {
moduleIds: 'named',
Expand All @@ -181,6 +191,9 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => {
],
},
plugins: [
process.env.NEXT_SERVER_EVAL_SOURCE_MAPS
? new EvalSourceMapDevToolPlugin({ shouldIgnorePath })
: new DevToolsIgnoreListPlugin({ shouldIgnorePath }),
new webpack.DefinePlugin({
'typeof window': JSON.stringify('undefined'),
'process.env.NEXT_MINIMAL': JSON.stringify('true'),
Expand Down
2 changes: 1 addition & 1 deletion test/integration/server-side-dev-errors/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('server-side dev errors', () => {
' ⨯ ReferenceError: missingVar is not defined' +
'\n at getServerSideProps (./test/integration/server-side-dev-errors/pages/gssp.js:6:3)' +
// TODO(veil): Should be sourcemapped
'\n a'
'\n at'
)
} else {
expect(stderrOutput).toStartWith(
Expand Down

0 comments on commit a8f3203

Please sign in to comment.