<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Arno</title>
    <description>The latest articles on DEV Community by David Arno (@davidarno).</description>
    <link>https://dev.to/davidarno</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3882727%2F4b97b1f7-7d48-4c6f-97e5-73eaa8687928.png</url>
      <title>DEV Community: David Arno</title>
      <link>https://dev.to/davidarno</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidarno"/>
    <language>en</language>
    <item>
      <title>How I Built an Automated JS/TS Repository Analyzer in C#</title>
      <dc:creator>David Arno</dc:creator>
      <pubDate>Fri, 24 Apr 2026 14:54:43 +0000</pubDate>
      <link>https://dev.to/davidarno/how-i-built-an-automated-jsts-repository-analyzer-in-c-2oai</link>
      <guid>https://dev.to/davidarno/how-i-built-an-automated-jsts-repository-analyzer-in-c-2oai</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built the JavaScript/TypeScript analysis engine for the &lt;a href="https://dashboard.silverfishsoftware.com/documentation" rel="noopener noreferrer"&gt;Silverfish IDP&lt;/a&gt; — an Internal Developer Portal that automatically detects packaging tools, identifies component types, and extracts complete dependency graphs from repos. It handles monorepos, multiple lock file formats, and mixed JS/TS codebases—all without making assumptions about repo structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;At Silverfish Software, we're building an IDP that helps individual developers and engineering teams understand their entire codebase. But when you have hundreds of repositories spanning multiple languages, frameworks, and tools, how do you automatically make sense of it all?&lt;/p&gt;

&lt;p&gt;For JavaScript and TypeScript repos specifically, the challenge is significant: every repo is different. Some use Yarn, others npm or pnpm. Some have monorepos with nested package.json files. Some mix JavaScript and TypeScript. Some have multiple lock files checked in (a real mess). And some don't have lock files at all.&lt;/p&gt;

&lt;p&gt;I needed an analyzer that could handle all these cases automatically, with no manual configuration. No "please tell us which package manager you use" questions. Just point it at a repo and get back structured metadata about components, dependencies, and versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Detect the Packaging Tool&lt;/strong&gt;&lt;br&gt;
The naive approach: Check if yarn.lock exists → use Yarn. Check if package-lock.json exists → use npm.&lt;/p&gt;

&lt;p&gt;Reality is messier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Priority order matters
1. Check packageManager field in package.json ("yarn@4.1.0")
2. Look for lock files (yarn.lock, pnpm-lock.yaml, package-lock.json, bun.lock)
3. Check config files (.yarnrc.yml, pnpm-workspace.yaml)
4. Default to npm

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The packageManager field was the key insight—it's set by corepack and is the source of truth. If it says Yarn, it's Yarn, even if npm somehow created a lock file too.&lt;/p&gt;

&lt;p&gt;I also had to handle conflicts: I found real repos with both yarn.lock and package-lock.json checked in. My solution? Detect all of them, report the conflict, and parse only the highest-priority one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PackagingToolDetectionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DetectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repoPaths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;readFileContentAsync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Check packageManager field first&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fromPackageManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;TryDetectFromPackageManagerFieldAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromPackageManager&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fromPackageManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Check lock files&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fromLockFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;TryDetectFromLockFiles&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromLockFile&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Check config files&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fromConfigFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;TryDetectFromConfigFiles&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromConfigFile&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Default to npm&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PackagingTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Npm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: (PackagingTool.Yarn, LockFileNeedsGenerating: false) or similar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Identify Components and Their Type&lt;/strong&gt;&lt;br&gt;
Each package.json is a component. But what kind? And what does it do?&lt;/p&gt;

&lt;p&gt;I classified each one into: Package (published to npm), Library (internal or private), and determined usage: Frontend, Backend, Fullstack, or Unknown.&lt;/p&gt;

&lt;p&gt;The key was looking at dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FrontendSignals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="s"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"vue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"@angular/core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"svelte"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"react-router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"redux"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BackendSignals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"koa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mongoose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"apollo-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"prisma"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// If a package depends on react + express = fullstack&lt;/span&gt;
&lt;span class="c1"&gt;// If only react = frontend&lt;/span&gt;
&lt;span class="c1"&gt;// If only express = backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also extracted language info:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pure JS? Check for no TypeScript signals&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript? Look for typescript pkg + @types/*&lt;/span&gt;
&lt;span class="c1"&gt;// Mixed? Has flow-bin + typescript OR tsconfig.json's allowJs = true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And pulled in version constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node version: from engines.node in package.json or .nvmrc file&lt;/span&gt;
&lt;span class="c1"&gt;// TS version: from devDependencies&lt;/span&gt;
&lt;span class="c1"&gt;// ECMAScript target: from tsconfig.json compilerOptions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: A JsComponent record with all metadata attached—used by Silverfish's dashboard to display component details instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Parse Lock Files (The Hard Part)&lt;/strong&gt;&lt;br&gt;
This was the gnarly part. Four different formats, each with quirks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yarn Lock (v1 Classic)&lt;/strong&gt;&lt;br&gt;
Looks like TOML with nested dependency lists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@pkgjs/parseargs@^0.11.0"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;version "0.11.0"&lt;/span&gt;
  &lt;span class="s"&gt;resolved "https://registry.npmjs.org/..."&lt;/span&gt;
  &lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;package-json "^6.0.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a line-by-line parser. The trick: track indentation to know when you're inside a package block vs. dependency list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm package-lock.json&lt;/strong&gt;&lt;br&gt;
Flat JSON structure (v2/v3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"node_modules/lodash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4.17.21"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easier to parse with JsonDocument, but the key names have node_modules/ prefixes that need stripping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pnpm-lock.yaml&lt;/strong&gt;&lt;br&gt;
YAML with name@version keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/lodash/4.17.21&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4.17.21&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;18.2.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I treated this as mostly line-based text parsing since I didn't want to add a full YAML dependency. Works for the common cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun Lock&lt;/strong&gt;&lt;br&gt;
JSONC format with array-based entries. Least common, so I parse it but mark binary bun.lockb files as unparseable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Resolve Dependencies&lt;/strong&gt;&lt;br&gt;
Once I had a parsed lock file, I needed to extract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local dependencies (internal workspace packages like &lt;a class="mentioned-user" href="https://dev.to/company"&gt;@company&lt;/a&gt;/shared)&lt;/li&gt;
&lt;li&gt;Direct dependencies (what's explicitly in package.json)&lt;/li&gt;
&lt;li&gt;Transitive dependencies (what your dependencies need)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read package.json dependencies&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;directRanges&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ReadDirectDependencyRanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packageJsonContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// For each direct dep, look it up in the lock file&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;directRanges&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsedLock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// It's resolved to version X.Y.Z&lt;/span&gt;
        &lt;span class="n"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResolvedDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Queue it to traverse its dependencies&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Depth-first traversal to collect transitives&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryDequeue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depRange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DependencyRanges&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsedLock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;transitive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
            &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Result: Three lists of ResolvedDependency objects with exact versions and requested ranges. The Silverfish dashboard uses this to build the full dependency graph in its UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Handle Monorepos&lt;/strong&gt;&lt;br&gt;
Monorepos have multiple package.json files. The key insight: walk up the directory tree to find the root lock file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;AncestorDirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDirectoryName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So packages/web/package.json in an entria-style monorepo correctly finds the root yarn.lock instead of failing. Each workspace member gets its own component record in Silverfish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Silverfish Uses This&lt;/strong&gt;&lt;br&gt;
Once the analyzer extracts all this metadata, Silverfish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maps dependencies visually — showing which components depend on what
Flags version mismatches — when different packages pin different versions of the same library&lt;/li&gt;
&lt;li&gt;Detects tech stacks — knowing which services are frontend, which are backend, which databases they use&lt;/li&gt;
&lt;li&gt;Tracks upgrades — identifying outdated packages and planning coordinated updates&lt;/li&gt;
&lt;li&gt;Enables governance — enforcing policies like "no direct jquery dependencies" or "all frontends must use React 18+"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Format-specific parsing is worth it: I could have given up on Yarn/pnpm/Bun and only parsed npm lock files. But each format's parser is ~100-150 lines and handles real repos that exist in the wild.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Conflicts are data, not errors: Instead of failing when I find multiple lock files, I report them. That's valuable information ("why do you have both yarn.lock and package-lock.json?").&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monorepos are normal: Walking ancestor directories for lock files + detecting internal workspace packages turned out to be essential, not an edge case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version constraints matter: Storing both the requested range (^1.2.3) and resolved version (1.2.5) proved useful—you can detect upgradeable deps without breaking changes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;br&gt;
The JS/TS analyzer is one piece of Silverfish's language support. I'll building similar analyzers for Python, Go, Java, and other ecosystems. The pattern is the same: detect the package manager, identify components, resolve dependencies, extract versions.&lt;/p&gt;

&lt;p&gt;If you're trying to understand complex multi-language codebases at scale, this approach should help. The code is C# 14 with only standard library dependencies—no bloat.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://dashboard.silverfishsoftware.com" rel="noopener noreferrer"&gt;Silverfish Dashboard&lt;/a&gt; to see the analyzer in action, or hit me up if you have questions about the implementation.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>showdev</category>
      <category>development</category>
    </item>
    <item>
      <title>Silverfish IDP: A low cost, language‑first Internal Developer Portal for understanding your software estate</title>
      <dc:creator>David Arno</dc:creator>
      <pubDate>Thu, 16 Apr 2026 15:29:54 +0000</pubDate>
      <link>https://dev.to/davidarno/silverfish-idp-a-low-cost-language-first-internal-developer-portal-for-understanding-your-1oo7</link>
      <guid>https://dev.to/davidarno/silverfish-idp-a-low-cost-language-first-internal-developer-portal-for-understanding-your-1oo7</guid>
      <description>&lt;p&gt;Today I’m releasing the first public version of the Silverfish IDP, an Internal Developer Portal designed to help developers and engineering teams understand the structure of their software estates — not just at the repository level, but at the component and dependency level too.&lt;/p&gt;

&lt;p&gt;The Silverfish IDP takes a language‑first approach. It starts with repository discovery and grows into a broader platform for engineering insight and governance. The aim is to give teams clarity without without needing a complex platform rollout and without signing up to $1000s a year for the privilege.&lt;/p&gt;

&lt;p&gt;The Silverfish IDP also takes an affordability‑first approach. It is completely free to use - and will remain so - for anyone just wanting to analyse public repos. I've built the product using a range of open source tools and libraries and this is just a way of me "paying forward" for the efforts of others.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s included in the first release
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub‑based sign‑in and onboarding&lt;/li&gt;
&lt;li&gt;Workspaces and Organizations&lt;/li&gt;
&lt;li&gt;Repository mapping + scan‑state control&lt;/li&gt;
&lt;li&gt;Hierarchy naming schemes&lt;/li&gt;
&lt;li&gt;Component and dependency discovery for supported languages (the .NET languages, C#, F# &amp;amp; VB.Net, and Ruby) &lt;/li&gt;
&lt;li&gt;In‑product documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Teams often know what they’ve built, but not always how it fits together. And sometimes, they aren't really sure what they have even built. Things are created, the developer who created it moves on and well, let's be honest with each other, devs aren't always the best at documenting what they did. Silverfish aim to address both of these issues by making software structure visible and navigable and by providing information on the contents of each repo to help identify what is is and what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;Short‑term improvements include more languages, better visualisations, hierarchy support, and workspace/org membership.&lt;/p&gt;

&lt;p&gt;Medium‑term plans include quality rules, reporting, release tracking, and support for Azure DevOps, Bitbucket, and Codeberg.&lt;/p&gt;

&lt;p&gt;Longer‑term: private repo support, GitLab, Stackhero, and richer metrics including selected DORA‑style measures.&lt;/p&gt;

&lt;p&gt;It’s early days, but the foundation is solid — and I’d love feedback from the Dev.to community as it evolves.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>development</category>
      <category>security</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Silverfish IDP: A low cost, language‑first Internal Developer Portal for understanding your software estate</title>
      <dc:creator>David Arno</dc:creator>
      <pubDate>Thu, 16 Apr 2026 15:29:54 +0000</pubDate>
      <link>https://dev.to/davidarno/silverfish-idp-a-low-cost-language-first-internal-developer-portal-for-understanding-your-2hf7</link>
      <guid>https://dev.to/davidarno/silverfish-idp-a-low-cost-language-first-internal-developer-portal-for-understanding-your-2hf7</guid>
      <description>&lt;p&gt;Today I’m releasing the first public version of the Silverfish IDP, an Internal Developer Portal designed to help developers and engineering teams understand the structure of their software estates — not just at the repository level, but at the component and dependency level too.&lt;/p&gt;

&lt;p&gt;The Silverfish IDP takes a language‑first approach. It starts with repository discovery and grows into a broader platform for engineering insight and governance. The aim is to give teams clarity without without needing a complex platform rollout and without signing up to $1000s a year for the privilege.&lt;/p&gt;

&lt;p&gt;The Silverfish IDP also takes an affordability‑first approach. It is completely free to use - and will remain so - for anyone just wanting to analyse public repos. I've built the product using a range of open source tools and libraries and this is just a way of me "paying forward" for the efforts of others.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s included in the first release
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub‑based sign‑in and onboarding&lt;/li&gt;
&lt;li&gt;Workspaces and Organizations&lt;/li&gt;
&lt;li&gt;Repository mapping + scan‑state control&lt;/li&gt;
&lt;li&gt;Hierarchy naming schemes&lt;/li&gt;
&lt;li&gt;Component and dependency discovery for supported languages (the .NET languages, C#, F# &amp;amp; VB.Net, and Ruby) &lt;/li&gt;
&lt;li&gt;In‑product documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Teams often know what they’ve built, but not always how it fits together. And sometimes, they aren't really sure what they have even built. Things are created, the developer who created it moves on and well, let's be honest with each other, devs aren't always the best at documenting what they did. Silverfish aim to address both of these issues by making software structure visible and navigable and by providing information on the contents of each repo to help identify what is is and what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;Short‑term improvements include more languages, better visualisations, hierarchy support, and workspace/org membership.&lt;/p&gt;

&lt;p&gt;Medium‑term plans include quality rules, reporting, release tracking, and support for Azure DevOps, Bitbucket, and Codeberg.&lt;/p&gt;

&lt;p&gt;Longer‑term: private repo support, GitLab, Stackhero, and richer metrics including selected DORA‑style measures.&lt;/p&gt;

&lt;p&gt;It’s early days, but the foundation is solid — and I’d love feedback from the Dev.to community as it evolves.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>development</category>
      <category>security</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
