Modernizing Steem.js — Day 1: Building a Safety Net & Modern Tooling

in Steem Dev11 hours ago

Welcome to a 7-part series documenting how I modernized Steem.js into a fully isomorphic library — one that runs unchanged on Node, browsers, edge functions (Cloudflare Workers / Vercel Edge), and Deno. Over the next six days I'll walk through each engineering phase, and on day seven I'll ship the final release.

159e841d-855b-4fec-bf81-d93703f1c473.png

Today is Phase 1: the foundation — the safety net and the modern build tooling that everything else stands on.

Why start with a safety net?

Steem.js does real cryptography: it derives keys, signs transactions, and encrypts memos. If a "modernization" changes even a single byte of a signature or a serialized transaction, every existing account and transaction breaks. That's not a bug you can patch later — it's a silent disaster.

So before touching any crypto, I froze the current behavior into golden vectors.

Golden-vector regression gate

test/golden.test.js records byte-exact outputs from the existing library and asserts them forever after:

// Frozen forever — any later change must reproduce these exactly
const wifActive   = '5HsARJZiSjTxTQhjbAeZgD1KLhqUvewkfWWPAMBiCTZvhBvL1Qp';
const postingPub  = 'STM7Fbx298R8Dnk1VUbNaKxwHy24rdKsP6Z8g64mJdBAF4M35jBGr';
const signature   = '206ae22f1fcd2d7e29410d7411a07c00a20d76f94ec6a78fed045d150f44baff…';
const txHex       = 'd2042e16000080009265010005616c69636503626f620d746573742d7065726d6c696e6b102700';

They're nobody's real keys — they're synthetic test fixtures.

These cover WIFs, public keys, a canonical signature, serialized transaction hex, and an encrypted-memo roundtrip. Every single phase after this had to keep these green. It's the difference between confident refactoring and crossing your fingers.

Out with webpack 4 + Babel, in with tsup + Vitest

The old build was webpack 4 + Babel producing a CommonJS lib/ and a hand-tuned browser bundle. Modern bundlers (Vite, esbuild) expect proper ESM with an exports map — the old setup gave them neither.

The new toolchain:

  • tsup (esbuild) produces four outputs from one source:
    • dist/index.mjs — ESM
    • dist/index.js — CommonJS
    • dist/steem.min.js — minified IIFE (global steem) for <script> / CDN users
    • dist/index.d.ts — TypeScript types
  • Vitest replaces babel-node + mocha — native ESM, and it handles the mixed ESM/CJS source plus ESM-only @noble packages without ceremony.

A real exports map

{
  "type": "commonjs",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  },
  "engines": { "node": ">=18" }
}

This is what lets import steem from '@steemit/steem-js' and const steem = require('@steemit/steem-js') both resolve correctly, and what lets Vite tree-shake the package instead of choking on it.

The result

By the end of Phase 1, nothing about how you use the library changed — but the project now builds with modern tooling, ships types, and is guarded by a byte-exact regression gate. That gate made every later phase safe.

Tomorrow: Phase 2 — replacing the entire crypto stack with @noble, the riskiest and most important phase.

Links

Support Secure Steem Development

If you value proactive engineering, UX polish, and performance optimizations for the STEEM ecosystem, please consider supporting my witness: blaze.apps

🗳️ Vote Here:
Vote for blaze.apps Witness