rea-reverse

JS bundle and source map restorer

Open a source map, a minified bundle, or both, to see what the map contains, rebuild the original source tree from sourcesContent and download it as a .zip, and map bundle positions or a pasted stack trace back to the original file, line and name.

Your files never leave your browser. The map and the bundle are read locally in a Web Worker that has no network access. Nothing is uploaded, and URLs inside the files are never loaded. How to verify this yourself

Drop a .map file and/or a bundle here

or

A .map, a .js / .mjs / .cjs / .css bundle, or both together. A bundle with an inline data: map works on its own. Up to 256 MiB in total.

What this tool cannot do

  • It only restores what the map carries: without sourcesContent there is no original text, only file names and positions.
  • Bundles without a source map are not unminified or unbundled (no Wakaru, no renaming).

A production JavaScript bundle is one or a few very long lines of renamed code. When the build also ships a source map (a .map file, or an inline data: URL at the end of the bundle), that map links every position in the bundle to a file, line and column of the original code. Many maps also embed the original files themselves in a field called sourcesContent.

This tool reads a source map in your browser and gives you three things: a report of what the map contains, the original source tree rebuilt from sourcesContent (browse it, preview a file, download one, or download everything as a .zip), and a lookup that turns a bundle position or a pasted stack trace into the original file, line, column and name. Nothing is uploaded.

What it does

Input. Drop or choose one or more files: a .map file, a .js, .mjs, .cjs or .css bundle, or both together. Files with another name are sniffed (a map starts with {). When you add a bundle, its last sourceMappingURL comment is read:

  • an inline map (//# sourceMappingURL=data:application/json;base64,...) is decoded and used, so the bundle works on its own;
  • an external reference (//# sourceMappingURL=app.js.map) is shown in the report with a note to add that file to the upload. The page never loads it for you.

If you upload several maps, the one the bundle names is used (or <bundle>.map, or the first one); the others are listed in the warnings. Limits: 256 MiB for all files together, 3,000,000 mapping segments, 2,000,000 generated lines, 200,000 sources, 10,000 index-map sections nested up to 64 levels, and a two-minute time limit.

Report.

  • Map fields: the format (a regular map, or an index map with sections, which is flattened), file, sourceRoot, the number of sources and how many carry sourcesContent, the number of names, mapping segments and generated lines.
  • Ignore list: how many sources are in ignoreList or the older x_google_ignoreList (code the bundler marks as third-party or generated). Those sources are tagged in the tree and in lookups.
  • Bundler hints: counts of source names that look like webpack (webpack://), Turbopack (turbopack://), Vite (/@fs/, /@vite/), Rollup (\0 virtual modules, commonjsHelpers), Rolldown, esbuild (<stdin>) or Parcel (/__parcel_source_root/). These are hints from names, not proof of the bundler.
  • node_modules share: how many sources live under a node_modules/ folder.
  • Bundle: size, number of lines and what its sourceMappingURL says.

Original sources. Every source that has content appears in a collapsible, searchable tree. Paths are normalized: the sourceRoot is joined, URL schemes such as webpack:// and file:// are dropped, . and .. segments are resolved, and paths stay inside the tree. You can preview a file as text, download one file, or click Download all as .zip. Sources that the map names but does not carry are listed separately.

Position lookup. Paste one or more lines into the lookup box:

  • a position such as 1:2446 (line and column, both counted from 1, as DevTools and stack traces show them);
  • stack trace lines from Chrome or Node (at explode (https://example.com/assets/app.js:1:2446)), or Firefox and Safari (explode@https://example.com/assets/app.js:1:2446).

Each result shows the original path:line:column, the original name when the map records one, and a few lines of the original code around it with the target marked. When the bundle was uploaded, the generated code around the position is shown too. Frames that name a different file are marked other file; if no frame names the uploaded bundle (for example after a rename), every frame is mapped.

How it works

Source map version 3 is defined by ECMA-426. The mappings field is a long string of Base64 VLQ numbers: ; separates generated lines, , separates segments, and each segment holds 1, 4 or 5 numbers (generated column, then source index, original line, original column and optional name index).

Before anything is decoded, the page checks the structure of the map and scans mappings once without decoding it. That pass counts segments and lines, rejects characters outside Base64, segments with 2 or 3 fields and numbers wider than 32 bits, and stops as soon as a limit is passed. Index maps are checked the same way, section by section: offsets must strictly increase, and a section that points to another map by url is refused, because the page loads nothing over the network. Then the map is decoded with @jridgewell/trace-mapping 0.3.31, and every decoded segment is checked against its own section’s sources and names.

A lookup takes the greatest generated position at or before the one you asked for, across lines, and returns every segment at that position. This rule, the preflight checks and their order follow REA, the open-source project by morluto (MIT), which traces positions through source maps in its JavaScript analysis.

The .zip is written in the page: stored entries (no compression), UTF-8 names and a CRC-32 per file. Before writing, every path is cleaned so the archive cannot write outside the folder you extract it to: no absolute paths, no .., no empty names, no characters that Windows rejects, and duplicate names (including names that differ only in letter case) get a ~2 suffix.

How your files stay private

Your files are not uploaded:

  • Parsing, decoding, lookups and the .zip all run in a Web Worker on your device. URLs in the files, including the sourceMappingURL of a bundle and the url of an index-map section, are never requested.
  • Source code, file names, paths and snippets are shown as plain text; nothing from the files is interpreted as HTML or run.
  • Analytics events are coarse: success or an error code, where the map came from (file, inline or none), a range for the number of sources (such as 11-100), a size bucket and a duration bucket. They never contain file names, source paths, positions, stack traces or code. The full list is on the privacy page.
  • The tool page shows no ads.

How to verify this yourself

  1. Open your browser’s developer tools and switch to the Network tab before you choose a file. Analyze a map, run a lookup and download the .zip. The only requests are page assets and the worker script /_astro/sourcemap-worker-*.js; none carries your files.
  2. Or load the page, go offline (turn off Wi-Fi, or tick Offline in the Network tab) and analyze a file. It still works.
  3. The worker runs under the Content Security Policy default-src 'none'; script-src 'self'; connect-src 'none', so the browser does not let it make any network request.

Limitations

  • No map, no restoration. Without a source map the tool does not unminify, rename variables or split the bundle into modules. That needs a deobfuscator such as Wakaru (unminify / unpack), which is out of scope here; the bundle recovery tutorial shows how REA uses it.
  • Content only if the map carries it. A map built with sourcesContent disabled (for example webpack’s nosources-source-map) gives you names and positions, but no original text.
  • Original text is what the bundler saw. For TypeScript, Vue or JSX the content is usually the original file, but some loaders store an intermediate, already transformed version.
  • External maps are not loaded. If the bundle points to a separate .map file, add that file yourself. Index maps whose sections use url must be merged into one file first.
  • Overlapping sections are rejected, as REA does, even where some other tools accept them.
  • Columns. Lookups use UTF-16 columns, as JavaScript engines and the source map format do. Positions after the last mapping of a line resolve to that last mapping.
  • Lookup budget. Up to 200 positions or stack lines per lookup; the rest are counted and skipped. Lines longer than 1,024 characters are ignored.
  • Zip format. No ZIP64: up to 65,534 files and 4 GiB in one archive.

FAQ

Where do I find a site’s source map?

Check the last line of a script for a sourceMappingURL comment, or the SourceMap HTTP response header. In DevTools, the Sources panel shows mapped original files when a map is present. Only download maps from sites you are allowed to analyze.

Why are some files listed without content?

The map names them but its sourcesContent entry is null or missing. They are in the “Sources without content” list; lookups still give you their file, line and column.

My stack trace maps to the wrong place.

Make sure the map belongs to the exact same build as the bundle that threw (bundles with the same name from different deployments do not match), and that the column comes from the browser, not from a beautified copy of the file.

Why do some paths have ~2 in their name?

Two sources ended up at the same path after cleaning (for example webpack://app/./src/a.js and webpack:///src/a.js, or A.js and a.js). The second one is renamed so both survive extraction on any system.

Can I map original positions back to the bundle?

Not in this tool. It maps generated positions to original ones, which is what you need for stack traces and for reading minified code.

Compliance

Analyze bundles and maps from your own projects, from projects you are authorized to assess, or from open-source software. Respect the licenses of the code you restore, including any terms that restrict reverse engineering, and the laws that apply to you. See the disclaimer.