<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>TypeScript on Jake Bailey</title>
    <link>https://jakebailey.dev/tags/typescript/</link>
    <description>Recent content in TypeScript on Jake Bailey</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 17 Oct 2023 11:22:43 -0700</lastBuildDate>
    <atom:link href="https://jakebailey.dev/tags/typescript/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>DefinitelyTyped is a monorepo!</title>
      <link>https://jakebailey.dev/posts/pnpm-dt-3/</link>
      <pubDate>Tue, 17 Oct 2023 11:22:43 -0700</pubDate>
      <guid>https://jakebailey.dev/posts/pnpm-dt-3/</guid>
      <description>Yes, it is! But for real this time!</description>
      <content:encoded><![CDATA[<h2 id="previously-on"><em>Previously, on &ldquo;Is DT a monorepo?&rdquo;</em></h2>
<p>In a <a href="https://jakebailey.dev/posts/pnpm-dt-1/">previous post</a>, I talked about the layout
of DefinitelyTyped and how it was indeed a monorepo, albeit a funky one. In
short, packages were laid out (more or less) like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">types/
</span></span><span class="line"><span class="cl">  gensync/
</span></span><span class="line"><span class="cl">    tsconfig.json
</span></span><span class="line"><span class="cl">    index.d.ts
</span></span><span class="line"><span class="cl">  node/
</span></span><span class="line"><span class="cl">    tsconfig.json
</span></span><span class="line"><span class="cl">    index.d.ts
</span></span><span class="line"><span class="cl">    package.json
</span></span><span class="line"><span class="cl">  react/
</span></span><span class="line"><span class="cl">    tsconfig.json
</span></span><span class="line"><span class="cl">    index.d.ts
</span></span><span class="line"><span class="cl">    package.json
</span></span></code></pre></div><p>And so on. Each <code>tsconfig.json</code> file contained bits like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;compilerOptions&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;baseUrl&#34;</span><span class="p">:</span> <span class="s2">&#34;../&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;typeRoots&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;../&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;types&#34;</span><span class="p">:</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This config means that when a types package looks for itself or another package,
it can map to other directories in <code>types</code>; no <code>package.json</code> is needed. At
publish time, we detected dependencies and add them. If a package needed an
external dependency, then the package <em>would</em> need a <code>package.json</code> file with
that dependency declared.</p>
<p>This provided a monorepo-like feel without any symlinking, but with many
downsides, including:</p>
<ul>
<li>Long <code>npm install</code> times when external dependencies are needed, especially
when testing the entire repo. The tooling just looped over every folder with a
<code>package.json</code> and ran <code>npm install</code>.</li>
<li>Completely unrealistic module resolution (no <code>node16</code> / <code>nodenext</code>, no
<code>export</code> maps, etc.) thanks to the use of <code>baseUrl</code>, <code>typeRoots</code>, and <code>paths</code>.
Not even <code>typesVersions</code> works.</li>
</ul>
<p>I also talked about <a href="https://jakebailey.dev/posts/pnpm-dt-1/#what-next">what we could do</a>
to remedy the situation, which boils down to &ldquo;what if we were just a monorepo
like everyone else uses in the JS ecosystem and let a package manager handle
things&rdquo;?</p>
<h2 id="making-fetch-happen">Making <code>fetch</code> happen</h2>
<p>Obviously, all of that was a good 6 months ago. There were some unresolved
blockers that made me put the project on the backburner. What changed?</p>
<p>Recently, Andrew merged
<a href="https://github.com/DefinitelyTyped/DefinitelyTyped/pull/66824"><code>fetch</code> support</a>
into <code>@types/node</code>. Yay!</p>
<p>But, you might have noticed that <em>only</em> <code>@types/node@20</code> got this feature.
Surprise! It&rsquo;s the second bullet point from above. DefinitelyTyped&rsquo;s fake module
resolution
<a href="https://github.com/DefinitelyTyped/DefinitelyTyped/pull/66824#issuecomment-1734391407">broke resolution</a>
inside <code>undici-types</code>, the package <code>@types/node</code> depends on to provide <code>fetch</code>
types (without depending on <code>undici</code> itself, which is vendored into Node). The
effect of this is that we could only add <code>fetch</code> to the <em>latest</em> types for Node,
not to any older versions. In fact, if <code>@types/node@22</code> were needed, we&rsquo;d have
to <em>drop</em> it from <code>@types/node@20</code>! Boo.</p>
<p>This problem shuffled the whole &ldquo;DefinitelyTyped monorepo&rdquo; thing straight to the
top of our interest list.</p>
<p>And so with that, I&rsquo;m happy to say that after a few weeks of effort from
<a href="https://github.com/sandersn">Nathan</a>,
<a href="https://github.com/andrewbranch">Andrew</a>, and myself, we&rsquo;re actually doing it!
DefinitelyTyped is becoming a monorepo!</p>
<h2 id="hello-pnpm">Hello, <code>pnpm</code></h2>
<p>If you&rsquo;ve read the previous posts, you won&rsquo;t be surprised to find that we&rsquo;re
using <code>pnpm</code> to do this. All modern package managers have some sort of monorepo
support these days, but DefinitelyTyped&rsquo;s unique situation limits what we can
use. Specifically, DefinitelyTyped contains <em>multiple versions</em> of the same
package. For example, we currently have <code>@types/react</code> v15-v18, <code>@types/node</code>
v16-v20, and so on. Both <code>npm</code> and <code>yarn</code> exit early when they see two workspace
packages with the same name. Understandable!<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> But, we need to
do it somehow.</p>
<p>With <code>pnpm</code>, this &ldquo;just works&rdquo;. Internally within <code>pnpm</code>, workspace packages are
identified by their paths, so there&rsquo;s no conflict. Then, when <code>pnpm</code> goes to
resolve packages, it only cares about the <code>name</code> and <code>version</code>. This actually
means we get something better than just &ldquo;it doesn&rsquo;t fail&rdquo;; it can actually
<em>resolve</em> to these workspace packages based on their versions! It behaves just
as though the packages were provided by the <code>npm</code> registry. So long, <code>paths</code>.</p>
<p>There&rsquo;s a bunch more goodness <code>pnpm</code> provides, but for now, let&rsquo;s just look at
the new layout.</p>
<h2 id="the-new-layout">The new layout</h2>
<p>Anyone who&rsquo;s worked in a monorepo will not be surprised by the new layout. Now,
we have:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">types/
</span></span><span class="line"><span class="cl">  gensync/
</span></span><span class="line"><span class="cl">    tsconfig.json
</span></span><span class="line"><span class="cl">    index.d.ts
</span></span><span class="line"><span class="cl">    package.json # new!
</span></span><span class="line"><span class="cl">  node/
</span></span><span class="line"><span class="cl">    tsconfig.json
</span></span><span class="line"><span class="cl">    index.d.ts
</span></span><span class="line"><span class="cl">    package.json
</span></span><span class="line"><span class="cl">  react/
</span></span><span class="line"><span class="cl">    tsconfig.json
</span></span><span class="line"><span class="cl">    index.d.ts
</span></span><span class="line"><span class="cl">    package.json
</span></span></code></pre></div><p>Every <code>@types</code> package now <em>requires</em> a <code>package.json</code>, even if it doesn&rsquo;t have
any external dependencies. Let&rsquo;s take a look at what&rsquo;s inside. Here&rsquo;s the new
bits of <code>package.json</code> for <code>@types/jsdom</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;private&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;@types/jsdom&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;21.1.9999&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;projects&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://github.com/jsdom/jsdom&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;minimumTypeScriptVersion&#34;</span><span class="p">:</span> <span class="s2">&#34;4.5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;dependencies&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;@types/node&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;@types/tough-cookie&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;parse5&#34;</span><span class="p">:</span> <span class="s2">&#34;^7.0.0&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;devDependencies&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;@types/jsdom&#34;</span><span class="p">:</span> <span class="s2">&#34;workspace:.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;owners&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Leonard Thieu&#34;</span><span class="p">,</span> <span class="nt">&#34;githubUsername&#34;</span><span class="p">:</span> <span class="s2">&#34;leonard-thieu&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Johan Palmfjord&#34;</span><span class="p">,</span> <span class="nt">&#34;githubUsername&#34;</span><span class="p">:</span> <span class="s2">&#34;palmfjord&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;ExE Boss&#34;</span><span class="p">,</span> <span class="nt">&#34;githubUsername&#34;</span><span class="p">:</span> <span class="s2">&#34;ExE-Boss&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>That&rsquo;s a lot of stuff. Much of this is information that was previously a part of
the <code>index.d.ts</code> &ldquo;header&rdquo;, i.e. something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="c1">// Type definitions for jsdom 21.1
</span></span></span><span class="line"><span class="cl"><span class="c1">// Project: https://github.com/jsdom/jsdom
</span></span></span><span class="line"><span class="cl"><span class="c1">// Definitions by: Leonard Thieu &lt;https://github.com/leonard-thieu&gt;
</span></span></span><span class="line"><span class="cl"><span class="c1">//                 Johan Palmfjord &lt;https://github.com/palmfjord&gt;
</span></span></span><span class="line"><span class="cl"><span class="c1">//                 ExE Boss &lt;https://github.com/ExE-Boss&gt;
</span></span></span><span class="line"><span class="cl"><span class="c1">// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
</span></span></span><span class="line"><span class="cl"><span class="c1">// Minimum TypeScript Version: 4.5
</span></span></span></code></pre></div><p>In the new layout, we&rsquo;re going to be using a package manager, so we need to
declare a <code>name</code> and <code>version</code> to get packages to link up. That&rsquo;s the first bit
of the header. At that point, we may as well just move everything into JSON and
be done with it. Additionally, this means that tools wanting to grab info about
a DefinitelyTyped package don&rsquo;t need to parse the header text; it&rsquo;s all in
<code>package.json</code>.</p>
<p>Let&rsquo;s break down the fields.</p>
<h3 id="private"><code>private</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;private&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is always set to true, telling <code>pnpm</code> to not attempt to publish this
package to the registry. The DefinitelyTyped publisher handles publishing.
Packages that had a <code>package.json</code> previously already had this set, so this is
nothing new.</p>
<h3 id="name"><code>name</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;@types/jsdom&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is new! Previously this was declared only via the directory name, but now
we&rsquo;re going to be using <code>pnpm</code> to handle things, so we need to specify this.</p>
<h3 id="version"><code>version</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;21.1.9999&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This one&rsquo;s funky; it&rsquo;s <em>almost</em> what we put in the header, but with a patch
version of <code>9999</code>. When DT packages are published (automatically on a schedule),
the patch version is generated; it&rsquo;s just whatever the previous version was,
plus one. So the patch version that&rsquo;s actually in the repo never matters.</p>
<p>At development time, we&rsquo;re making use of the fact that <code>pnpm</code> can resolve to
local versions. Normally, we could set <code>prefer-workspace-packages</code>, which would
force <code>pnpm</code> to always link to the local workspace package. But we actually have
a few packages which <em>intentionally</em> point to old versions of <code>@types</code> packages.
If we were to do the much nicer thing of using <code>0</code> as our patch version, the
version from the registry would always be chosen instead. So, we can&rsquo;t use
<code>prefer-workspace-packages</code>. Instead, we just pick an arbitrarily high patch
version, such that it will always be newer than what&rsquo;s in the registry, hence
<code>9999</code>. 9999 publishes ought to be enough for anyone, right?</p>
<h3 id="projects"><code>projects</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;projects&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://github.com/jsdom/jsdom&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is an array of helpful links to info about a project. Usually it contains a
GitHub link, but can sometimes contain more.</p>
<h3 id="minimumtypescriptversion"><code>minimumTypeScriptVersion</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;minimumTypeScriptVersion&#34;</span><span class="p">:</span> <span class="s2">&#34;4.5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This defines the minimum supported version of TypeScript version for a package.</p>
<h3 id="dependencies"><code>dependencies</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;dependencies&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;@types/node&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;@types/tough-cookie&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;parse5&#34;</span><span class="p">:</span> <span class="s2">&#34;^7.0.0&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This isn&rsquo;t new, but it is bigger! Dependencies on <code>@types</code> packages are now
<em>explicit</em>. No longer can every package access every other package; <code>pnpm</code> won&rsquo;t
link them. This removes the complexity of the infrastructure; we don&rsquo;t need to
parse the code or rely on heuristics to figure out what packages depend on what.</p>
<p>Additionally, this makes <code>pnpm</code> fully aware of how the packages interrelate,
meaning that we can use fun features like <code>--filter</code> (more on that later).</p>
<h3 id="devdependencies"><code>devDependencies</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;devDependencies&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;@types/jsdom&#34;</span><span class="p">:</span> <span class="s2">&#34;workspace:.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is new. For the most part, this will contain just one thing; a
self-dependency. Without the <code>baseUrl</code> / <code>typeRoots</code> / <code>paths</code> combo, a package
can&rsquo;t find itself anymore, but that&rsquo;s the API that we&rsquo;re wanting to test. <code>pnpm</code>
doesn&rsquo;t yet support creating self-links, so we do it ourselves using a
<code>workspace:.</code> specifier.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p>This list can also contain packages that are needed for testing. This
technically an improvement over the previous setup, which didn&rsquo;t allow
<code>devDependencies</code> at all. But, it&rsquo;s generally better to not have any testing
dependencies anyhow.</p>
<h3 id="owners"><code>owners</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;owners&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Leonard Thieu&#34;</span><span class="p">,</span> <span class="nt">&#34;githubUsername&#34;</span><span class="p">:</span> <span class="s2">&#34;leonard-thieu&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Johan Palmfjord&#34;</span><span class="p">,</span> <span class="nt">&#34;githubUsername&#34;</span><span class="p">:</span> <span class="s2">&#34;palmfjord&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;ExE Boss&#34;</span><span class="p">,</span> <span class="nt">&#34;githubUsername&#34;</span><span class="p">:</span> <span class="s2">&#34;ExE-Boss&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is a list of the users that &ldquo;own&rdquo; the package. They get pings when people
send PRs to packages and can approve them. This used to be in the as URLs (as
that&rsquo;s the syntax needed for the <code>contributors</code> array in <code>package.json</code>), but
our tooling only wants usernames and loads of people incorrectly typed their
GitHub profile URLs. For owners that aren&rsquo;t directly on github, <code>url</code> can still
be passed (though not shown above).</p>
<h3 id="nonnpm-nonnpmdescription"><code>nonNpm</code>, <code>nonNpmDescription</code></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;nonNpm&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;nonNpmDescription&#34;</span><span class="p">:</span> <span class="s2">&#34;Google Maps JavaScript API&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>My example didn&rsquo;t have these two, but some of the packages in DefinitelyTyped
describe things that aren&rsquo;t <code>npm</code> packages at all. For example,
<code>@types/google.maps</code> describes the Google Maps API (a global), and had a header
like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="c1">// Type definitions for non-npm package Google Maps JavaScript API 3.54
</span></span></span></code></pre></div><p>This info is used to inform various checks and is carried into the published
package. In the new layout, this information is represented in JSON.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<h2 id="installing-dependencies">Installing dependencies</h2>
<p>Let&rsquo;s start by doing the naive thing and just run <code>pnpm install</code> in the root of
the repo.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> pnpm install
</span></span><span class="line"><span class="cl"><span class="go">Scope: all 9114 workspace projects
</span></span></span><span class="line"><span class="cl"><span class="go">...
</span></span></span><span class="line"><span class="cl"><span class="go">Done in 3m 35.4s
</span></span></span></code></pre></div><p>Wow, that&rsquo;s a lot of install. But it&rsquo;s a major improvement over the previous
layout, where installing the entire repo (with 10x fewer <code>package.json</code> files)
took some 30 minutes.</p>
<p>The good news is that those working on DT don&rsquo;t actually need to install the
entire repo. <code>pnpm</code> supports <a href="https://pnpm.io/filtering">filtering</a>. Let&rsquo;s say
I&rsquo;m working on <code>@types/node</code>, and want to be able to test it and any packages it
depends on. I can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> pnpm install -w --filter <span class="s1">&#39;...@types/node...&#39;</span>
</span></span><span class="line"><span class="cl"><span class="go">Scope: 2722 of 9114 workspace projects
</span></span></span><span class="line"><span class="cl"><span class="go">...
</span></span></span><span class="line"><span class="cl"><span class="go">Done in 1m 2.5s
</span></span></span></code></pre></div><p>That&rsquo;s a good bit better! Since we have explicit dependencies, <code>pnpm</code> can
actually figure out what packages are needed for <code>@types/node</code> and install
those, but also figure out which packages <em>depend</em> on <code>@types/node</code> and install
those too. The <code>-w</code> tells <code>pnpm</code> to also install the workspace root, which is
needed to get the DefinitelyTyped tooling, linters, <code>dprint</code>, etc.</p>
<p>What about package that <em>isn&rsquo;t</em> so hefty?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> pnpm install -w --filter <span class="s1">&#39;...@types/lodash...&#39;</span>
</span></span><span class="line"><span class="cl"><span class="go">Scope: 372 of 9114 workspace projects
</span></span></span><span class="line"><span class="cl"><span class="go">...
</span></span></span><span class="line"><span class="cl"><span class="go">Done in 7.2s
</span></span></span></code></pre></div><p>Now we&rsquo;re talking. Most packages won&rsquo;t need to do a huge install, so long as
people read the docs (🙃) to know how to avoid the big install.</p>
<p>From this point on, the workflow is the same as DefinitelyTyped was before.</p>
<h3 id="filtering-in-ci">Filtering in CI</h3>
<p>There&rsquo;s one other cool trick that we can use in CI; not only can we filter by
package name, but we can also filter by what changed since a specific git ref.
In a PR build, we can use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> pnpm install -w --filter <span class="s1">&#39;...[origin/master]&#39;</span>
</span></span></code></pre></div><p>And only get what we need.</p>
<h2 id="other-misc-improvements">Other misc improvements</h2>
<p>There&rsquo;s also a grab bag of other improvements that come with this change. In no
particular order:</p>
<ul>
<li>Having to redo a bunch of the DefinitelyTyped tooling has led to improvements
like <code>dtslint-runner</code> no longer bailing early on certain kinds of errors. Many
more things are collected for reporting at the end such that doing one thing
wrong doesn&rsquo;t hide the problem until a second run.</li>
<li>As a part of making everything work in the new monorepo, we manually fixed a
few <em>hundred</em> packages. These packages were silently broken (or at least,
weird) in various ways. For example, multiple packages imported the <code>events</code>
library. This could mean <code>@types/node</code>, but it could also mean
<code>@types/events</code>. In practice, it resolved to the latter, but then sometimes,
the tooling would say the package depended on <em>neither</em> (probably due to a bug
in the implicit dependency resolution). Now, each package actually has to say
which they need. There are other weird things besides just this; invalid
<code>references</code> directives, packages depending on the wrong versions of things,
etc.</li>
<li>Having a complete working <code>package.json</code> for every package means that one can
theoretically just <code>npm pack</code> and get a working tarball. This is likely to
become useful for tools like
<a href="https://arethetypeswrong.github.io/">Are The Types Wrong</a>, although the
publisher still does a bunch of stuff (notably, deciding which files actually
get included in each package, which is still mostly implicit).</li>
<li>It turns out that a load of <code>react</code>-based types packages are broken at the
moment due to <code>@types/react</code> using <code>typesVersions</code>. Since <code>typesVersions</code> is
in <code>package.json</code>, but <code>baseUrl</code> / <code>typesRoot</code> / <code>paths</code> skip <code>package.json</code>
resolution, packages that depend on <code>@types/react</code> always get the types meant
for TS 5.1 and above. Oops. With actual <code>node_modules</code> linking, this isn&rsquo;t a
problem and things work as intended. Another reason to speed this along.</li>
</ul>
<h2 id="its-not-all-rainbows-and-sunshine">It&rsquo;s not all rainbows and sunshine</h2>
<p>Everything I&rsquo;ve described so far has been an improvement over the previous
layout. But there are some warts left to figure out.</p>
<h3 id="using-shared-workspace-lockfilefalse">Using <code>shared-workspace-lockfile=false</code></h3>
<p>The astute reader may have noticed that the performance of <code>pnpm install</code> seems
<em>way</em> slower than expected, especially given the numbers I achieved in the
<a href="https://jakebailey.dev/posts/pnpm-dt-2/">previous post</a> about making <code>pnpm</code> faster.</p>
<p>The reason for this slowdown is our use of <code>shared-workspace-lockfile=false</code>.
This is a lesser-used option which instructs <code>pnpm</code> to instead handle each
workspace package individually. Each package gets is own dependency graph, and
calculating that 9000 some times is a lot slower than doing it once.</p>
<p>Why enable it, then? It&rsquo;s a bit hard to fully explain. I filed
<a href="https://github.com/pnpm/pnpm/issues/6457">an issue</a> for it upstream, but the
gist is that since <code>pnpm</code> is stricter about how it handles dependencies (they
aren&rsquo;t just all hoisted to the top), the combo of so many packages forgetting to
declare dependencies on <code>@types</code> packages along with some packages (outside DT)
explicitly depending on <code>@types/node@17</code> (why??) causes the &ldquo;fallback&rdquo;
<code>@types/node</code> to point to that awkward v17 version.</p>
<p>I haven&rsquo;t quite figured out what the best solution is; it&rsquo;s possible that <code>pnpm</code>
could gain a yarn-like hoisting-limit system to avoid this problem, or always
resolve &ldquo;fallback&rdquo; dependencies to the latest version.</p>
<p>I had previously skipped working on this project due to this problem, but the
upsides of the migration (especially with <code>fetch</code> getting thrown in the mix)
tipped the scales. Although <code>shared-workspace-lockfile=false</code> is quite a bit
slower, it&rsquo;s still an improvement when installing the entire repo, and filtering
provides a very straightforward way to reduce the cost of package installs.</p>
<p>Just to show what the difference is, here&rsquo;s the install without this setting.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> pnpm install
</span></span><span class="line"><span class="cl"><span class="go">Scope: all 9114 workspace projects
</span></span></span><span class="line"><span class="cl"><span class="go">...
</span></span></span><span class="line"><span class="cl"><span class="go">Done in 1m 5.8s
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">$</span> pnpm install -w --filter <span class="s1">&#39;...@types/node...&#39;</span>
</span></span><span class="line"><span class="cl"><span class="go">Scope: 2722 of 9114 workspace projects
</span></span></span><span class="line"><span class="cl"><span class="go">...
</span></span></span><span class="line"><span class="cl"><span class="go">Done in 30.2s
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">$</span> pnpm install -w --filter <span class="s1">&#39;...@types/lodash...&#39;</span>
</span></span><span class="line"><span class="cl"><span class="go">Scope: 372 of 9114 workspace projects
</span></span></span><span class="line"><span class="cl"><span class="go">...
</span></span></span><span class="line"><span class="cl"><span class="go">Done in 7.9s
</span></span></span></code></pre></div><p>Not helpful super for a small number of packages, but quite a bit faster if you
ever need to work with the whole thing.</p>
<h3 id="git-clean-is-broken-on-windows"><code>git clean</code> is broken on Windows</h3>
<p><code>pnpm</code> uses symlinks under the hood. On POSIX-ish platforms like Linux and
macOS, this is all good; symlinks work for any user and behave as expected. On
Windows, however, the story is different. For a very long time, the only way to
get &ldquo;real&rdquo; symlinks was to gain elevated permissions. Without being an admin,
the best you could hope for was a &ldquo;junction&rdquo;. I&rsquo;m not sure I could explain the
ins and outs of junctions other than to say that they&rsquo;re <em>like</em> a symlink, but
only for directories, and they act kinda weird sometimes. But, they do the job.</p>
<p>There&rsquo;s a gotcha; if you run <code>git clean</code>, <code>git</code> treats junctions as directories!
This is normally not a big deal, but every one of our packages has a self link,
which makes the symlinks recursive. And since <code>git</code> doesn&rsquo;t treat junctions like
symlinks, <code>git clean</code> will just keep recursing infinitely until it hits the max
path length. It may <em>eventually</em> finish, but without loads of errors and
complaining.</p>
<p>There are two ways forward:</p>
<ul>
<li>Make <code>git</code> treat junctions as symlinks. I sent
<a href="https://github.com/git-for-windows/git/pull/4383">a PR for this</a> earlier this
year, but it hasn&rsquo;t yet been accepted. I personally think this is the correct
solution; if <code>git clean</code> had been implemented in shell scripts (like much of
<code>git</code>), it would have treated junctions as symlinks and just worked. But
<code>git clean</code> is written in C, so instead of going through <code>git-bash</code>, it goes
through the shims which translate the POSIX-y file system accesses into the
Windows API, and those shims disagree with <code>git-bash</code>.<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></li>
<li>Make <code>pnpm</code> use real symlinks. You&rsquo;re probably confused; didn&rsquo;t I just say
that you needed elevated privileges to do that? Normally, yes, but if you
enable Developer Mode, any user is able to make symlinks! And, I fully suspect
that most people developing on Windows have this enabled. You even need
enabled to enable WSL. This is probably a good idea whether or not <code>git</code>
changes; real symlinks don&rsquo;t have the same warts as junctions. I&rsquo;ll disclaim
that I haven&rsquo;t actually proposed this change upstream. There are some gotchas
in that it may be awkward to enable this automatically (what happens if you
end up with a <code>node_modules</code> with both junctions and symlinks?), but I think
it should be straightforward to detect.</li>
</ul>
<p>For now, DefinitelyTyped has included a script Windows users can run to clean up
<code>node_modules</code>; <code>pnpm run clean-node-modules</code> will find and delete all
<code>node_modules</code> directories within the repo. Good enough for now.</p>
<h3 id="removing-a-package-wont-expose-breaking-changes-in-newly-typed-packages">Removing a package won&rsquo;t expose breaking changes in newly-typed packages</h3>
<p>This one&rsquo;s subtle. Imagine we have a package <code>@types/foo</code>. Another package (in
the repo or even external) depends on <code>@types/foo</code>. But, <code>foo</code> has just gained
types, which means that it&rsquo;s time to remove <code>@types/foo</code> from DefinitelyTyped.</p>
<p>In the old layout, we&rsquo;d delete the directory and add it to <code>notNeededPackages</code>.
When the PR that does this is merged, the publisher will publish one final
version of <code>@types/foo</code> that contains only a <code>package.json</code> with a dependency on
the real <code>foo</code>.</p>
<p>But, when you&rsquo;re actually working on the PR that does this, the shim package
hasn&rsquo;t yet been published! If another package within DefinitelyTyped depends on
<code>@types/foo</code> it will stop pointing to the one in the repo (it&rsquo;s been deleted).
In the old layout, things would stop compiling until dependencies are updated.
But in the new layout, <code>pnpm</code> will just resolve to the latest version of
<code>@types/foo</code> in the registry, which will be exactly the same code that is being
deleted. This means that the PR will definitely pass CI, when it may actually
fail later if the real upstream <code>foo</code> package has types which differ enough to
break things.</p>
<p>There&rsquo;s not really a great solution to this other than to ban external
dependencies on <code>@types</code> packages that aren&rsquo;t contained in the repo; that
handles some of the situations but not all. (If you have any clever ideas, let
me know.)</p>
<h3 id="removing-a-package-isnt-reflected-in-pnpm-git-filter">Removing a package isn&rsquo;t reflected in pnpm git filter</h3>
<p>In addition to the above, when we delete a package, <code>pnpm</code>&rsquo;s behavior for
<code>--filter '...[origin/master]'</code> doesn&rsquo;t pick up on the removed package. In an
ideal world, it&rsquo;d see the package removed, and then figure out which local
packages are affected by the removal. But, it doesn&rsquo;t do that, either simply
because the package is gone (so there&rsquo;s no more edges to check), or due to the
previous section (where the package is still there, just not in the repo). The
workaround is to instead use
<code>pnpm ls --depth -1 --filter '...@types/removed...'</code> to get some sort of list of
what may need to be tested. In CI, if a <code>package.json</code> is removed from the repo,
we also don&rsquo;t use <code>pnpm install --filter '...[origin/master]'</code>, resorting to a
complete install.</p>
<h2 id="future-work">Future work</h2>
<p>After this is merged, we&rsquo;re still not done! There is some exiting stuff that
gets unlocked:</p>
<ul>
<li>Since there&rsquo;s no more header, there&rsquo;s nothing special about <code>index.d.ts</code>
anymore. Since we&rsquo;re trying to get DT as close to the upstream packages as
possible, it may actually be <em>wrong</em> to have an <code>index.d.ts</code> if the package
doesn&rsquo;t contain <code>index.js</code>. We should be able to remove the requirement that
packages have an <code>index.d.ts</code>.</li>
<li>Right now, there&rsquo;s no way to really verify that types work properly for people
using <code>nodenext</code> resolution (though, some analysis has shown that
DefinitelyTyped does <em>better</em> in this regard than those publishing types
themselves).Now that everything works via <code>node_modules</code>, we could finally use
enable those options in <code>tsconfig.json</code> and verify that things work. Maybe
even required.</li>
<li>We could offload even more of <code>dtslint-runner</code> onto <code>pnpm</code> scripts.</li>
<li>We could move tests out into their own packages, forcing them to use the
public APIs of the packages they&rsquo;re testing. Or even, have multiple
<code>tsconfig</code>s to make sure things work with various settings.</li>
</ul>
<h2 id="anyway">Anyway&hellip;</h2>
<p>I hope this info dump was interesting; I&rsquo;m really excited to see this change
happen.</p>
<p>Big thanks to everyone involved with this big migration, including
<a href="https://github.com/sandersn">Nathan</a> and
<a href="https://github.com/andrewbranch">Andrew</a> from the TypeScript team,
<a href="https://github.com/zkochan">Zoltan</a> of <code>pnpm</code> fame, and everyone who spent time
finding <em>more</em> ways to speed <code>pnpm</code> and <code>semver</code> up for our ridiculous use case.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Honestly, this is a little unsatisfying. If you think about
it, all package managers already need to be able to handle multiple versions
of the same package when they&rsquo;re sourced from the npm registry.
Theoretically, they could support multiple versions of workspace packages,
but alas, no.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Technically, <code>link:.</code> would also work, but for &ldquo;reasons&rdquo;,
<code>workspace:.</code> has better performance.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>There&rsquo;s still some clarity needed about what these fields are
supposed to represent. There are quite a few packages (~200) that don&rsquo;t have
this field set but aren&rsquo;t on <code>npm</code> either. We&rsquo;ll get it sorted; my hope is
that this field becomes defined specifically as &ldquo;this package is not on npm,
do not look at npm for it, but if you do find an npm package with this name,
then that may be a problem so CI should fail until we triage the problem&rdquo;.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>It&rsquo;s absolutely possible that my take is the wrong one here;
I know Go recently recently changed things to treat these special reparse
points as some sort of &ldquo;irregular&rdquo; file. Honestly, I have no clue.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Speeding up pnpm</title>
      <link>https://jakebailey.dev/posts/pnpm-dt-2/</link>
      <pubDate>Sun, 26 Mar 2023 13:29:45 -0700</pubDate>
      <guid>https://jakebailey.dev/posts/pnpm-dt-2/</guid>
      <description>DefinitelyTyped contains over 8000 packages. What could go wrong?</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>For more background, see the <a href="https://jakebailey.dev/posts/pnpm-dt-1/">previous post about DefinitelyTyped</a>.</p>
<p>TL;DR: DefinitelyTyped is huge; installing it in its entirety involves
processing <em>over 9,000</em> packages. And that&rsquo;s slow! Or is it?</p>
<h2 id="taking-a-profile">Taking a profile</h2>
<p>Many people may not know this, but I&rsquo;ve actually written more Go than I have
TypeScript.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> As such, when I have a performance problem, the tool I like to
use is <a href="https://github.com/google/pprof">pprof</a>.</p>
<p>More commonly, this tool is used when profiling Go, C, C++ code. And I like this
tool! Lucky for me, there is
<a href="https://www.npmjs.com/package/@datadog/pprof">a library</a> which lets you use it
with Node.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> The API is pretty straightforward; you can start and stop
both CPU and heap profiles, and write them to disk.</p>
<p>Unfortunately, that&rsquo;s a little annoying, because effectively 100% of the time,
I&rsquo;m profiling a CLI application or someone else&rsquo;s project where I don&rsquo;t really
want to inject the code. It does include some code to let you do
<code>node --require=pprof myScript.js</code>, but there&rsquo;s no way to configure its
behavior.</p>
<p>So a few years ago, I made a little wrapper,
<a href="https://www.npmjs.com/package/pprof-it">pprof-it</a>, which makes things much
easier to use. You can check the README for more details, but in short, to get a
pprof profile you just run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">$ pprof-it /path/to/script.js
</span></span></code></pre></div><p><code>pprof-it</code> will start profiling both CPU and heap allocation immediately at
startup then dump profiles to the current directory on exit. These files can
then be loaded into <code>pprof</code> (or one of the many other tools which support the
format, like <a href="https://flamegraph.com">flamegraph.com</a> or
<a href="https://www.speedscope.app">speedscope</a>).</p>
<p>So, let&rsquo;s take a profile of <code>pnpm install</code> on one of my work-in-progress &ldquo;DT as
a monorepo&rdquo; branches. (Forgive the roundabout way of running things; some of my
fixes are already released, so I need to do a little movie magic.)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">$ npx --package=pnpm@7.30.0 -c &#39;pprof-it $(which pnpm) install&#39;
</span></span></code></pre></div><p>This actually OOMs on my laptop (I have yet to determine why), but on my
desktop, I get this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">pprof-it: Starting profilers (heap, time)
</span></span><span class="line"><span class="cl">    # a very long pause...
</span></span><span class="line"><span class="cl">Scope: all 9031 workspace projects
</span></span><span class="line"><span class="cl">    # a very very long warning about cycles (I need to file an issue for this!)
</span></span><span class="line"><span class="cl">Lockfile is up to date, resolution step is skipped
</span></span><span class="line"><span class="cl">Already up to date
</span></span><span class="line"><span class="cl">    # another long pause
</span></span><span class="line"><span class="cl">Done in 1m 39.7s
</span></span><span class="line"><span class="cl">pprof-it: Stopping profilers
</span></span><span class="line"><span class="cl">pprof-it: Writing heap profile to pprof-heap-286252.pb.gz
</span></span><span class="line"><span class="cl">pprof-it: Writing time profile to pprof-time-286252.pb.gz
</span></span></code></pre></div><p>Great, now let&rsquo;s run pprof:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">$ pprof -http=: pprof-time-286252.pb.gz
</span></span></code></pre></div><p>Automatically, <code>pprof</code> starts up my browser and puts me right into the graph
view. This view outside of Node profiles is very useful, but Node profiles have
an unfortunate problem which leads to all anonymous (i.e. arrow) functions being
counted as one node named &ldquo;(anonymous)&rdquo;.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> So, let&rsquo;s flip into the
flame view.</p>
<p><img alt="A pprof profile of the original test case; two large blocks. The overall execution takes about 100 seconds." loading="lazy" src="/posts/pnpm-dt-2/profile1.png#center"></p>
<p>Already, I&rsquo;m excited; this is every profiler&rsquo;s dream. Two very obvious chunks of
work attributed to real names I can search for. Roughly 50 seconds are spent in
<code>createPkgGraph</code> and another 32 seconds in <code>getRootPackagesToLink</code>. I should
note that at this point in my adventure, I know <em>absolutely nothing</em> about how
<code>pnpm</code> works; I haven&rsquo;t even checked out the repo. But, now I know exactly where
to look! (If <code>pnpm</code> had been minified, I&rsquo;d be in a much worse position.)</p>
<h2 id="working-through-the-code">Working through the code</h2>
<p>From the get-go I can see that there&rsquo;s a lot of time spent in <code>resolve</code>. One
thing I hadn&rsquo;t mentioned was how I set up this huge monorepo; my
<a href="https://github.com/jakebailey/DefinitelyTyped/tree/blog-pnpm-workspaces-with-paths">initial version</a>
of the monorepo transition used version specifiers like <code>workspace:../node</code> to
directly map packages to each other, avoiding the need for us to specify
names/versions in every <code>package.json</code> (they&rsquo;re already auto-generated by the DT
publisher). Without even looking at the code, I (correctly) guessed that these
paths were involved in the slowdown and
<a href="https://github.com/pnpm/pnpm/issues/6277">filed an issue</a>.</p>
<p>It turns out that this path mapping is actually a negative for other reasons as
well, so I just rewrote my transform to use versions instead of paths. After
switching to this
<a href="https://github.com/jakebailey/DefinitelyTyped/tree/blog-pnpm-workspaces-with-versions">new version</a>,
the profile looks like this:</p>
<p><img alt="A pprof profile of the &ldquo;no paths&rdquo; test case, two large blocks, first one smaller than before. The overall execution takes about 65 seconds." loading="lazy" src="/posts/pnpm-dt-2/profile2.png#center"></p>
<p>Alright, that&rsquo;s better already, down from ~100 seconds to 64 seconds. We&rsquo;ll come
back to <code>resolve</code> later.</p>
<h2 id="createpkggraph"><code>createPkgGraph</code></h2>
<p>The first block is the first &ldquo;very long pause&rdquo; (which happens even in the &ldquo;new&rdquo;
version of the repo), so let&rsquo;s start there. Searching the <code>pnpm</code> codebase, I
find the offending function. It looks something like this (cut down for
brevity):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">createPkgGraph</span><span class="p">(</span><span class="nx">pkgs</span>: <span class="kt">Array</span><span class="p">&lt;</span><span class="nt">Package</span><span class="p">&gt;)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">pkgMap</span> <span class="o">=</span> <span class="nx">createPkgMap</span><span class="p">(</span><span class="nx">pkgs</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">mapValues</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">        <span class="nx">dependencies</span>: <span class="kt">createNode</span><span class="p">(</span><span class="nx">pkg</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="kr">package</span><span class="o">:</span> <span class="nx">pkg</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span> <span class="nx">pkgMap</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">function</span> <span class="nx">createNode</span><span class="p">(</span><span class="nx">pkg</span>: <span class="kt">Package</span><span class="p">)</span><span class="o">:</span> <span class="kt">string</span><span class="p">[]</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">dependencies</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="p">...</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">devDependencies</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">...</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">optionalDependencies</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">...</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">dependencies</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">entries</span><span class="p">(</span><span class="nx">dependencies</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">map</span><span class="p">(([</span><span class="nx">depName</span><span class="p">,</span> <span class="nx">rawSpec</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="kr">const</span> <span class="nx">isWorkspaceSpec</span> <span class="o">=</span> <span class="nx">rawSpec</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="s2">&#34;workspace:&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="kr">const</span> <span class="nx">spec</span> <span class="o">=</span> <span class="nx">npa</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">depName</span><span class="p">,</span> <span class="nx">rawSpec</span><span class="p">,</span> <span class="nx">pkg</span><span class="p">.</span><span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">spec</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="s2">&#34;directory&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="kr">const</span> <span class="nx">matchedPkg</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">pkgMap</span><span class="p">).</span><span class="nx">find</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">path</span><span class="p">.</span><span class="nx">relative</span><span class="p">(</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">dir</span><span class="p">,</span> <span class="nx">spec</span><span class="p">.</span><span class="nx">fetchSpec</span><span class="p">)</span> <span class="o">===</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">                    <span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">return</span> <span class="nx">matchedPkg</span><span class="o">?</span><span class="p">.</span><span class="nx">dir</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="kr">const</span> <span class="nx">pkgs</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">pkgMap</span><span class="p">).</span><span class="nx">filter</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="nx">depName</span>
</span></span><span class="line"><span class="cl">                <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">pkgs</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="kr">const</span> <span class="nx">versions</span> <span class="o">=</span> <span class="nx">pkgs</span><span class="p">.</span><span class="nx">filter</span><span class="p">(({</span> <span class="nx">manifest</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="nx">manifest</span><span class="p">.</span><span class="nx">version</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">version</span><span class="p">)</span> <span class="kr">as</span> <span class="kt">string</span><span class="p">[];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">isWorkspaceSpec</span> <span class="o">&amp;&amp;</span> <span class="nx">versions</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="kr">const</span> <span class="nx">matchedPkg</span> <span class="o">=</span> <span class="nx">pkgs</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="nx">depName</span>
</span></span><span class="line"><span class="cl">                    <span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">return</span> <span class="nx">matchedPkg</span><span class="o">!</span><span class="p">.</span><span class="nx">dir</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">versions</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">rawSpec</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="kr">const</span> <span class="nx">matchedPkg</span> <span class="o">=</span> <span class="nx">pkgs</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="nx">depName</span>
</span></span><span class="line"><span class="cl">                        <span class="o">&amp;&amp;</span> <span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">rawSpec</span>
</span></span><span class="line"><span class="cl">                    <span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">return</span> <span class="nx">matchedPkg</span><span class="o">!</span><span class="p">.</span><span class="nx">dir</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">            <span class="p">})</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Alright, so we can sort of see what might be going on here. First off, we have
<code>pkgMap</code>. By attaching to the code and looking at the variable, we find that
it&rsquo;s an object which consists of all 9,000+ packages. So doing anything with
that is going to take a while.</p>
<p>At the top level, we&rsquo;re already looping over every entry in the object via
ramda&rsquo;s <code>mapValues</code>. But, if we look inside <code>createNode</code>, we can see that it is
<em>also</em> looping over all of <code>pkgMap</code> by calling <code>Object.values(pkgMap)</code>! This is
quadratic; we&rsquo;ll be doing 9,000 x 9,000 scans over the array. We could fix this
by instead creating a mapping and accessing it instead. For example, one of the
loops is just looking for all of the entries in <code>pkgMap</code> where
<code>pkg.manifest.name</code> is some value. We could precalculate this mapping, producing
an object of type <code>Record&lt;string, Package[]&gt;</code>.</p>
<p>The other loop is more complicated; this is where <code>resolve</code> comes in. We can see
that we&rsquo;re searching not for a specific name but for a specific set of packages
whose paths map the one we specified (that <code>workspace:../node</code> from earlier).
This one is tricky, but it&rsquo;s possible that we could precalculate some table here
too, depending on how sensitive this code is to <code>path.resolve</code>&rsquo;s
platform-specific semantics.</p>
<p>Speaking of precalculating&hellip; We just said that <code>pkgMap</code> was huge. But, for
every call to <code>createNode</code>, we call <code>Object.values(pkgMap)</code>! The profile doesn&rsquo;t
explicitly state so, but this is really, really expensive. The good news is that
<code>pkgMap</code> is never modified. This means that we could calculate this big array
once and then reuse it, for example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">createPkgGraph</span><span class="p">(</span><span class="nx">pkgs</span>: <span class="kt">Array</span><span class="p">&lt;</span><span class="nt">Package</span><span class="p">&gt;)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">pkgMap</span> <span class="o">=</span> <span class="nx">createPkgMap</span><span class="p">(</span><span class="nx">pkgs</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">pkgMapValues</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">pkgMap</span><span class="p">);</span> <span class="c1">// &lt;-- NEW!
</span></span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">mapValues</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">        <span class="nx">dependencies</span>: <span class="kt">createNode</span><span class="p">(</span><span class="nx">pkg</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="kr">package</span><span class="o">:</span> <span class="nx">pkg</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span> <span class="nx">pkgMap</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">function</span> <span class="nx">createNode</span><span class="p">(</span><span class="nx">pkg</span>: <span class="kt">Package</span><span class="p">)</span><span class="o">:</span> <span class="kt">string</span><span class="p">[]</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">entries</span><span class="p">(</span><span class="nx">dependencies</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">map</span><span class="p">(([</span><span class="nx">depName</span><span class="p">,</span> <span class="nx">rawSpec</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">spec</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="s2">&#34;directory&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="kr">const</span> <span class="nx">matchedPkg</span> <span class="o">=</span> <span class="nx">pkgMapValues</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">path</span><span class="p">.</span><span class="nx">relative</span><span class="p">(</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">dir</span><span class="p">,</span> <span class="nx">spec</span><span class="p">.</span><span class="nx">fetchSpec</span><span class="p">)</span> <span class="o">===</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">                    <span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">return</span> <span class="nx">matchedPkg</span><span class="o">?</span><span class="p">.</span><span class="nx">dir</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="kr">const</span> <span class="nx">pkgs</span> <span class="o">=</span> <span class="nx">pkgMapValues</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">pkg</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">pkg</span><span class="p">.</span><span class="nx">manifest</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="nx">depName</span>
</span></span><span class="line"><span class="cl">                <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">            <span class="p">})</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This turns out to save the bulk of the time. Yay!</p>
<p>Algorithmically, the code is still quadratic, but it&rsquo;s still a lot faster and
this kind of change is very safe, safe enough to be backported. I sent this one
as a <a href="https://github.com/pnpm/pnpm/pull/6281">quick PR</a>, and it&rsquo;s now out in
v7.30.4.</p>
<p>The fix to the quadratic-ness is going to be a different, more complicated
change I plan to send later.</p>
<p><strong>UPDATE:</strong> Later is now the past! All of the quadratic-ness has been fixed as
of:</p>
<ul>
<li><a href="https://github.com/pnpm/pnpm/pull/6287">perf(pkgs-graph): speed up createPkgGraph by using a table for manifest name lookup</a></li>
<li><a href="https://github.com/pnpm/pnpm/pull/6317">perf(pkgs-graph): speed up createPkgGraph when directory specifiers are present</a></li>
</ul>
<h2 id="getrootpackagestolink"><code>getRootPackagesToLink</code></h2>
<p>Let&rsquo;s look at the second big chunk. Cut down for brevity again, we have:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">getRootPackagesToLink</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nx">lockfile</span>: <span class="kt">Lockfile</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">opts</span><span class="o">:</span> <span class="p">{</span><span class="cm">/* some options */</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">importerManifestsByImporterId</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">manifest</span> <span class="p">}</span> <span class="k">of</span> <span class="nx">opts</span><span class="p">.</span><span class="nx">projects</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">importerManifestsByImporterId</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="o">=</span> <span class="nx">manifest</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">projectSnapshot</span> <span class="o">=</span> <span class="nx">lockfile</span><span class="p">.</span><span class="nx">importers</span><span class="p">[</span><span class="nx">opts</span><span class="p">.</span><span class="nx">importerId</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">allDeps</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="p">...</span><span class="nx">projectSnapshot</span><span class="p">.</span><span class="nx">devDependencies</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">...</span><span class="nx">projectSnapshot</span><span class="p">.</span><span class="nx">dependencies</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">...</span><span class="nx">projectSnapshot</span><span class="p">.</span><span class="nx">optionalDependencies</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="k">await</span> <span class="nx">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Object</span><span class="p">.</span><span class="nx">entries</span><span class="p">(</span><span class="nx">allDeps</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kr">async</span> <span class="p">([</span><span class="nx">alias</span><span class="p">,</span> <span class="nx">ref</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="c1">// a bunch of props
</span></span></span><span class="line"><span class="cl">                <span class="p">};</span>
</span></span><span class="line"><span class="cl">            <span class="p">}),</span>
</span></span><span class="line"><span class="cl">    <span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">)</span> <span class="kr">as</span> <span class="nx">LinkedDirectDep</span><span class="p">[];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Again, the profile is not being very specific. It&rsquo;s just saying that a lot of
time is being spent in <code>getRootPackagesToLink</code>. Thankfully, there&rsquo;s not much
code actually inside this function. It can only be the calculation of
<code>importerManifestsByImporterId</code>, or the spread to produce <code>allDeps</code>.</p>
<p>I debugged this to try and get the size of these elements.
<code>getRootPackagesToLink</code> is called for every package in the repo, and <code>allDeps</code>
is small. So that&rsquo;s not likely to be it.</p>
<p>The <code>importerManifestsByImporterId</code> loop, on the other hand, is suspicious. I
just said that <code>getRootPackagesToLink</code> is called once per package in the repo.
But, <code>opts.projects</code> <em>is</em> a big list of all packages in the repo! We&rsquo;re
quadratic again!</p>
<p>This is better than before, in theory; there are lookups inside the <code>.map</code> call
below, but they&rsquo;re efficient because they don&rsquo;t loop over <code>opts.projects</code> (as
opposed to <code>createNode</code> from earlier, which <em>does</em> do the linear lookup). But,
<code>getRootPackagesToLink</code> is recreating this mapping every single time it&rsquo;s
called!</p>
<p>If we scroll down a little bit, we can find its sole caller:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">projectsToLink</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">fromEntries</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="nx">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nx">projects</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">rootDir</span><span class="p">,</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">modulesDir</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="nx">id</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">dir</span>: <span class="kt">rootDir</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">modulesDir</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">dependencies</span>: <span class="kt">await</span> <span class="nx">getRootPackagesToLink</span><span class="p">(</span><span class="nx">filteredLockfile</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">                <span class="nx">projects</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">            <span class="p">}),</span>
</span></span><span class="line"><span class="cl">        <span class="p">}]),</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>There&rsquo;s that &ldquo;for each package&rdquo; thing again. Thankfully, we can again see that
<code>projects</code> is not changing between calls. So, we can instead calculate this
mapping <em>once</em> and pass it in to <code>getRootPackagesToLink</code>, again without changing
much logic.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">importerManifestsByImporterId</span> <span class="o">=</span> <span class="p">{}</span> <span class="kr">as</span> <span class="p">{</span> <span class="p">[</span><span class="nx">id</span>: <span class="kt">string</span><span class="p">]</span><span class="o">:</span> <span class="nx">ProjectManifest</span><span class="p">;</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">manifest</span> <span class="p">}</span> <span class="k">of</span> <span class="nx">opts</span><span class="p">.</span><span class="nx">projects</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">importerManifestsByImporterId</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="o">=</span> <span class="nx">manifest</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">projectsToLink</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">fromEntries</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="nx">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nx">projects</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">rootDir</span><span class="p">,</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">modulesDir</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="nx">id</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">dir</span>: <span class="kt">rootDir</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">modulesDir</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">dependencies</span>: <span class="kt">await</span> <span class="nx">getRootPackagesToLink</span><span class="p">(</span><span class="nx">filteredLockfile</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">                <span class="nx">importerManifestsByImporterId</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">            <span class="p">}),</span>
</span></span><span class="line"><span class="cl">        <span class="p">}]),</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>Now drop the code to produce the mapping from <code>getRootPackagesToLink</code> and we&rsquo;re
done.</p>
<p>I sent this as <a href="https://github.com/pnpm/pnpm/pull/6282">a PR</a> over too, and it
also is available in v7.30.4.</p>
<h2 id="the-final-result-for-now">The &ldquo;final&rdquo; result (for now)</h2>
<p>Now that we have these two fixes in, let&rsquo;s re-profile <code>pnpm install</code> for the
newer version:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">$ npx --package=pnpm@7.30.4 -c &#39;pprof-it $(which pnpm) install&#39;
</span></span><span class="line"><span class="cl"># ...
</span></span><span class="line"><span class="cl">Done in 13.6s
</span></span></code></pre></div><p>Immediately, the difference is evident. There&rsquo;s no longer a huge delay before I
get the cycle warning. The whole thing now takes <em>13.6 seconds</em>. That&rsquo;s a huge
improvement! It&rsquo;s outlandishly good to be processing 9,000+ packages in such a
short time.</p>
<p>What about the profile, though?</p>
<p><img alt="A pprof profile of the finalized code, with the two blocks (mostly) gone, and a lot of little stuff now showing. The overall execution takes about 13 seconds." loading="lazy" src="/posts/pnpm-dt-2/profile3.png#center"></p>
<p>Much different. We can see that the huge obvious blocks are gone, leaving us
with a bunch of small stuff (if two obvious chunks were &ldquo;the dream&rdquo;, a bunch of
small stuff is &ldquo;the nightmare&rdquo;). We can still see that <code>createPkgGraph</code> is still
the most obvious chunk, lending to the fact that we didn&rsquo;t fix the fact that
it&rsquo;s quadratic. But, if we fix that, that&rsquo;ll be a few more seconds saved! And,
we can profile it again, and maybe we can look into <code>sequenceGraph</code> or
<code>getAllProjects</code>, the next big chunks.</p>
<h2 id="recapping">Recapping</h2>
<p>To recap, we:</p>
<ul>
<li>Ran <code>pnpm</code> on a huge monorepo, and found it to be suspiciously slow, visibly
hanging at times.</li>
<li>Ran <code>pprof-it</code> to take a look under the hood.</li>
<li>Found a couple of big candidates for optimization.</li>
<li>Stared at some code.</li>
<li>Got lucky, addressing both problems by simply shifting some code around.</li>
<li>Made <code>pnpm</code> 4x faster! (For this super ridiculous test case, anyway.)</li>
</ul>
<p>I hope this was informative. Profiling is an excellent trick to have in your
toolbox. Sometimes, you&rsquo;ll be unlucky and it won&rsquo;t show you much. But, when you
<em>do</em> find something, it&rsquo;s worth having spent a few minutes trying it out.</p>
<p>In case you&rsquo;re curious what else we&rsquo;ve (me and the TypeScript team) have been
able to find, check out these PRs and issues:</p>
<ul>
<li>A <a href="https://github.com/microsoft/TypeScript/pull/53346">performance boost</a> from
avoiding the calculation of all properties of unions / intersections where all
we wanted to know is if any type matches a condition.</li>
<li>A <a href="https://github.com/microsoft/TypeScript/pull/53358">performance boost</a> by
discovering that a computation was not being cached.</li>
<li>A
<a href="https://github.com/microsoft/TypeScript/issues/52345">performance regression</a>
I (unwittingly) introduced in TypeScript&rsquo;s string template literals when used
with intersections, with two PRs
(<a href="https://github.com/microsoft/TypeScript/pull/53406">#53406</a> and
<a href="https://github.com/microsoft/TypeScript/pull/53413">#53413</a>) attempting to
address it.</li>
<li>A <a href="https://github.com/microsoft/TypeScript/pull/52382">performance boost</a> in
TypeScript 5.0, where I identified that we weren&rsquo;t reusing our &ldquo;printers&rdquo; as
much as we could have, saving a few percent (and even more in some projects).</li>
<li>An <a href="https://github.com/microsoft/TypeScript/pull/44100">older PR</a> where
<code>pprof</code> had pointed out that a lot of time during a build of a TypeScript
project was being spent normalizing paths, even if the platform was UNIX-like
and the paths were already using the correct slashes.</li>
<li>A <a href="https://github.com/microsoft/pyright/pull/1774">PR I sent back</a> when I was
working on Pylance/pyright, where 50% of GC time was spent concatenating
strings.</li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Well, this used to be true, but might not be anymore. Definitely not if
you <code>git blame</code> the TypeScript repo and forget to use
<code>.git-blame-ignore-revs</code>!
<a href="https://devblogs.microsoft.com/typescript/typescripts-migration-to-modules/">Thanks, modules</a>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Okay, this is a fork of
<a href="https://www.npmjs.com/package/pprof">the original</a> released by Google, but
that one hasn&rsquo;t been updated in years, and DataDog&rsquo;s fork includes prebuilt
binaries.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>This is something I&rsquo;ve been meaning to dig into, but it turns out
to be a problem that also happens to the more typical <code>.cpuprofile</code> files
Node performance nerds may already be familiar with, so I just haven&rsquo;t
prioritized looking into it.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>What is DefinitelyTyped, and is it a monorepo?</title>
      <link>https://jakebailey.dev/posts/pnpm-dt-1/</link>
      <pubDate>Sun, 26 Mar 2023 11:26:21 -0700</pubDate>
      <guid>https://jakebailey.dev/posts/pnpm-dt-1/</guid>
      <description>Yes, it is! Kinda.</description>
      <content:encoded><![CDATA[<p>This post is a brief(-ish) overview of the current state of DefinitelyTyped and
its (potential) future. If you&rsquo;re looking for deep history, you should
definitely check out John Reilly&rsquo;s
<a href="https://johnnyreilly.com/definitely-typed-the-movie">&ldquo;Definitely Typed: The Movie&rdquo;</a>,
which tells the story of DefinitelyTyped from the start in 2012.</p>
<h2 id="what-is-definitelytyped">What is &ldquo;DefinitelyTyped&rdquo;?</h2>
<p>Generally speaking, there are two categories of packages on npm:</p>
<ol>
<li>Packages authored in TypeScript.</li>
<li>Packages authored in JavaScript.</li>
</ol>
<p>Whenever you install a package authored in TypeScript, you&rsquo;ll also get its
types.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> This means that when you import it in your own
project, you&rsquo;ll get the types that the authors wrote in their code. This is the
easy path; the hard work is done!</p>
<p>But what if the package <em>wasn&rsquo;t</em> written in TypeScript? In this situation, it
may be the case that the author hand-wrote types for their package, but most of
the time, you&rsquo;ll have to install types separately. It&rsquo;s likely you&rsquo;ve installed
a package like <code>@types/node</code> or <code>@types/react</code>.</p>
<p>Packages published under the <code>@types</code> scope come from
<a href="https://github.com/DefinitelyTyped/DefinitelyTyped">&ldquo;DefinitelyTyped&rdquo;</a>, aka
&ldquo;DT&rdquo;. DT is huge, comprising of 8,000+ packages, 6,000+ package owners, and
17,000+ unique contributors since its inception in 2012. Operating at this scale
is hard, but the infrastructure is powerful enough to automate most PRs and
automatically publish these packages every half hour.</p>
<h2 id="how-is-dt-laid-out">How is DT laid out?</h2>
<p>In the DT repo, there&rsquo;s a directory named &ldquo;types&rdquo;, and that directory has all
8,000+ packages. With so many packages, you&rsquo;d expect this to be one of those
newfangled &ldquo;monorepos&rdquo; everyone&rsquo;s been talking about. And it is! Well, kinda.</p>
<p>It turns out that even though there are over 8,000 packages in the repo, there
are only about 1,200 <code>package.json</code> files. What gives? How does anything work?</p>
<p>Let&rsquo;s look a file that every package <em>does</em> have; <code>tsconfig.json</code>. Here&rsquo;s the
<code>tsconfig</code> for <code>@types/minimist</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;compilerOptions&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;module&#34;</span><span class="p">:</span> <span class="s2">&#34;commonjs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;lib&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;es6&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;noImplicitAny&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;noImplicitThis&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;strictNullChecks&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;strictFunctionTypes&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;baseUrl&#34;</span><span class="p">:</span> <span class="s2">&#34;../&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;typeRoots&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;../&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;types&#34;</span><span class="p">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;noEmit&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;forceConsistentCasingInFileNames&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;files&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;index.d.ts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;minimist-tests.ts&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Pretty standard stuff, but this is the critical subset:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;compilerOptions&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;baseUrl&#34;</span><span class="p">:</span> <span class="s2">&#34;../&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;typeRoots&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;../&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;types&#34;</span><span class="p">:</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>What does this do?</p>
<ul>
<li><a href="https://www.typescriptlang.org/tsconfig#baseUrl"><code>baseUrl</code></a> defines a path
where TypeScript is allowed to perform absolute lookups. So if this package
were to write <code>import _ from &quot;lodash&quot;</code>, TypeScript will look for that in the
<code>types</code> directory.</li>
<li><a href="https://www.typescriptlang.org/tsconfig#typeRoots"><code>typeRoots</code></a> tells
TypeScript to consider the <code>types</code> directory to be the <code>@types</code> directory that
would typically be in <code>node_modules</code>; now, it can find <code>@types/lodash</code> as
<code>/types/lodash</code>!</li>
<li><a href="https://www.typescriptlang.org/tsconfig#types"><code>types</code></a> configures which
<code>@types</code> packages are automatically included in the compilation. This can be
convenient for typical projects since installing <code>@types/node</code> will declare
all of Node&rsquo;s packages and ambient types. But on DT, this is a bad idea,
because we&rsquo;d pull <em>every</em> <code>@types</code> package in. Setting this to the empty array
stops this and allows us to manually pull things in with
<code>/// &lt;reference types=&quot;...&quot;&gt;</code>.</li>
</ul>
<p>The combined result is that DT works like a monorepo already, just without the
involvement of a package manager (for the most part). If a package depends on
another DT package, the publisher detects that import and automatically adds a
dependency to the final package when publishing to npm.</p>
<p>And so, DT is a monorepo, but, it also isn&rsquo;t, at least not in the way that
people have come to know <em>most</em> monorepos in the JS world.</p>
<p>Of course, there are exceptions to every rule. A small fraction (~15%) of DT
<em>do</em> have <code>package.json</code> files. This is because some packages depend on the
types of packages <em>not in DefinitelyTyped</em>. This makes sense; a lot of packages
are now written in TypeScript directly, and so publish their types directly,
without involving DT. If a package typed on DT depends on a package that already
has types, then the DT types will likely need types from that dependency as
well.</p>
<h2 id="whats-the-problem">What&rsquo;s the problem?</h2>
<p>It turns out that we&rsquo;ve recently felt the need to change the status quo, for at
least two reasons.</p>
<p>Firstly, since each package with a <code>package.json</code> needs its own external
dependencies, we need to run <code>npm install</code>. But, we&rsquo;re not a monorepo! This
turns into over <em>30 minutes</em> of just looping over every folder with a
<code>package.json</code> and running <code>npm install</code>. Recently (as of writing), we&rsquo;ve had
issues with the install step randomly timing out. It&rsquo;s really frustrating for
the TypeScript team as we test all of DefinitelyTyped on most type checking
changes, just to make sure we don&rsquo;t break anyone (or, only break things in
<em>desirable</em> ways).</p>
<p>Secondly, you may remember that the <code>tsconfig.json</code> from earlier set
<code>&quot;module&quot;: &quot;commonjs&quot;</code>. This is the <em>only</em> valid configuration on DT and it has
worked for a very long time. But as more and more packages start using features
like ESM and export maps, DT needs to be able to support those features. And it
does! Mostly. The <code>&quot;module&quot;: &quot;commonjs&quot;</code> lie can be worked around for the most
part, but DT <em>should</em> really be set to <code>&quot;moduleResolution&quot;: &quot;node16&quot;</code> and then
actually <em>test</em> that the packages and their dependencies and dependants actually
work in that more modern mode.</p>
<p>A solution to both of these problems is to turn DefinitelyTyped into a monorepo
more like what other major projects are doing, meaning:</p>
<ul>
<li>Add a <code>package.json</code> to every DT package.</li>
<li>Explicitly declare all dependencies, even those within the repo.</li>
<li>Let a package manager or monorepo tool link the projects in <code>node_modules</code>.</li>
<li>Install everything at once.</li>
<li>Drop <code>baseUrl</code> and <code>typeRoots</code> out of every <code>tsconfig.json</code>.</li>
</ul>
<p>This (theoretically) gets us a much faster install time, as well as getting us a
final state on disk that matches what downstream users see, enabling packages to
start making use of <code>&quot;moduleResolution&quot;: &quot;node16&quot;</code>.</p>
<h2 id="what-next">What next?</h2>
<p>This is a cool idea in theory, but to make it real, we have to make some
choices. Specifically, the tooling. There are some unique restrictions which
make this choice complicated:</p>
<ul>
<li>The tool has to be handle the 8,000+ DT packages and their external
dependencies.</li>
<li>The tool shouldn&rsquo;t hoist anything, unless it&rsquo;s safe to do so. We don&rsquo;t want to
accidentally resolve anything.</li>
<li>The tool must be able to handle multiple versions of packages in <code>types</code> (e.g.
<code>@types/react</code> in <code>types/react</code>, <code>@types/react@v17</code> in <code>types/react/v17</code>, and
so on).</li>
<li>The tool should be fast. Right now, if you work on one package, you may not
even need to install anything. If you do install a package, you&rsquo;re only going
to pay for the cost of installing that one DT package&rsquo;s deps. If we have to
get the whole monorepo, that experience hopefully shouldn&rsquo;t suffer.</li>
<li>The tool shouldn&rsquo;t try and do anything else. We just want package linking, not
a build system. There&rsquo;s nothing to build!</li>
</ul>
<p>This set of requirements really narrows it down; at the time of writing, the
only package manager which meets these requirements is
<a href="https://pnpm.io/"><code>pnpm</code></a>. The other choices either ban packages of duplicated
names, are generally not configurable enough, or take too long to install
(though no option is likely <em>slower</em> than the 30 minute CI install). I&rsquo;m not
super surprised; <code>pnpm</code> is the default package manager of the
<a href="https://rushstack.io/"><code>rushstack</code></a> tooling and there are some pretty
ridiculously sized monorepos using it.</p>
<p>Even still, <code>pnpm</code>&rsquo;s great performance still <em>felt</em> a little slow. I noticed
that on install it&rsquo;d hang and then start printing text, implying some
performance problem. Not shocking; the number of packages it finally resolves to
is <a href="https://www.youtube.com/watch?v=SiMHTK15Pik">over 9,000</a>, and I&rsquo;d think any
tool would chug with that much work to do.</p>
<p>But, there&rsquo;s good news! By profiling <code>pnpm install</code>, I discovered that that the
performance holes are mostly just cases of
<a href="https://accidentallyquadratic.tumblr.com/">&ldquo;accidentally quadratic&rdquo;</a> code, and
therefore can be addressed.</p>
<p>And that&rsquo;s the <em>actual</em> thing I wanted to write about before I got carried away.
For details on that, check out the <a href="https://jakebailey.dev/posts/pnpm-dt-2/">next post in this series</a>.</p>
<h2 id="-package-manager-maintainers">👋 package manager maintainers</h2>
<p>There&rsquo;s no doubt in my mind that this post will eventually make its way to the
maintainers of the other package managers and monorepo tools. Understand, I
really truly do not mean anything negative in the above. I use all of your tools
and they&rsquo;re all great! My focus on <code>pnpm</code> above is due to the fact that I&rsquo;m able
to make immediate progress with it and that it also lets me demo some cool
profiling techniques I&rsquo;ve been meaning to share for a while. I have no idea how
DT will actually end up, we&rsquo;re just hurting <em>now</em> and I&rsquo;m finding this fun to
play around with.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>If the author set <code>&quot;declaration&quot;: true</code> and published them,
anyway.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
  </channel>
</rss>
