<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>LoupeKit - notes: builds</title>
    <link>https://loupekit.com/blog/</link>
    <atom:link href="https://loupekit.com/blog/builds/rss.xml" rel="self" type="application/rss+xml" />
    <description>Notes on builds, from the blog that https://loupekit.com/blog/rss.xml carries in full.</description>
    <language>en</language>
    <lastBuildDate>Tue, 15 Sep 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>What a source map says about the build that shipped it</title>
      <link>https://loupekit.com/blog/source-maps-in-production/</link>
      <guid isPermaLink="true">https://loupekit.com/blog/source-maps-in-production/</guid>
      <pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate>
      <category>builds</category>
      <dc:creator>Ján Turský</dc:creator>
      <description>A source map is not a leak by itself. It is a decision, and on most sites nobody made it - the bundler defaulted, the flag was never read, and the original source went out with the minified file. (builds, 5 min)</description>
      <content:encoded><![CDATA[<p><strong>A source map is not a leak by itself. It is a <strong>decision</strong>, and on most sites nobody made it - the bundler defaulted, the flag was never read, and the original source went out with the minified file.</strong></p><p>Minified JavaScript is unreadable by design. A source map is the index that reverses it: a JSON file mapping every position in the shipped bundle back to a position in a file that was never shipped - and, optionally, <mark class="bg-accent-light text-text">carrying the contents of those files inside itself</mark>.</p><p>That optional part is the whole subject. A map without it is a set of names and offsets. A map with it is your repository, served as a static asset.</p><h2>The anatomy, and the field that matters</h2><p><em>a source map, with the interesting fields kept</em></p><pre><code>{
  &quot;version&quot;: 3,
  &quot;file&quot;: &quot;main.a1b2c3.js&quot;,
  &quot;sources&quot;: [
    &quot;../src/lib/billing/stripeWebhook.ts&quot;,
    &quot;../src/lib/internal/featureFlags.ts&quot;,
    &quot;../../packages/admin-shared/src/roles.ts&quot;
  ],
  &quot;sourcesContent&quot;: [ &quot;import Stripe from …&quot;, … ],
  &quot;names&quot;: [&quot;chargeCustomer&quot;, &quot;isStaffOverride&quot;],
  &quot;mappings&quot;: &quot;AAAA,SAASA,…&quot;
}</code></pre><dl><dt>sources</dt><dd>The original paths, relative. Names your folder structure, your internal package names, and often the shape of a monorepo you have never published.</dd><dt>sourcesContent</dt><dd><strong>The files themselves.</strong> Comments, TODOs, commented-out code, the variable names you picked. This is the field that turns a map into a publication.</dd><dt>names</dt><dd>Original identifiers. Enough to reconstruct intent even when <code class="font-mono text-[0.92em]">sourcesContent</code> is absent.</dd><dt>mappings</dt><dd>The offsets. Useless alone, and the only part strictly required to symbolicate a stack trace.</dd></dl><blockquote><p><strong>A map is not automatically a vulnerability.</strong> It exposes no secret a bundle did not already contain - an API key in client code was public the moment it was bundled. What it exposes is <em>structure and intent</em>, which is a disclosure decision rather than a security bug, and it deserves to be made rather than defaulted.</p></blockquote><h2>Checking what a site actually publishes</h2><p>Three states, and they are commonly confused with each other:</p><ol><li><strong>No map at all.</strong> No <code class="font-mono text-[0.92em]">sourceMappingURL</code> comment, nothing to fetch.</li><li><strong>A comment pointing at a map that 404s.</strong> Generated, then not deployed - or deployed and later removed. Common, and the one people mistake for the first.</li><li><strong>A comment pointing at a map that serves.</strong> Whether that matters depends entirely on <code class="font-mono text-[0.92em]">sourcesContent</code>.</li></ol><p><em>which of the three, for every script on a page</em></p><pre><code>for (const s of document.scripts) {
  if (!s.src) continue;
  const body = await (await fetch(s.src)).text();
  const ref = body.match(/sourceMappingURL=(\S+)/)?.[1];
  if (!ref) continue;

  const map = new URL(ref, s.src).href;
  const res = await fetch(map);
  console.log(s.src, res.status, res.ok
    ? ((await res.json()).sourcesContent ? 'full source' : 'names only')
    : 'referenced, not served');
}</code></pre><p>Worth running on your own build after a config change. <strong>A bundler upgrade is the usual way this flips</strong>, in either direction, and nothing in the deploy output mentions it.</p><h2>The three configurations that are defensible</h2><dl><dt>Ship nothing</dt><dd>No map, no comment. Stack traces from production are unreadable and you accept that. Honest, and increasingly rare because error monitoring made the alternative cheap.</dd><dt>Build it, upload it, delete it</dt><dd>The one most teams want. The map is generated, sent to the error tracker at deploy time, then <strong>removed from the output directory</strong> so it is never served. Traces symbolicate; nothing is public.</dd><dt>Ship it deliberately</dt><dd>A defensible choice for an open codebase, a developer tool, or a team that considers its frontend source uninteresting. The point is that somebody decided.</dd></dl><p><em>the middle option, which is the one to copy</em></p><pre><code>// vite.config.ts
export default { build: { sourcemap: true } };

// deploy step, in this order
// 1. build
// 2. upload dist/**/*.map to the error tracker
// 3. rm dist/**/*.map

// Deleting before uploading is the mistake to avoid,
// and it fails silently: traces stay minified and
// nothing in the pipeline reports a problem.</code></pre><blockquote><p>If you take the third option, take it <strong>completely</strong>. A half-published map - sources listed, contents stripped - gives an attacker your architecture and gives your own team nothing. <mark class="bg-accent-light text-text">It is the only configuration with no upside.</mark></p></blockquote><h2>What it tells you about somebody else's build</h2><p>Read from the outside, a map is one of the highest-signal artefacts a site publishes - not because of the source, but because of the <code class="font-mono text-[0.92em]">sources</code> array:</p><ul><li><strong>The framework and its version</strong>, from the paths inside <code class="font-mono text-[0.92em]">node_modules</code> that survived bundling.</li><li><strong>The shape of the team.</strong> <code class="font-mono text-[0.92em]">packages/admin-shared</code> in a public bundle says there is an admin surface and that it shares code with this one.</li><li><strong>What was tree-shaken and what was not.</strong> A dependency in <code class="font-mono text-[0.92em]">sources</code> that no feature on the page uses is dead weight somebody is still shipping.</li><li><strong>Whether the build is what the site says it is.</strong> A page advertising one framework, mapping back to files from another, is a migration nobody finished.</li></ul><p>None of that needs the source contents, and all of it survives <code class="font-mono text-[0.92em]">sourcesContent</code> being stripped. Which is the argument for the middle option one more time: <strong>the map you did not serve tells nobody anything.</strong></p>]]></content:encoded>
    </item>
    <item>
      <title>What a page tells you about how it was built</title>
      <link>https://loupekit.com/blog/reading-a-stack/</link>
      <guid isPermaLink="true">https://loupekit.com/blog/reading-a-stack/</guid>
      <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
      <category>builds</category>
      <dc:creator>Ján Turský</dc:creator>
      <description>Every page announces its own construction, in six different ways, with six different levels of honesty. Knowing which kind of evidence you are looking at matters more than the list of names it produces. (builds, 5 min)</description>
      <content:encoded><![CDATA[<p><strong>Every page announces its own construction, in six different ways, with six different levels of honesty. Knowing which kind of evidence you are looking at matters more than the list of names it produces.</strong></p><p>Stack detection looks like pattern matching and mostly is. What separates a useful answer from a confident wrong one is whether the tool tells you <em>how</em> it knew - because the six kinds of evidence a page leaves are <strong>not equally reliable</strong>, and two of them are trivially faked.</p><h2>The six, weakest last</h2><dl><dt>A DOM marker</dt><dd>An attribute or class the framework itself writes - <code class="font-mono text-[0.92em]">data-reactroot</code> and its successors, a hydration marker, a generated class prefix. <strong>Strong</strong>: it is a side effect of the thing running, not a declaration about it.</dd><dt>A page global</dt><dd>A variable the library leaves on <code class="font-mono text-[0.92em]">window</code>. Equally strong, and often carries a version the page did not mean to publish.</dd><dt>A namespaced CSS variable</dt><dd>A custom property whose prefix belongs to one design system. Quietly one of the best signals, because it <mark class="bg-accent-light text-text">survives the minification that erases the others</mark>.</dd><dt>An asset URL</dt><dd>A script or stylesheet whose path names the thing. Good, with a caveat: a bundled copy under a hashed name says nothing, so absence here is not evidence of absence anywhere.</dd><dt>A response header</dt><dd>The server naming its own software. Reliable when present, and routinely removed, rewritten or set to something untrue as a matter of policy.</dd><dt>The generator meta tag</dt><dd>A declaration by the page about itself. <strong>The weakest of the six</strong>: a string somebody typed, surviving long after the thing it names was replaced, and the first thing edited by anyone who does not want to be identified.</dd></dl><p><em>The same six kinds, weighted by how hard each is to fake. The top three are side effects of code running; the bottom one is a sentence in the markup. - <a href="https://loupekit.com/blog/reading-a-stack/">runs on the page itself</a>.</em></p><blockquote><p>The ordering is the useful part. Two tools can report the same name from the top of that list and from the bottom of it, and <mark class="bg-accent-light text-text">those are not the same finding</mark>.</p></blockquote><p><em>the same claim, at both ends of the list</em></p><pre><code>&lt;meta name=&quot;generator&quot; content=&quot;WordPress 5.2&quot;&gt;

&lt;link rel=&quot;stylesheet&quot;
      href=&quot;/wp-content/themes/twentytwenty/style.css?ver=6.5&quot;&gt;

&lt;!-- The first is a string in a template, unchanged since 2019.
     The second is a file the server is really serving,
     and the two disagree by four major versions. --&gt;</code></pre><h2>Why the evidence has to be shown</h2><p><mark class="bg-accent-light text-text">A flat list of names is not checkable.</mark> Told that a page is running a particular framework, you have no way to know whether that came from a hydration marker or from a meta tag somebody left in a template five years ago - and those two facts deserve different amounts of belief.</p><p>So every match here comes with the literal thing that matched and where it was found. It makes the readout longer and it is the difference between a claim and a finding.</p><p>It also makes the detector correctable. When a match is wrong, the evidence says which rule to fix rather than leaving somebody to guess at a heuristic they cannot see.</p><h2>Absence is not evidence</h2><p>The failure that produces confidently wrong readouts is not a bad match. It is treating a missing signal as a negative finding - and on a modern build, <mark class="bg-accent-light text-text">missing is the normal case</mark>.</p><dl><dt>Bundling</dt><dd>A library compiled into <code class="font-mono text-[0.92em]">main.a1b2c3.js</code> leaves no filename to match. It is running; the URL says nothing.</dd><dt>Minification</dt><dd>Class prefixes and global names are renamed. A framework can be present with every one of its usual markers gone.</dd><dt>Server rendering</dt><dd>Markup arrives fully formed. The runtime that produced it may never have reached the browser at all.</dd><dt>A stripped header</dt><dd>Removed as a matter of policy at more or less every serious host. Its absence is a security setting, not a fact about the stack.</dd></dl><p>So the honest output has two states rather than three: <strong>identified, with the evidence</strong> and <strong>not identified</strong>. There is no <em>not present</em>, and a tool offering one is telling you something the page did not say.</p><h2>Versions, and what a version is worth</h2><p>Where a page states a version, it is read and reported. Where it does not, <strong>nothing is invented</strong> - a guessed version is worse than a missing one, because the missing one prompts a question and the guessed one ends it.</p><p>Some versions cannot be told apart from the outside at all. Two major releases of the same tool can declare identical variable names, and a detector claiming to distinguish them is claiming something the page did not say.</p><h2>Detecting yourself is the useful direction</h2><p>The instinct is to point this at somebody else's site. The finding that changes a decision is usually on your own, because a stack list from the outside is <strong>a list of what actually reached a browser</strong> - not what the lockfile says, not what the bundler was configured to split, not what a colleague removed six months ago.</p><ul><li>A library everyone believes was dropped, still shipping in a chunk nothing imports any more.</li><li>Two versions of the same framework, because a widget brought its own.</li><li>A tag manager loading a second analytics vendor that no one on the team chose.</li><li>A consent banner that itself loads three third parties <strong>before</strong> consent is given, which is the most common finding of all and the most expensive one.</li></ul><blockquote><p>That last one is worth checking on any site with a cookie banner. <mark class="bg-accent-light text-text">It is a compliance failure that is invisible from the inside</mark> and takes about four seconds to see from the outside.</p></blockquote><h2>What to do with the answer</h2><p>The immediate use is orientation: opening an unfamiliar codebase's <em>output</em> before opening its source tells you what you are about to read.</p><p>The slower use is the third-party list. Most pages carry more analytics, tag managers, session replay and consent tooling than anybody on the team believes, because each was added once by somebody with a good reason. Seeing them named together, with the evidence for each, is usually the most uncomfortable part of the readout - and the most useful.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
