<rss version="2.0">
  <channel>
    <title>Deno on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/deno/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sat, 12 Sep 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>The Registry Rewrote My Import and My Catch Block Covered For It</title>
      <link>https://llbbl.blog/2026/09/12/the-registry-rewrote-my-import.html</link>
      <pubDate>Sat, 12 Sep 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/09/12/the-registry-rewrote-my-import.html</guid>
      <description>&lt;p&gt;Users installing my logger from JSR were getting this at runtime:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[logan-logger] Winston not found, falling back to console logging:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
&amp;#39;/path/to/node_modules/.pnpm/@jsr+logan__logger@1.1.x/node_modules/@jsr/logan__logger/src/runtime/winston&amp;#39;
imported from .../@jsr/logan__logger/src/runtime/node.js
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Winston was installed. It was right there in their &lt;code&gt;dependencies&lt;/code&gt;. The message was wrong, and it was my message, printed by my code, from a catch block I wrote.&lt;/p&gt;
&lt;p&gt;Look at the path it failed on. &lt;code&gt;src/runtime/winston&lt;/code&gt;. Not &lt;code&gt;node_modules/winston&lt;/code&gt;. It was looking for a &lt;strong&gt;file next to my own source&lt;/strong&gt;, one directory deep in my package.&lt;/p&gt;
&lt;h2 id=&#34;the-source-i-published-isnt-the-source-i-wrote&#34;&gt;The source I published isn&amp;rsquo;t the source I wrote&lt;/h2&gt;
&lt;p&gt;What I wrote was ordinary:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;winston&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A bare specifier, resolved by the package manager the way every bare specifier has been since forever. What ended up in the published tarball was this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;./winston&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I confirmed it by pulling the actual artifact from &lt;code&gt;https://npm.jsr.io/~/11/@jsr/logan__logger/1.1.16.tgz&lt;/code&gt; and reading &lt;code&gt;src/runtime/node.ts&lt;/code&gt; inside it. The registry changed my code between &lt;code&gt;deno publish&lt;/code&gt; and the file on disk in a user&amp;rsquo;s &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The reason is that JSR statically analyzes ESM imports at publish time. Bare specifiers have to be explicitly mapped to a known specifier, something like &lt;code&gt;npm:winston&lt;/code&gt;, in your &lt;code&gt;jsr.json&lt;/code&gt; or &lt;code&gt;deno.json&lt;/code&gt; imports map. Winston wasn&amp;rsquo;t in mine, because it was an &lt;em&gt;optional&lt;/em&gt; peer dependency. The whole point was that it might not be there. So the analyzer, finding a bare specifier it couldn&amp;rsquo;t resolve to anything it knew about, treated it as a relative path and normalized it to &lt;code&gt;./winston&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Node then did exactly what it should: looked for &lt;code&gt;./winston&lt;/code&gt; relative to the module, found nothing, and threw &lt;code&gt;ERR_MODULE_NOT_FOUND&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-catch-block-is-the-actual-villain&#34;&gt;The catch block is the actual villain&lt;/h2&gt;
&lt;p&gt;The import was wrapped in a try/catch, and that catch existed for a legitimate reason. Winston was optional. If it wasn&amp;rsquo;t installed, falling back to console logging was correct behavior.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;winston&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;createWinstonLogger&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;warn&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;[logan-logger] Winston not found, falling back to console logging:&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That catch cannot tell the difference between &amp;ldquo;the user didn&amp;rsquo;t install winston&amp;rdquo; and &amp;ldquo;the registry mangled my import statement.&amp;rdquo; Both arrive as &lt;code&gt;ERR_MODULE_NOT_FOUND&lt;/code&gt;. So a packaging defect got laundered into a routine, expected, entirely normal-looking warning.&lt;/p&gt;
&lt;p&gt;The consequence for users wasn&amp;rsquo;t a crash. It was worse. They silently lost file transports and production JSON formatting, and got console output instead, while a message on stderr confidently told them the cause was a missing dependency they&amp;rsquo;d already installed. If you&amp;rsquo;re going to debug that, you have to distrust your own error message first.&lt;/p&gt;
&lt;p&gt;The npm build was fine the whole time, because Vite externalizes &lt;code&gt;winston&lt;/code&gt; correctly during the bundler build. Only the JSR distribution was broken. One library, two registries, two different published sources, one of which nobody was checking.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a false fix in the history I should own. A commit from 2025-12-03 titled &amp;ldquo;handle optional Winston dependency with TypeScript ignore&amp;rdquo; added &lt;code&gt;// @ts-ignore&lt;/code&gt; comments around this code. It silenced a type complaint and touched nothing about the import statement, so it did nothing for the actual bug while looking, in the log, like the bug had been addressed.&lt;/p&gt;
&lt;h2 id=&#34;the-fix-im-not-proud-of&#34;&gt;The fix I&amp;rsquo;m not proud of&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Dynamic specifier prevents JSR from rewriting the bare &amp;#39;winston&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// import to a relative &amp;#39;./winston&amp;#39; path during publish.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winstonModule&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;winston&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;winstonModule&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assign the string to a variable first. The static analyzer can&amp;rsquo;t follow it, so it leaves the import alone, and at runtime the value is identical.&lt;/p&gt;
&lt;p&gt;This works. I shipped it. It&amp;rsquo;s also a workaround that operates by &lt;strong&gt;deliberately defeating static analysis&lt;/strong&gt;, which is the same capability the tooling is trying to give you. The build systems are all moving toward being able to see your dependency graph, and my fix was to hide from them. &lt;code&gt;deno publish --dry-run&lt;/code&gt; will even warn you about it, &lt;code&gt;unanalyzable-dynamic-import&lt;/code&gt;, which in this case was the goal.&lt;/p&gt;
&lt;p&gt;The alternative was declaring &lt;code&gt;winston&lt;/code&gt; as an &lt;code&gt;npm:&lt;/code&gt; specifier in &lt;code&gt;deno.json&lt;/code&gt;, which pins JSR consumers to a specific npm package and breaks the optional-peer semantics the dependency existed to provide. I picked the hack.&lt;/p&gt;
&lt;h2 id=&#34;then-i-deleted-the-dependency&#34;&gt;Then I deleted the dependency&lt;/h2&gt;
&lt;p&gt;That wasn&amp;rsquo;t a reaction to the workaround being ugly, the 2.0 work was already queued. But writing that comment is what made the cost legible. I had a peer dependency that couldn&amp;rsquo;t be declared, published through a pipeline that rewrote it, hidden behind a catch block that misreported it, patched with a trick that lied to the analyzer.&lt;/p&gt;
&lt;p&gt;The numbers on the way out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pnpm-lock.yaml&lt;/code&gt; lost &lt;strong&gt;205 lines&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/runtime/node.ts&lt;/code&gt; went to &lt;strong&gt;84 lines with zero dynamic imports&lt;/strong&gt;, replaced by an owned transport layer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deno publish --dry-run&lt;/code&gt; reports &lt;strong&gt;zero&lt;/strong&gt; &lt;code&gt;unanalyzable-dynamic-import&lt;/code&gt; warnings against it&lt;/li&gt;
&lt;li&gt;The library now has &lt;strong&gt;no runtime dependencies at all&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I kept &lt;code&gt;docs/jsr-winston-import-bug.md&lt;/code&gt; in the repo, with a banner at the top saying it&amp;rsquo;s resolved. The failure mode outlived the dependency and it&amp;rsquo;ll happen to somebody else.&lt;/p&gt;
&lt;p&gt;Three things I&amp;rsquo;d tell you to check today:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Download your own published tarball and read it.&lt;/strong&gt; Not the git tag, the artifact. &lt;code&gt;npm pack&lt;/code&gt;, or pull the &lt;code&gt;.tgz&lt;/code&gt; from the registry, and diff the source you shipped against the source you wrote. If you publish to two registries, do it twice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never let a catch block report a cause it can&amp;rsquo;t distinguish.&lt;/strong&gt; &amp;ldquo;Winston not found&amp;rdquo; was an inference from an error code, and the inference was wrong. Log what happened, and be honest that you&amp;rsquo;re guessing about why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional dependencies and static analysis are in direct conflict.&lt;/strong&gt; A tool that resolves every import at publish time has no way to express &amp;ldquo;this one might not exist.&amp;rdquo; If you&amp;rsquo;re publishing to JSR with an optional peer, you will hit this.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Happy Saturday. Go read your tarball.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://jsr.io/docs/publishing-packages&#34;&gt;JSR: Publishing packages&lt;/a&gt; — the static analysis and import requirements behind the rewrite&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://jsr.io/docs/troubleshooting&#34;&gt;JSR troubleshooting&lt;/a&gt; — including the unanalyzable dynamic import warning&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://nodejs.org/api/errors.html#err_module_not_found&#34;&gt;Node.js ERR_MODULE_NOT_FOUND&lt;/a&gt; — the error the catch block was swallowing&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/llbbl/logan-logger-ts/blob/v2.5.2/docs/jsr-winston-import-bug.md&#34;&gt;docs/jsr-winston-import-bug.md&lt;/a&gt; — the full writeup kept in the repo, tarball inspection included&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://jsr.io/@logan/logger&#34;&gt;@logan/logger on JSR&lt;/a&gt; — the package, now dependency-free&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>