<?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/">
  <channel>
    <title>DEV Community: Tamer Kalla</title>
    <description>The latest articles on DEV Community by Tamer Kalla (@tamerkalla).</description>
    <link>https://dev.to/tamerkalla</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4111542%2F10e142fd-ab57-47bc-be6e-5aa1eb1e1403.png</url>
      <title>DEV Community: Tamer Kalla</title>
      <link>https://dev.to/tamerkalla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tamerkalla"/>
    <language>en</language>
    <item>
      <title>The same glob pattern means three different things in one repository</title>
      <dc:creator>Tamer Kalla</dc:creator>
      <pubDate>Sun, 06 Sep 2026 16:55:47 +0000</pubDate>
      <link>https://dev.to/tamerkalla/the-same-glob-pattern-means-three-different-things-in-one-repository-3hda</link>
      <guid>https://dev.to/tamerkalla/the-same-glob-pattern-means-three-different-things-in-one-repository-3hda</guid>
      <description>&lt;p&gt;I spent twenty minutes last week convinced my tsconfig was broken.&lt;/p&gt;

&lt;p&gt;The pattern was &lt;code&gt;src/**&lt;/code&gt;. It works in a &lt;code&gt;.gitignore&lt;/code&gt;. It works in every glob library I have ever installed. In &lt;code&gt;tsconfig.json&lt;/code&gt; it does nothing at all, and the reason turned out to be that TypeScript rejects it outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File specification cannot end in a recursive directory wildcard ('**'): 'src/**'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not "matched nothing". Rejected. The diagnostic is right there in the output if you go looking, and I had not gone looking, because it had never occurred to me that a glob could be a syntax error in one file and completely ordinary in the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A glob is not one language
&lt;/h2&gt;

&lt;p&gt;Count the places you write a glob pattern in a normal week. I get six without trying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsconfig.json&lt;/code&gt;, in &lt;code&gt;include&lt;/code&gt; and &lt;code&gt;exclude&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an ESLint config, which is minimatch underneath&lt;/li&gt;
&lt;li&gt;a Vite or rollup config, which is picomatch&lt;/li&gt;
&lt;li&gt;lint-staged, which is micromatch&lt;/li&gt;
&lt;li&gt;Node itself, &lt;code&gt;path.matchesGlob&lt;/code&gt; and &lt;code&gt;fs.glob&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are six dialects that share a syntax and disagree about what it means. Nothing warns you when you cross from one to another, because each tool answers only for itself, and none of them has any reason to tell you that the other five read your pattern differently.&lt;/p&gt;

&lt;p&gt;So I took 25 patterns straight out of config files I actually had lying around, 20 ordinary paths, and asked all six engines every combination. 500 questions, no sampling, same answers on a re-run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;19 of the 25 patterns&lt;/strong&gt; are not read the same way by all six.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 of them&lt;/strong&gt; are refused outright by one engine and accepted by the other five. Every one of those five ends in &lt;code&gt;**&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Of the &lt;strong&gt;400&lt;/strong&gt; pattern and path pairs drawn from the patterns that every engine accepts, &lt;strong&gt;48&lt;/strong&gt; come back with different answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 patterns&lt;/strong&gt; are read identically by all six.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six out of twenty-five. If you have ever written a pattern meant to mean the same thing in your &lt;code&gt;.gitignore&lt;/code&gt; and your &lt;code&gt;tsconfig&lt;/code&gt; and your bundler config, you had roughly a one in four chance of getting one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the disagreements actually live
&lt;/h2&gt;

&lt;p&gt;Two rules cause most of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dotfiles.&lt;/strong&gt; &lt;code&gt;src/**&lt;/code&gt; matches &lt;code&gt;src/.hidden/file.ts&lt;/code&gt; under git. It does not match under minimatch, picomatch, micromatch or Node, because those four skip dotfiles unless you ask for them and git does not. This is the one that bites, because the file you were trying to ignore is usually the dotfile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Braces.&lt;/strong&gt; &lt;code&gt;**/*.{ts,tsx}&lt;/code&gt; expands in the three npm matchers and in Node. git does not expand braces at all. Neither does tsconfig. So a pattern that is unremarkable in a Vite config quietly matches nothing in a &lt;code&gt;.gitignore&lt;/code&gt;, and nothing anywhere tells you.&lt;/p&gt;

&lt;p&gt;Put those together on one line and you get the case that started this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/** vs src/.hidden/file.ts

  git          match
  micromatch   no match
  minimatch    no match
  node         no match
  picomatch    no match
  typescript   rejected: cannot end in a recursive directory wildcard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different answers. One pattern, one path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking all six at once
&lt;/h2&gt;

&lt;p&gt;That output is a command, not a screenshot. I packaged the harness up so it is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx glob-consensus &lt;span class="s1"&gt;'src/**'&lt;/span&gt; src/.hidden/file.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits &lt;code&gt;0&lt;/code&gt; when every engine agreed and &lt;code&gt;1&lt;/code&gt; when they did not, so it works in a script. There is a library underneath it if you want the report as data.&lt;/p&gt;

&lt;p&gt;It implements no matching logic of its own, deliberately. Every verdict comes from an engine that already exists and that somebody already maintains, which means the answer to "is this correct" is never a matter of my opinion. Two of the six answer about a filesystem rather than about a string, so those two get asked by building a temporary tree of empty files and throwing it away afterwards.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing before you read the output: tsconfig filters by file extension as well as by pattern, so a &lt;code&gt;.log&lt;/code&gt; file is never included by a tsconfig no matter what you write. That is tsconfig being tsconfig rather than a glob dialect difference, and the report shows it as it is rather than trying to be clever about it.&lt;/p&gt;

&lt;p&gt;This is the second time I have built a thing whose whole job is to ask several implementations the same question and print where they disagree. The first was &lt;code&gt;schema-parity&lt;/code&gt;, which does it for JSON Schema validators, and the shape keeps being useful for the same reason: the interesting bug is almost never inside one implementation, it is in the gap between two of them that everybody assumed were the same.&lt;/p&gt;

&lt;p&gt;It does not tell you which engine is right, because there is no right. They are all correct about themselves. It just stops you spending twenty minutes blaming your tsconfig.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/glob-consensus" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/glob-consensus&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A source map is a claim, and nothing checks whether it is true</title>
      <dc:creator>Tamer Kalla</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:15:41 +0000</pubDate>
      <link>https://dev.to/tamerkalla/a-source-map-is-a-claim-and-nothing-checks-whether-it-is-true-9f0</link>
      <guid>https://dev.to/tamerkalla/a-source-map-is-a-claim-and-nothing-checks-whether-it-is-true-9f0</guid>
      <description>&lt;p&gt;A source map is a claim about two files: that this position in the built output came from that position in a source file. I had never thought of it as a claim that could be false until a stack trace sent me to a line that had nothing to do with the crash.&lt;/p&gt;

&lt;p&gt;The build had gained a step. Something small, a licence banner prepended at the end of the pipeline, after the bundler had already written the map. The bundler was right. The banner was right. The map was now a careful description of a file that no longer existed.&lt;/p&gt;

&lt;p&gt;What made it expensive was that nothing anywhere said so. A broken map is still well formed JSON with all the right keys. It parses. It loads. The debugger opens it and shows you a file and a line, with exactly the same confidence it shows you a correct one. There is no error, no warning, no degraded mode. You just get an answer that is wrong, and you spend an afternoon doubting your own reading of the code.&lt;/p&gt;

&lt;p&gt;So I went looking for a way to ask a map whether it was still true, and found that three things can be checked without knowing anything at all about the transform that produced it.&lt;/p&gt;

&lt;p&gt;A mapping never points past the end of a line. If a map says position 1:26 and line 1 is eighteen characters long, it is describing text that is not there.&lt;/p&gt;

&lt;p&gt;A mapping never lands strictly inside an identifier. Generators emit positions at token boundaries, because the two halves of a name are not separately addressable in either file.&lt;/p&gt;

&lt;p&gt;And when a mapping carries a &lt;code&gt;name&lt;/code&gt;, the original source at that position begins with that name. The map is asserting an identity. Either the identifier is there or the map is wrong, and there is no third answer.&lt;/p&gt;

&lt;p&gt;The first thing I did was run those three checks over output I had no reason to doubt, because a checker that flags everything passes every soundness test ever written and is worth nothing. Across bundles from esbuild and files from tsc: 371 mappings, of which 26 carried a name, and not one violation. That number is the reason I trusted anything that came after it.&lt;/p&gt;

&lt;p&gt;Then I took the same bundle and the same map, and prepended three lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx sourcemap-truth &lt;span class="nt"&gt;--demo&lt;/span&gt;
&lt;span class="go"&gt;
dist/bundle.min.js   93 mappings, 2 sources

  generated side    5 of 93 land on a token,   88 do not
  original side    93 of 93 land on a token,    0 do not
  names            26 of 26 are at the position the map gives

  1:6    inside an identifier
  1:22   past the end of that line
  1:26   past the end of that line

  The map is not true of this file. Something changed dist/bundle.min.js
  after the map was written.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eighty-eight of ninety-three. Nothing about the file on disk hints at that, and the debugger will not mention it.&lt;/p&gt;

&lt;p&gt;The shape of the failure tells you where to look, which turned out to be the part I use most. In the run above the original side is perfect and the generated side collapsed, which means the output moved after the map was written. Shift the sources instead and it inverts: every position still checks out and all 26 names point at the wrong identifier. Those are different bugs in different steps, and you can tell which one you have before you start bisecting the pipeline.&lt;/p&gt;

&lt;p&gt;There is one thing it deliberately does not do, and it took a wrong turn to get there. My first version counted a mapping that landed on whitespace as a fault. That felt reasonable until I ran it against tsc, which deliberately maps the space after a keyword: 14 times in 122 mappings on one small file. Correct output scored 108 of 122. A checker that calls the compiler wrong is not measuring the compiler, it is measuring itself, so whitespace targets are not faults and the tool says nothing about them.&lt;/p&gt;

&lt;p&gt;The other thing it refuses to do is guess. A map whose sources carry no content cannot be shown true or untrue, and the report says unchecked rather than passing, with its own exit code. Keeping "this disagreed" apart from "nobody could tell" is the single decision I would keep if I had to throw the rest away. Collapsing them is how a checker ends up certifying everything it is handed.&lt;/p&gt;

&lt;p&gt;It is one function if you want it in CI, and one command if you just want to know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx sourcemap-truth dist/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero for a map that is true, one for a map that is not, two for one nobody could check.&lt;/p&gt;

&lt;p&gt;The same question turns up in a lot of places once you start looking for it. It is roughly how I ended up writing &lt;code&gt;schema-parity&lt;/code&gt; a while back, for whether a generated JSON Schema still accepts what the validator it came from accepts. Different artifacts, same shape of doubt: two things that are supposed to describe each other, and nothing that ever checks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/sourcemap-truth" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/sourcemap-truth&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>debugging</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
