A buildless app still has a build step: 41 pinned URLs and a sed pass that missed two
One advisory turned into a repo-wide find-and-replace that quietly skipped two subpath imports. Import maps put resolution back in one place the browser will actually honour.
The Engineer · Build desk

What happened
- A grep for one pinned esm.sh lodash-es URL under src/ came back with 41 files, each carrying the version as a hand-typed string.
- The advisory bump shipped as a sed pass across the repo, which silently skipped two files importing a named subpath with a different query string.
- Browsers reject bare specifiers outright, throwing a TypeError on "lodash", which is why the full CDN URLs were pasted into every module in the first place.
- Pointing that map at an external JSON file via src works in Chrome only; Firefox and Safari accept the inline block.
Why it matters
- exposure A partial rewrite leaves no artifact recording which import sites were patched, so verifying the fix falls back on grep and whoever remembers the query string that broke it.
- capability Running two pinned versions of the same package on different URL paths stops being a reason to adopt a bundler.
- decision Dependency versions move into the document head, so a bump becomes an HTML deployment owned by whoever controls the templates rather than a change the module authors can make.
- constraint With no cross-engine story for combining maps, mappings cannot be contributed per component or per team; one person has to plan the whole block up front.
The sed pass failed in the way that never wakes anyone up. A pinned CDN URL that still resolves is indistinguishable to the loader from the one you meant to write, so the two stragglers kept fetching the advisory-affected build, according to the dev.to account [14], and the page kept working. Two misses out of forty-one sites is under five per cent of the import statements in that repo [16], small enough to clear review and precisely the thing the bump existed to remove [13].
Nothing in the browser was positioned to catch it. The module spec allows a browser to resolve a full URL or a specifier beginning with /, ./ or ../, and nothing else [4]. A bare name like "lodash" throws before a request is made [5]. Filling that gap has always taken either a bundler rewriting specifiers at build time or a resolution algorithm of the kind Node has and browsers do not, since a page has no node_modules to walk [6].
An import map is the third route, and the only one the browser honours by itself: a JSON block stating what a bare specifier resolves to, consulted by every import statement on the page, which puts the version in one location and turns a bump into a one-line diff [1]. It also has its own version of the subpath blind spot. A key ending in a slash is a prefix mapping, and the value has to end in one too or the browser refuses to register the mapping at all [7]. Leave the slash off the key and you have mapped the exact string "lodash-es" and nothing more, so every "lodash-es/debounce" in the codebase is back to being an unresolvable bare specifier [8]. That is the same class of error as the sed miss, relocated to one file where it fails on first load instead of hiding in two modules nobody opened.
The scoped form is keyed by the path of the importing module and is checked ahead of the top-level imports, so modules under /admin/ can hold at 4.17.19 while the rest of the app moves to 4.17.22 [9]; where a specifier matches more than one scope path, the longest wins [10]. Then count the copies again. One map per document [2] and inline only outside Chrome [3] means 41 per-module string literals become one block per HTML entry point [15]. That is a genuine reduction if your server renders the head from a template, and close to a lateral move if your pages are static and hand-edited. The block also has to appear above the first module script that depends on it, or it is JSON that does nothing [11].
What to watch
- Whether the spec's merging story for multiple import maps lands in shipping engines, which would let components contribute mappings instead of one owner writing the whole block.
- Firefox and Safari support for the src attribute, which is the difference between one JSON file per site and one inline block per page.
- Whether server-side templating and static site tooling start emitting the inline map automatically, so the per-page copies stay in sync without another find-and-replace.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence54
- Adoption
- Insufficient
- Hype gap+8
- Incentives34
- Confidence52
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
An import map is a small JSON block telling the browser what URL a bare specifier resolves to; one declaration covers every import statement in every module on the page, so the version lives in one place and bumping it is a one-line diff rather than a repo-wide search.
- [2]
Current browsers allow one import map per page: a second <script type="importmap"> is ignored with a console warning, and the spec's story on merging multiple import maps is still catching up across engines.
- [3]
The src attribute pointing an import map at an external JSON file is a Chrome-only shortcut today; Firefox and Safari accept only the inline form.
- [4]
The ES module spec lets a browser resolve only three kinds of specifier on its own: a full URL, or a path starting with /, ./ or ../.
- [5]
A bare specifier such as "lodash" in a plain <script type="module"> produces: Uncaught TypeError: Failed to resolve module specifier "lodash".
- [6]
Bare specifiers only mean something if a bundler rewrites them at build time or a package manager resolution algorithm of the kind Node has fills in the blank; a browser has no node_modules to walk.
- [7]
A key ending in / is a prefix mapping, and the mapped value must also end in / or the browser refuses to register the mapping at all.
- [8]
Mapping "lodash-es" without a trailing slash maps only the exact specifier "lodash-es", not "lodash-es/debounce" or other subpaths.
- [9]
The scopes field is a fallback map keyed by the path of the importing module and checked before the top-level imports, so modules under /admin/ can resolve lodash-es to 4.17.19 while the rest of the app resolves it to 4.17.22.
- [10]
If a specifier matches more than one scope path, the browser picks the longest, most specific one.
- [11]
The import map must appear before any module script that relies on it; placed late in <head> after a module script has begun resolving imports, it does nothing.
- [12]
Running grep -rl "esm.sh/lodash-es@4.17.21" src/ returned forty-one files, each with the same pinned CDN URL typed out by hand at the top of a module.
- [13]
A security advisory called for bumping lodash-es from 4.17.21 to 4.17.22.
- [14]
A sed one-liner run across the repo silently skipped the two files that imported a named subpath with a slightly different query string.
- [15]
Because a document gets one import map and non-Chrome engines accept only the inline form, the 41 per-module version strings collapse to one JSON block per HTML entry point rather than one per repository.
- [16]
The sed pass rewrote 39 of the 41 import sites and left 2, just under 5 per cent of them, on the old pin.
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toOne version bump, 40 files to edit. Import maps fix that.
1 article · August 22, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.