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.

eval-source-map vs inline-source-map: what the error tracker receives Two parallel tracks showing the build and runtime outcome of eval-source-map (fragmented, unresolvable) versus inline-source-map (single data-URI pragma, fully resolvable). eval-source-map (broken) inline-source-map (fixed) bundle.js eval("...//# sourceURL=src/a.js") eval("...//# sourceURL=src/b.js") Error tracker receives: no sourceMappingURL → symbolication fails eval at <anonymous>:1:1 (raw frame) bundle.js /* compiled output */ //# sourceMappingURL=data:...base64, Error tracker receives: complete v3 map → full symbolication src/utils/auth.ts:42 (resolved)

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-map and upload via CI. Inline format is appropriate for development and staging.

  • CSP false alarm. The data: URI in //# sourceMappingURL is read by DevTools as a comment, not executed as a script. It does not require data: in your script-src CSP directive. However, some security scanners incorrectly flag it — add a suppression annotation rather than weakening the CSP.

  • Eval wrapper remnants. If you switch devtool but 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 changing devtool: rm -rf .webpack_cache dist before rebuilding.

  • source-map vs source-map-js package confusion. Mozilla’s source-map package (v0.7+) uses a WASM decoder and requires await new SourceMapConsumer(raw). The community fork source-map-js (v1.x) is synchronous and skips WASM. Their APIs appear identical until you hit async initialization — mixing them produces consumer.originalPositionFor is not a function errors 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.