Published on

devutils: 33 browser-only dev tools and what the README overstated

devutils: 33 browser-only dev tools and what the README overstated
Authors
On this page

Pasting a JWT or a production SQL query into a random website is a bad habit. devutils is my answer: a single-page app where every tool runs in the browser tab.

It is live at devutils-one.vercel.app. The README said it works offline once loaded and that no data leaves your computer. This post checks both against the code.

Note: The numbers below come from reading the source and counting. The test figures are counts of test cases in the files, not a pass report.

The problem

Many online formatters run server-side or load third-party scripts around the textarea. For a JWT, a certificate or a Kubernetes secret, that is the wrong place to paste.

A local tool avoids the question. If the code never makes a request with your input, there is nothing to leak.

How it works

The app is React 18, TypeScript, Vite 6 and Tailwind, deployed on Vercel. vercel.json has one rule: rewrite every path to /index.html, so each tool gets a real URL.

Four step cards: pick a tool in the sidebar, its route renders, the component converts in the tab, the result appears with no network call

Tools are a typed list

Every tool is an entry in one array in src/App.tsx, built from this type:

// src/App.tsx:114-121
type Tool = {
  id: string;
  name: string;
  icon: React.ReactNode;
  component: React.ReactNode;
  url: string;
  isEnabled: boolean;
};

The router maps over that array to create one Route per tool, and / redirects to /unix-time. The sidebar lists the same array, and drag order is saved in localStorage under toolsOrder.

The other localStorage keys hold UI preferences: theme, the code language in the JSON-to-code tool, and tour progress. None store tool input.

Base64 handles UTF-8

src/utils/base64.ts wraps btoa and atob with encodeURIComponent and escape so non-ASCII text survives the round trip:

// src/utils/base64.ts
export function encodeBase64(input: string): string {
  try {
    // Encode to UTF-8, then to Base64
    return btoa(unescape(encodeURIComponent(input)));
  } catch (e) {
    throw new Error('Failed to encode to Base64.');
  }
}

Failures become a readable message instead of a raw browser exception.

Checking the claims

Three cards comparing a README claim to what the code shows: no data leaves holds, offline once loaded holds, 35+ tools gives 33 usable

No data leaves the browser: holds. I searched src/ and index.html for fetch(, XMLHttpRequest, sendBeacon and analytics snippets. The only matches were sample curl strings and generated code inside the cURL-to-code tool, plus GitHub links and Open Graph tags. No tool sends your input anywhere.

Works offline once loaded: holds, with no service worker. There is no service worker, web app manifest or PWA plugin in vite.config.ts. Once the page is loaded, nothing needs the network, so an open tab keeps working. A fresh visit with no connection will not load.

35+ tools: did not hold. src/App.tsx has 39 entries. One is the Credits page, and five set isEnabled: false: the CSS, JavaScript and HTML minifiers, Markdown preview and the color converter. That leaves 33 usable tools. PR #25 corrected the README to 33 (and PR #26 again, after a later rewrite).

Before PR #25, the README also listed a Makefile validator and a Kubernetes secret decoder. Both have components and tests, but neither is referenced in App.tsx, so no route reaches them. The disabled tools and these two components are unfinished work, parked until they work.

The decoder passes decoded values through DOMPurify with ALLOWED_TAGS: [] and ALLOWED_ATTR: [] in src/components/k8s/shared/SecretUtils.ts, which strips the tags from a decoded value instead of escaping them. A secret that contains angle brackets would show up altered, which is a bug for a decoder. Today no user can reach that code.

The results

FactValueSource
Commits97git log
First commit2025-01-26git log
Latest commit2026-10-14git log
Usable tools33isEnabled count in src/App.tsx
Test files12*.test.* under src/
Test cases166count of it( and test( calls
Stars / forks14 / 3GitHub API
LicenseMITLICENSE

Commits come in bursts: 10 in January 2025, 19 in July 2025, 14 in November 2025, 18 in August 2026 and 17 in October 2026. Twelve commits are authored by Claude and one by claude[bot].

What I fixed after writing this

On 2026-10-14 I merged one PR that fixed these:

  1. CI swallowed test failures. The workflow ran pnpm test --passWithNoTests || echo "Skipping broken unit tests", so a red suite still passed. CI no longer swallows test failures.
  2. Saved state could break the shell. JSON.parse on the toolsOrder value in Layout and on tour progress had no try/catch, so a corrupted value would break the page on load. The localStorage JSON.parse calls are now guarded.
  3. README count. PR #25 corrected the README to 33 tools (and PR #26 again, after a later rewrite).
  4. Label accessibility. Labels in TextAreaWithCopy and Base64SideBySide now point at their textareas.
  5. Two tool bugs. The CSS beautifier returned empty output, and the PHP serializer had emoji and object bugs. Both are fixed.

Still open:

  1. Finish the disabled and unrouted tools. Five components are switched off and two are never referenced, all unfinished work parked until it works.
  2. Cover more than the helpers. Tests exist for Base64, Base64SideBySide, readStoredArray, the CSS processor, the PHP serializer, TextAreaWithCopy and the debounce hook. The Makefile validator and K8s decoder are tested too, but unrouted. Most formatters and converters have none.
  3. Use or delete the debounce hook. src/utils/useDebouncedConversion.ts is a tested 300 ms debounce hook, but no tool imports it yet.
  4. Load tools lazily. The tool components are imported at the top of App.tsx, so the first load pays for every tool.

Try it at devutils-one.vercel.app, and open an issue if a tool you need is missing.

Resources