npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kud/webext

v0.2.0

Published

Typed settings and plumbing for Firefox WebExtensions

Readme

webext

Typed settings and plumbing for Firefox WebExtensions.

Consuming this without a build step

The extensions this library is built for have no bundler — web-ext build just zips the source directory as-is. So there is no import "@kud/webext" resolving through node_modules at runtime; instead you vendor the built file straight into the extension repo:

cp node_modules/@kud/webext/dist/index.global.js src/vendor/webext.js

Commit src/vendor/webext.js, and list it in manifest.json before the consumer's own script — the IIFE build exposes a webext global that the following script relies on:

{
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["src/vendor/webext.js", "src/content.js"]
    }
  ]
}

webext.defineSettings(...) is then available as a global inside src/content.js.

Do the same for popup and options pages — three plain <script> tags rather than one <script type="module">. A module gets its own scope and cannot see the vendored global, so it needs its own copy of the schema, and a second copy of the schema is the thing this library exists to delete:

<script src="vendor/webext.js"></script>
<script src="settings.js"></script>
<script src="popup.js"></script>

This costs something and it isn't hidden here: you commit a vendored file, and you re-copy it by hand every time the library changes. That's the price of a no-build extension. There's no watcher and no registry resolution at runtime — just a file you copy in and keep in sync yourself.

Usage

Declare the schema once, in a classic script every context loads:

// src/settings.js
const settings = webext.defineSettings(
  { enabled: true, threshold: 30 },
  { area: "sync" },
)

A top-level const in a classic script lands in the shared global lexical scope, so settings is in scope for every later script in that context — the popup, the options page, the background page, and the content script — without an import anywhere.

Read it from a popup:

// src/popup.js
const values = await settings.get()
document.querySelector("#enabled").checked = values.enabled

Subscribe to changes from a content script:

// src/content.js
settings.onChange((values, changed) => {
  if ("enabled" in changed) toggleFeature(values.enabled)
})

Where a context needs a value before the first get() resolves — a content script rendering on load — read settings.defaults:

// src/content.js
let values = settings.defaults
settings.get().then((stored) => (values = stored))

It is deep-frozen, so a nested default cannot be mutated into something every later get() silently merges over.

defineSettings also throws on set() calls with an undeclared key, so a typo in a plain-JS content script fails loudly instead of silently writing under a name nothing reads.

When get() rejects

It does not swallow storage failures, and that is deliberate — the failure is almost never transient. In Firefox the usual cause is storage.sync throwing because the manifest has no browser_specific_settings.gecko.id; check that first. Resolving defaults instead would leave the extension running on defaults forever while the user's saved settings appear to be ignored, with nothing anywhere to point at the cause.

Where a caller genuinely wants to carry on, the defaults are one expression away — and reading them from the schema is the point, rather than restating the literal in a catch:

const values = await settings.get().catch(() => settings.defaults)

invoke — for APIs this library does not wrap

invoke(namespace, method, ...args) is the promise/callback adapter the rest of the library is built on, exported so an API with no wrapper here does not need a hand-rolled Chrome-MV2 branch at the call site:

const [tab] = await webext.invoke(webext.api.tabs, "query", {
  active: true,
  currentWindow: true,
})

It passes a callback and honours a returned thenable, so it is correct under Firefox, Chrome MV3 and Chrome MV2 alike, and it checks runtime.lastError — which a hand-rolled new Promise((resolve) => chrome.tabs.query(q, resolve)) does not, silently resolving undefined on failure.

⚠ It is for callback-or-promise async APIs, not a universal wrapper. It appends a callback argument to every call, so a synchronous API (i18n.getMessage) would receive an argument it does not expect.

Development

npm install
npm run build      # emits dist/index.js (ESM) and dist/index.global.js (IIFE)
npm run typecheck
npm test