metafetch

Metafetch drops its last runtime dependency — and dodges a GPL relicense doing it

typed.js quietly went GPL-3.0 in v3. Instead of freezing on the old version, we rebuilt the typing animation in ~100 lines of our own TypeScript — and the plugin bundle shrank 18%.

Why Care?

Every dependency you ship is a little contract with a stranger. Metafetch had exactly one runtime dependency — typed.js, the library animating the playful "Consulting the oracle of structured data..." messages while your metadata fetches. In January, typed.js v3 changed its license from MIT to GPL-3.0. Nothing about the code changed; everything about what it means to bundle it did. GPL code compiled into a plugin's main.js would obligate the whole plugin — and arguably anything built on it — to carry GPL terms.

We'd rather our plugins stay frictionless to fork, study, and build on. So instead of pinning the old MIT version forever and hoping, we removed the dependency entirely.

What's New?

  • Zero runtime dependencies. typed.js is gone from package.json.

  • The animation stayed. A new in-repo Typewriter class (~100 lines, src/utils/typewriter.ts) reproduces exactly what we used: type/backspace cycling, smart backspacing to the common prefix of the next message, and the blinking cursor — which was already styled by our own CSS.

  • The bundle shrank 18% — 47.6 KB → 39.1 KB.

  • Dev toolchain refreshed while we were in there: typescript-eslint 8.65, eslint 10.7, esbuild 0.28.1 — and @types/node deliberately pinned down to ^22, because Obsidian's desktop app ships Electron 39, which runs Node 22. Types for Node 25 describe APIs the plugin's runtime doesn't have; matching the types to the real runtime means the compiler tells the truth.

How it Works

The rebuilt animation is a small recursive setTimeout chain: type characters at 25 ms, hold a full message for a second, backspace at 30 ms — but only as far as the common prefix with the next message (typed.js called this smart backspace), then type the remainder of the new one. The cursor is a sibling <span class="typed-cursor"> reusing the blink keyframes already in styles.css — the old integration ran with autoInsertCss: false, so the visuals were always ours. destroy() clears the pending timer and removes the cursor, same contract the modals already called.

TypeScript 6's stricter defaults earned their keep immediately: its no-unchecked-indexed-access rule flagged two spots where an out-of-range string index would have produced undefined — fixed before the code ever ran.

Part of the family-wide dependency-upgrade campaign: [[content-farm/context-v/plans/Dependency-Upgrades-Across-Plugin-Family]].