A small Node CLI that unpacks a single ("mono") JavaScript bundle — from webpack, rspack, rollup, esbuild, or Vite — into:
- One file per module (
modules/<id>.js) for webpack/rspack, or one file per chunk (chunks/<name>.js) for ES-module bundles. - A loader (
runtime.js+index.js) that reconstructs the original webpack/rspack bundle shape using the original UMD header and webpack runtime verbatim, so the unpacked tree runs identically to the original bundle. - A manifest (
manifest.json) with module/chunk IDs, sizes, dependency edges, and best-effort inferred names. - A rebuild script (
rebuild.js) that re-concatenates the (possibly edited) webpack/rspack module files back into a single runnable bundle.
📖 Full documentation: open.awareride.com/packscope/docs
git clone https://github.com/awareride/packscope.git
cd packscope
npm install # installs acorn + escodegen + js-beautifynpx packscope <bundle.js|URL> <outDir> [options]
# example (local webpack/rspack CLI bundle)
npx packscope ./examples/node_large_example.js ./out
# example from a remote URL
npx packscope https://example.com/app.js ./outOptions:
--beautify— pretty-print the output.- For webpack/rspack modules: regenerates module bodies via escodegen. Best-effort — for modules involved in fragile circular-dependency timings this can change statement order and break execution.
- For ES module chunks: pretty-prints the entry and each downloaded chunk with
js-beautify. Default OFF keeps the original source slices, which are guaranteed to run identically to the bundle.
--decompose— for ES module bundles, extract top-level classes, services, functions, and CommonJS-style module wrappers into a read-onlydecomposed/tree for navigation. Not executable; best-effort.--rename— rename the 3 wrapper params tomodule/exports/require(opt-in, scope-aware; only applies when--beautifyis on for webpack/rspack).--fetch-assets— auto-download referenced source maps and other asset URLs found in the bundle (default ON for URL inputs, OFF for local file inputs).--no-fetch-assets— skip downloading referenced assets.--entry <N>— force entry module id (auto-detected otherwise).--devtools— mirror the original site's URL paths into<outDir>(e.g.out/<host>/assets/index-*.jsinstead ofout/index-*.js). Chrome DevTools Local Overrides stores files at<folder>/<host>/<path>, so this keeps the local layout identical to the remote and lets you override the original URLs with zero symlinks and a single origin (no mixed-content / CORS / SSR hydration mismatch, and no parser-insertion race that breaks Tampermonkey-style DOM rewrites). For ESM bundles the entry and chunks keep their relative import structure; for webpack/rspack the rebuilt single bundle is written at the mirrored path.
header.js # original UMD + bundle header, up to `{` of the modules dictionary
webpack-runtime.js # original webpack/rspack runtime + footer, from `}` onward
runtime.js # loader: reconstructs the bundle with per-file module delegation
index.js # shebang entry; wires externals, runs the entry module (the CLI)
modules/<id>.js # one CommonJS file per webpack module (faithful original body)
assets/ # referenced source maps / asset URLs (when --fetch-assets is on)
manifest.json # ids, sizes, dependencies, inferred names, asset list
rebuild.js # regenerate a single runnable bundle from the module files
package.json # makes <outDir> a self-contained node package
node_modules -> ... # symlink to the source project's node_modules (best-effort)
<entry>.js # the entry chunk, import specifiers rewritten locally
chunks/ # all statically or dynamically imported JS chunks
sources/ # original source modules extracted from source maps (when available)
decomposed/ # best-effort class/module extracts for reading/navigation (with --decompose)
assets/ # referenced source maps / asset URLs (when --fetch-assets is on)
index.html # simple HTML page that loads the entry as a module
manifest.json # bundle type, chunk graph, source list, asset list
package.json # type: "module" for the unpacked tree
ES module bundles are already split into chunks by the bundler. Further splitting
into individual original modules requires source maps; when a chunk contains an
inline or external source map, the tool extracts each original source into
sources/.
Production builds often omit source maps. In that case you can use --decompose
to get a read-only, best-effort decomposition of each chunk into top-level
classes, services, functions, and CommonJS-style module wrappers. The files in
decomposed/ are NOT executable — they are for navigation and grep/inspection
only.
You can pass an http:// or https:// URL as the bundle argument. The file is
downloaded to a local cache directory (.packscope-cache/) and then unpacked as
usual:
# webpack/rspack bundle
npx packscope https://example.com/app.js ./out
# ES module bundle (rollup / esbuild / Vite)
npx packscope https://example.com/main-ABCD1234.js ./out
# ES module bundle with pretty-printed chunks
npx packscope https://example.com/main-ABCD1234.js ./out --beautify
# ES module bundle with best-effort per-class/per-module decomposition
npx packscope https://example.com/main-ABCD1234.js ./out --decompose
# both
npx packscope https://example.com/main-ABCD1234.js ./out --beautify --decomposeFor ES module bundles, the tool automatically resolves and downloads all statically and dynamically imported chunks from the same base URL, then rewrites their import specifiers to local relative paths so the unpacked tree can be loaded in a browser.
By default, referenced source maps and other asset URLs found inside the bundle
are also downloaded into out/assets/. Failures are logged but do not stop the
unpack. To skip passive asset downloads:
npx packscope https://example.com/bundle.js ./out --no-fetch-assetsChunk downloads for ES module bundles happen regardless of --fetch-assets,
because the chunks are part of the executable module graph.
node out/index.js --version # should print the same version as the original
node out/index.js --help
node out/index.js --print "hello"The unpacked ES module tree can be served by any static HTTP server and opened in a browser:
cd out && python3 -m http.server 8080
# open http://localhost:8080/index.htmlFor the fastest edit-and-reload loop inside a real browser, use Chrome DevTools
Local Overrides together with the --devtools flag. packscope mirrors the
original site's URL paths (including the host) into out/, so Chrome swaps the
responses in place — no local server, no symlinks, single origin.
npx packscope --devtools https://example.com/assets/index-CLHtNMqj.js ./outWith --devtools, the unpacked files land at the same paths as the original
URLs, e.g.:
out/example.com/assets/index-CLHtNMqj.js # entry (was /assets/index-*.js on the site)
out/example.com/assets/<chunk>.js # each imported chunk
Then in Chrome:
- Open the page you want to debug (e.g.
https://example.com/chatPc). - Open DevTools (F12) → Sources panel → left sidebar → Overrides.
- Click + Select folder for overrides and choose
./out— theout/directory itself, notout/example.com/. - Click Allow, then enable Enable Local Overrides.
- Reload the page. Chrome now serves
https://example.com/assets/*fromout/example.com/assets/*.
See Google's guide: https://developer.chrome.com/docs/devtools/overrides
-
ES module bundles (Vite / rollup / esbuild): edit any file under
out/example.com/assets/and reload — the change is live, no rebuild needed. -
webpack / rspack bundles: edit
out/modules/<id>.js, then regenerate the single bundle (written to the mirrored path) and reload:node out/rebuild.js
- No local server. Chrome serves the overridden files straight from disk,
so
packscope serveis not needed for this workflow. - Single origin. The file paths match the original URLs, so there is no mixed-content, no CORS, and no SSR hydration mismatch.
- No parser-insertion race. The swap happens at the network layer, so it
works even for parser-inserted
<script type="module">tags (where a Tampermonkey-style DOM rewrite would lose the race and load the original).
Note:
packscope serveonly matters if you instead prefer a proxy (whistle / mitmproxy) that maps the origin to127.0.0.1:8765.
node out/rebuild.js bundle-edited.js
node bundle-edited.js --version- Parse the bundle with acorn and locate the webpack modules dictionary. It is found by shape: the tool identifies the webpack-style require function (the one that calls
<modules>[id].call(...)) and then resolves the variable that holds the modules object. This works even when__webpack_modules__and__webpack_require__have been minified to single-letter identifiers. - For each module, extract the original factory body verbatim and write it as:
Preserving the original wrapper shape (function vs. arrow) keeps the
// webpack module <id> // params: eA, el, ec (=> module, exports, require) module.exports = function(eA, el, ec) { <original body> }; // or, when the original wrapper is an arrow function: module.exports = (eA, el, ec) => { <original body> };
this-binding semantics identical, so the unpacked tree executes exactly like the original. - The loader (
runtime.js) rebuilds the original bundle expression:header.js+__webpack_modules__ = { <delegator props> }+webpack-runtime.js. Each__webpack_modules__[id]is a tiny delegator thatrequires the correspondingmodules/<id>.jsand calls its factory with the originalmodule/exports/require. - Externals (e.g.
chokidar) are detected from theCannot find module '<pkg>'shim and wired in via the UMD header (same as the original).
Production webpack bundles often contain fragile circular-dependency timings and
let/const TDZ patterns that are extremely sensitive to exact statement order.
Even AST-faithful code generators like escodegen can subtly shift the relative
position of variable initializations and require() calls, which causes runtime errors
that the original bundle does not have.
For that reason the default output keeps the original minified body slices — they
are still in separate, navigable files with documented param mappings, and they are
guaranteed executable. Use --beautify when you want prettier code for reading,
but be aware it is best-effort.
Full documentation is available at open.awareride.com/packscope/docs.
| Page | Description |
|---|---|
| Overview | What Packscope does and why |
| Getting Started | Installation and first unpack |
| CLI Reference | All options and examples |
| DevTools Overrides | Chrome DevTools workflow |
| Architecture | How Packscope works under the hood |
Documentation source lives in docs/ - English pages in en/ (the default locale and source of truth) plus Chinese translations in zh/ - and is synced to the central hub on every push to main.
MIT — see LICENSE.