Converting eval Source Maps to Inline Format
When Webpack’s eval-family devtool settings fragment source mappings across per-module eval() wrappers, external error-tracking SDKs receive no usable sourceMappingURL pragma and cannot symbolicate any frame. This page shows how to diagnose the failure, switch build configuration to emit a single inline base64 data-URI map, and programmatically merge eval-embedded maps when you cannot change the build tool directly. Familiarity with the v3 spec fields covered in Understanding Source Map v3 Specification and Formats is assumed; the broader source map generation and stack trace debugging context covers production upload strategies.
Symptom / Trigger
The failure manifests in your error tracker as frames pointing at eval execution contexts rather than original source files:
Error: Cannot read properties of undefined (reading 'userId')
at eval (webpack:///src/utils/auth.js?:14:9)
at Object.eval (webpack:///src/index.js?:3:1)
at eval ()
at Module../src/index.js (bundle.js:1:457)
The webpack:/// protocol prefix and the trailing ? are diagnostic markers of Webpack’s eval devtool mode. The SDK sees eval as the function name and <anonymous> as the file — neither resolves to an original source.
Confirm the root cause before changing any configuration:
# This grep returns nothing when eval mode is active — no global sourceMappingURL pragma
grep 'sourceMappingURL' dist/bundle.js
# This shows the per-module sourceURL injected inside eval strings instead
grep 'sourceURL' dist/bundle.js | head -5
# Output: //# sourceURL=webpack:///src/utils/auth.js?
# //# sourceURL=webpack:///src/index.js?
The empty output from the first command is the definitive trigger. If sourceMappingURL is absent from the compiled output, no SDK-level symbolication is possible regardless of what .map files exist on disk.
Root Cause Explanation
Webpack’s devtool option has a family of eval-prefixed values (eval, eval-source-map, eval-cheap-source-map, eval-cheap-module-source-map) that prioritize rebuild speed by compiling each module independently and wrapping it in an eval() call:
// What Webpack outputs for each module in eval-source-map mode
eval("\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", { value: true });\n// ... compiled module code ...\n//# sourceURL=webpack:///src/utils/auth.js?\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZX...");
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// The per-module inline map exists — but it's INSIDE the eval string, invisible to external SDKs
Each module’s //# sourceMappingURL comment lives inside the string argument to eval(). The JavaScript engine (V8 or SpiderMonkey) can read it during DevTools debugging sessions, but error-tracking SDKs that inspect Error.stack at runtime see only the outer execution context: eval at <anonymous>. The per-module maps are unreachable from outside the eval scope.
The broken pattern contrasts directly with what a non-eval devtool produces: a single //# sourceMappingURL comment appended as a real JavaScript comment at the end of the bundle file, readable by any string scan of the artifact.
Step-by-Step Fix
1. Switch Webpack devtool to inline-source-map
// webpack.config.js
module.exports = {
mode: 'development', // use 'production' + hidden-source-map for prod builds
devtool: 'inline-source-map', // replaces eval-source-map; emits single data-URI pragma
// No other changes needed — Webpack merges all module maps automatically
output: {
filename: 'bundle.js',
path: __dirname + '/dist',
},
};
After this change, grep 'sourceMappingURL' dist/bundle.js returns a single line containing the full base64-encoded map. Rebuild time increases because Webpack can no longer cache individual module maps independently; for large projects, consider source-map (external file, fastest full rebuild) instead of inline-source-map.
2. Switch Vite to inline sourcemap mode
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
sourcemap: 'inline', // 'true' writes an external .map file; 'inline' embeds the data-URI
minify: 'esbuild', // minifier must also pass sourcemap: true through its own pipeline
},
});
Note that Vite’s sourcemap: true (boolean) produces an external .map file with a //# sourceMappingURL=bundle.js.map reference — this is not the same as 'inline'. If your error tracker’s SDK does not follow the external reference (common in sandboxed iframe contexts), the inline string form is necessary.
3. Programmatically merge eval-embedded maps into a single inline map
When you cannot modify the build tool configuration (legacy monorepo, vendor-locked CI pipeline), extract the per-module maps from the bundle and merge them into an index source map:
const fs = require('fs');
const sourceMap = require('source-map');
async function mergeEvalMapsToInline(bundlePath) {
const bundleCode = fs.readFileSync(bundlePath, 'utf8');
// Extract all per-module inline maps from inside eval() strings
const moduleMapPattern = /sourceMappingURL=data:application\/json;(?:charset=utf-8;)?base64,([A-Za-z0-9+/=]+)/g;
const sections = [];
let match;
let generatedOffset = 0; // track generated-line offset per module (simplified; real impl needs AST)
while ((match = moduleMapPattern.exec(bundleCode)) !== null) {
const raw = Buffer.from(match[1], 'base64').toString('utf8'); // decode base64 to JSON string
sections.push({
offset: { line: generatedOffset, column: 0 },
map: JSON.parse(raw),
});
generatedOffset++; // real implementation: count newlines between match positions
}
if (sections.length === 0) {
throw new Error('No embedded maps found — bundle may not be in eval mode');
}
// Build an index source map (v3 "sections" form) from the per-module maps
const indexMap = { version: 3, sections };
// Base64-encode and append as a global pragma
const b64 = Buffer.from(JSON.stringify(indexMap)).toString('base64');
const pragma = `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`;
fs.appendFileSync(bundlePath, pragma); // modifies the bundle in-place
console.log(`Merged ${sections.length} module maps into inline index map`);
}
mergeEvalMapsToInline('./dist/bundle.js');
This approach is a best-effort extraction. For accurate offset values, parse the bundle AST or count newlines between eval( call sites. The simplified version above works for small bundles where modules are each on their own line.
4. Validate the converted output
const sourceMap = require('source-map');
const fs = require('fs');
async function validateInlineMap(bundlePath) {
const bundle = fs.readFileSync(bundlePath, 'utf8');
const match = bundle.match(/sourceMappingURL=data:application\/json;(?:charset=utf-8;)?base64,([A-Za-z0-9+/=]+)/);
if (!match) throw new Error('No inline sourceMappingURL found — conversion did not produce a pragma');
const raw = Buffer.from(match[1], 'base64').toString('utf8'); // decode to JSON
const mapObj = JSON.parse(raw);
if (mapObj.version !== 3) throw new Error(`Expected version 3, got ${mapObj.version}`);
const consumer = await new sourceMap.SourceMapConsumer(raw);
console.log('Sources resolved:', consumer.sources); // should list original file paths, not eval URLs
consumer.destroy();
}
validateInlineMap('./dist/bundle.js');
A successful run prints Sources resolved: [ 'src/utils/auth.js', 'src/index.js', ... ] — original paths, not webpack:/// eval URLs.
Verification
# Confirm the pragma exists and is non-trivial
node -e "
const b = require('fs').readFileSync('./dist/bundle.js', 'utf8');
const m = b.match(/sourceMappingURL=data:[^,]+,([A-Za-z0-9+/=]+)/);
if (!m) { console.error('FAIL: no inline map'); process.exit(1); }
const json = JSON.parse(Buffer.from(m[1], 'base64').toString('utf8'));
console.log('version:', json.version); // must print 3
console.log('sources:', json.sources); // must list original source files
console.log('sourcesContent present:', !!json.sourcesContent); // true for full offline resolution
"
Expected output:
version: 3
sources: [ 'src/utils/auth.js', 'src/index.js' ]
sourcesContent present: true
If sources contains webpack:/// paths or <anonymous>, the eval wrappers were not fully removed and the devtool change was not applied to the correct Webpack configuration file.
Edge Cases & Gotchas
-
Bundle size increase. Inline maps add roughly 33% overhead due to base64 encoding on top of the raw JSON size. A 500 KB bundle with a 400 KB map becomes roughly 1 MB. For production use
hidden-source-mapand upload via CI. Inline format is appropriate for development and staging. -
CSP false alarm. The
data:URI in//# sourceMappingURLis read by DevTools as a comment, not executed as a script. It does not requiredata:in yourscript-srcCSP directive. However, some security scanners incorrectly flag it — add a suppression annotation rather than weakening the CSP. -
Eval wrapper remnants. If you switch
devtoolbut do not clean the output directory (rm -rf dist), Webpack’s persistent cache may re-emit the old eval-based output for unchanged modules. Always clear the cache when changingdevtool:rm -rf .webpack_cache distbefore rebuilding. -
source-mapvssource-map-jspackage confusion. Mozilla’ssource-mappackage (v0.7+) uses a WASM decoder and requiresawait new SourceMapConsumer(raw). The community forksource-map-js(v1.x) is synchronous and skips WASM. Their APIs appear identical until you hit async initialization — mixing them producesconsumer.originalPositionFor is not a functionerrors if the wrong package’s consumer is passed to code expecting the other’s interface.
FAQ
Does converting to inline-source-map fix symbolication in all error trackers?
Yes, for SDK-based trackers (Sentry Browser SDK, Datadog RUM, LogRocket) that read the sourceMappingURL comment from the bundle at runtime. Server-side upload-based symbolication (Sentry CLI, @sentry/webpack-plugin) does not require inline format — it reads the map from disk. Use inline for environments where you cannot guarantee the .map file is co-located or uploadable.
Will Chrome DevTools still work after the switch from eval to inline?
Yes. DevTools reads //# sourceMappingURL from both inline and external forms. The switch from eval-source-map to inline-source-map removes the per-module fast-path caching that speeds up hot reloads in development, so incremental rebuild times increase. For development where rebuild speed matters more than SDK compatibility, keep eval-source-map locally and switch only in CI/staging builds.
Can I convert only specific modules rather than the entire bundle?
Not natively with Webpack’s devtool flag — it applies globally. Use source-map-loader with an include/exclude pattern to apply inline map generation selectively at the module level, then merge the result with a custom Webpack plugin that post-processes the final asset.