How to find the exact version of an npm package used by a site
Most technology detectors use regex against URLs and headers, which rarely surfaces more than a major version. The only reliable way to get the exact semver is to read the package's own `package.json`, which shows up inside the site's sourcemap whenever the bundler includes `node_modules/<pkg>/package.json` as a source. Once you know to look there, the workflow is mechanical: fetch the map, walk `sources[]`, parse the matching `sourcesContent[]` entry, read the `version` field. No guessing, no inference, no false confidence.
By Mapree ·
Background
Every real-world bundler — webpack, esbuild, Rollup, Vite, SWC, Turbopack, Bun's built-in bundler, Parcel — resolves your `import 'react'` statements against `node_modules/react/` on disk. When the bundler emits a sourcemap, each source file it used gets a `sources[]` entry with its original path. And crucially, `node_modules/<pkg>/package.json` is often included as one of those source files — either because the bundler's module resolver touched it directly during the build, because a Babel/SWC plugin reached for it, or because the bundler's import-meta features inlined it. When it's included, the matching `sourcesContent[]` entry is the literal contents of that `package.json`, including the `name` and `version` fields, the dependency tree, the export map, the type definitions, the engines requirements — everything you'd see if you opened the package on disk yourself.
That means the exact semver of every bundled library is sitting inside the sourcemap, text-searchable, if you know to look for it. The extraction is mechanical: walk every entry in `sources[]`, filter for ones ending in `node_modules/<name>/package.json` (or `./node_modules/<name>/package.json`, depending on how the bundler wrote the path), grab the matching index from `sourcesContent[]`, JSON.parse the string, read `version`. No regex guessing, no version inference from URL hashes, no fragile heuristics that get confused by minified bundles. Just JSON. The only failure mode is the small set of cases where the bundler decided to omit `package.json` from the map for byte-saving reasons, and even then path-based detection still tells you the package is present.
This approach works equally well for JavaScript and CSS packages. `node_modules/tailwindcss/package.json` shows up inside the CSS sourcemap on Tailwind-using sites; `node_modules/postcss/package.json` shows up alongside it; `@radix-ui/themes` exposes both a JS and a CSS sourcemap and the package.json is in both. The technique is bundler-agnostic and matches whatever the production build actually shipped, which is the only version that matters when you are doing security or compatibility research.
Why this matters
Version precision matters for security research ('is this site running a CVE-vulnerable react-dom 18.0.0?', 'does the Stripe SDK on this checkout match the version in the latest advisory?'), for compatibility research ('does this Stripe integration use Elements v5 or v6?', 'is the Vite version old enough to need the recent path-traversal patch?'), for competitive analysis ('they're still on Next.js 12 — they haven't migrated to App Router'), and for authoritative technographic data ('what is the React 19 adoption rate among the Fortune 500 marketing sites we just audited').
A major-version-only detector gives you a ballpark; a package.json-derived version gives you the ground truth. The difference shows up most starkly in security workflows. Knowing a site uses 'React' tells you nothing about whether it is exposed to CVE-2024-XXXX in a specific React-DOM patch range; knowing it uses 'react-dom 18.2.0' lets you map directly against the advisory database. The same goes for client-side cryptography libraries, payment SDKs, image-processing utilities — any library whose security posture changes meaningfully between point releases.
For product and integration work the version detail is just as load-bearing. 'They are on @tanstack/query-core 4' is a different conversation from 'they are on @tanstack/query-core 5' because the API surface changed. 'They are on Next.js 13.4' is a different conversation from 'they are on Next.js 14.2' because App Router maturity is the deciding factor for whether your integration shape works. Treating libraries as undifferentiated names misses the version-level facts that actually drive technical decisions, and exact-version reading is the antidote.
Prerequisites
- Target site that exposes sourcemaps in production. Most public Next.js, Remix, Nuxt and Astro sites do by default; many enterprise apps do too because the build pipeline never disabled the option.
- Chrome DevTools or `curl` for direct fetches of the `.js` and `.map` files. Either works; DevTools is faster for a single file, curl is faster when you need to script across multiple URLs.
- A JSON parser — either `jq` from a terminal, your favorite language's stdlib, or just a browser-based JSON viewer. Sourcemaps are large (sometimes tens of megabytes) so a streaming or indexable viewer is more pleasant than scrolling through a flat editor.
- Optional: Sourcemap Explorer installed, which collapses every step in this guide into a single popup click and runs the extraction across every bundle on the page in parallel.
Step-by-step
- 1
Confirm the site exposes sourcemaps
Open DevTools, switch to the Network tab, filter by 'JS', and reload the page. Click any of the application's own script requests (skip vendor URLs from `cdn.googletagmanager.com` and similar third parties — those almost never expose maps). In the Headers panel of the request, look for `SourceMap:` or `X-SourceMap:` in the response headers. If either is present, the URL it points at is the sourcemap. If neither is present, fetch the last few kilobytes of the `.js` file (just `curl -s <url> | tail -c 4096`) and look for a `//# sourceMappingURL=` comment — many builds put the reference there instead of in a header. If both checks come up empty, the build deliberately disabled sourcemap output and exact version detection via this technique is not possible on this site.
curl -sI https://example.com/_next/static/chunks/main.js | grep -i sourcemap # or curl -s https://example.com/_next/static/chunks/main.js | tail -c 4096 | grep -o '//# sourceMappingURL=.*'
- 2
Download and inspect the map
Fetch the `.map` URL with `curl` or save-as in DevTools. The file is a JSON document with (at least) `version`, `sources[]`, `sourcesContent[]`, `mappings`, `names[]`, and a few other fields. The two arrays you care about are `sources[]` (the original file paths the bundler used) and `sourcesContent[]` (the literal contents of those files, in the same order). For exact-version reading you want every entry in `sources[]` whose path ends in `node_modules/<package>/package.json` — that is the canonical location the bundler resolved when it imported the package.
curl -s https://example.com/_next/static/chunks/main.js.map > main.js.map jq -r '.sources | to_entries[] | select(.value | test("node_modules/[^/]+/package.json$")) | "\(.key)\t\(.value)"' main.js.map - 3
Read the version out of sourcesContent
For each index returned by the previous step, the matching entry in `sourcesContent[]` is the package's `package.json` as a string. Parse it as JSON and read `name`, `version`, optionally `peerDependencies` and `engines`. That gives you the exact semver and the dependency contract the package shipped with. Repeat across every map on the site (the application might be split across a dozen chunk files, each with its own map and its own slice of `node_modules/`).
jq -r '. as $m | $m.sources | to_entries[] | select(.value | test("node_modules/react/package.json$")) | $m.sourcesContent[.key] | fromjson | "react@\(.version)"' main.js.mapTip: If `jq` complains that the entry is `null`, the bundler included the path in `sources[]` but stripped the body from `sourcesContent[]` (an optimization webpack does occasionally). The package is still present; you just cannot read the version this way and have to fall back to path-based detection.
- 4
Cross-reference with node_modules paths
Even without `package.json` as a source, `node_modules/<pkg>/<file>` paths in `sources[]` confirm the package is bundled. Run a second jq query that filters every entry under `node_modules/<pkg>/` and you have the file footprint of the package — useful when you want to reason about which exports the site is actually using. The version may not be extractable from paths alone, but distinctive file names sometimes give it away (an `esm/2024/...` directory in a Stripe build, for example, telegraphs which generation of the SDK is in use).
- 5
Use Sourcemap Explorer to scale across the whole site
The extension automates all of the above across every bundle on the page, extracts versions from every embedded `package.json`, and reports them on the Stack tab. It's the only path that scales to an entire site without writing custom code or fetching every map manually. Versions from sourcemap `package.json` are marked as authoritative; versions inferred from URLs are marked as inferred. Click into a row and you can see the exact `package.json` excerpt that produced the answer, plus a link back to the originating chunk and source path.
- 6
Verify against the npm registry
For an additional layer of confidence, query the npm registry directly: `curl -s https://registry.npmjs.org/react/<version> | jq '{version, dist}'` returns the exact semver record from npm's perspective. If the version Sourcemap Explorer reported matches a real npm release (which it should, almost always), you have ground truth. The few times it does not match are interesting — usually a mono-repo internal build that pinned a release-candidate version, or a customer-private package with an overlapping name.
Real-world example
Alternative methods
Read window globals
Some libraries expose a version on a runtime global — `React.version`, `Vue.version`, jQuery's `$.fn.jquery`, Stripe's `Stripe.version`. Open the Console and type the global. Works for the libraries that bother to expose it (less common in modern bundled apps where ESM imports do not pollute the global object). Sourcemap reading does not depend on the library cooperating, which is why it is the more general technique.
Inspect the chunk filename hash
Some bundlers include a content hash in the chunk filename, which lets you compare 'is this the same build as the one I last audited' across deploys. The hash does not give you a version directly, but it tells you when the build changed, which is sometimes what you actually need.
Check the npm registry for known fingerprints
If you find a distinctive function signature, error string or comment inside the bundle, you can grep the npm registry's published files for matches. NerdyData is the right shape of tool for that lookup. Tedious by hand; useful when sourcemaps are not available and you need to identify a heavily-minified library by its surface.
Troubleshooting
`node_modules/react/package.json` shows up but version is empty.
The bundler stripped the `version` field as an optimization. Very rare but possible (especially with custom esbuild plugins). Check other `package.json` files in the same sourcemap — they'll usually have versions, and React's version can be cross-checked via `React.version` in the console if the global is exposed.
I see the same package version listed twice.
Monorepo package hoisting can cause the same library to be bundled twice from two locations. Both are real; the UI deduplicates to the semver-max. If the two versions differ, that is a sign the site has a real duplicate-React or duplicate-zod situation worth flagging in any audit.
The map file is enormous and `jq` is slow.
Some production sourcemaps are 50-200 MB. `jq --stream` is dramatically faster for narrow filters; alternatively, Sourcemap Explorer streams the file in chunks instead of slurping it whole, which is why it can handle gigabyte-sized site dumps without choking.
I get a 404 on the .map URL referenced in the SourceMap header.
Some sites strip the `.map` files from production for size or IP-protection reasons but forget to remove the header. The header is informational; if the file is not there, your only path is path-based detection from chunk content.
I see versions that don't match anything on npm.
Almost always an internal monorepo package or a private fork. The `name` field will give it away (`@company/foo` rather than a public name). For public packages that legitimately do not exist on npm, double-check the version against the package's GitHub releases — sometimes pre-release tags get bundled in production builds.
Caveats
What to do next
With exact versions in hand, the typical follow-up is either security research (cross-referencing the versions against CVE databases, vendor advisories, the Snyk vulnerability database, the GitHub Advisory Database) or architecture research (correlating library choices across frameworks: 'every Next.js 14 site we audited paired App Router with TanStack Query 5 and shadcn/ui'). The [see-every-javascript-library-a-site-uses](/how-to/see-every-javascript-library-a-site-uses) guide expands from 'find one version' to 'list everything bundled', and [extract-package-json-from-a-sourcemap](/how-to/extract-package-json-from-a-sourcemap) goes deeper on the JSON-walking primitive. For stack-wide audits the right next step is usually the framework detection — knowing the React version is one thing, knowing it sits inside Next.js 14.2.4 with App Router and Server Actions enabled is much more actionable. [check-if-a-site-is-built-with-nextjs](/how-to/check-if-a-site-is-built-with-nextjs) and [detect-framework-of-any-website](/how-to/detect-framework-of-any-website) cover that. For comparing tools, the [Wappalyzer alternatives](/alternatives/wappalyzer) page walks through why exact-version reading is the thing browser-extension detectors structurally cannot do and why a sourcemap-aware tool changes the workflow.
FAQ
Why aren't most detectors doing this already?
Because they don't parse sourcemaps. They run on HTML/headers/DOM only. Adding sourcemap parsing is non-trivial — you need to fetch the `.map` (often tens of MB), JSON-parse it, walk `sources[]` for every package, and tolerate the various ways different bundlers structure the output. Sourcemap Explorer was designed specifically to do that, which is why it surfaces version detail Wappalyzer / BuiltWith / WhatRuns / SimilarTech all miss.
Can I do this in DevTools alone?
Yes, manually per map. Download the `.map`, open it in a JSON viewer or pipe it through `jq`, search for `package.json`. The technique is straightforward; the friction is volume — a single page might load 10-20 chunks, each with its own map. The extension automates the fan-out across every map on the page in parallel.
What about CSS-only packages?
`node_modules/tailwindcss/package.json` shows up in the CSS sourcemap on Tailwind-using sites, and `node_modules/postcss/package.json` shows up alongside it. Same extraction method works for any CSS-source-maps-enabled bundle. The extension reads both JS and CSS maps in the same pass.
Why would a site disable sourcemaps in production?
Three common reasons: (1) bundle size — sourcemaps add hundreds of KB to deploys; (2) IP protection — sourcemaps reconstruct most of the original source, which some companies want to hide; (3) old habits — some build pipelines disabled sourcemaps in 2017 for size reasons and never re-enabled them. Modern frameworks default to keeping them on because the developer-experience and observability wins outweigh the costs.
Can the version inside the sourcemap be wrong?
Almost never. The bundler reads `node_modules/<pkg>/package.json` from disk during the build and writes it verbatim into the map. The only failure modes are deliberate stripping (the version field is removed as an optimization) or genuine error (a custom plugin rewrites the file). If the version is present, it is what the build actually used.
Does this work on minified production bundles?
Yes. The minification happens in the JavaScript output; the sourcemap stays unminified and carries the original `node_modules/<pkg>/package.json` contents intact. That is the entire point of sourcemaps — to map the minified output back to the original sources, including the package metadata.
How do I cross-check against the npm registry?
`curl -s https://registry.npmjs.org/<package>/<version> | jq` returns the canonical npm record for that exact version. If the version Sourcemap Explorer reported exists on npm, you have a published-package match; if not, you are looking at a private build, a release-candidate, or a renamed fork.
Related
Skip the manual steps.
Sourcemap Explorer automates every workflow in this guide — free, local, no sign-up.