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

electron-api-mock-extension

v1.0.1

Published

Electron DevTools extension that mocks page-originated API calls.

Readme

API Mock — Electron DevTools extension

Mocks page-originated API calls from a DevTools panel inside an Electron app. Match requests by method and URL pattern, return authored fixed responses, rewrite request URLs, and inject latency.

Manifest V2, because Electron does not support Manifest V3 background service workers.

What it does

  • Fixed mock responses — author status, headers, and body; returned without touching the network
  • URL rewrite — send a matched request to a different URL instead
  • Latency injection — fixed per-rule delay, 0–300000 ms
  • Live request log — see every evaluated request and which rule applied
  • Rule sets — persisted locally, exportable and importable as JSON
  • Master switch — one toggle disables all interception

Scope

Interception happens in the page's main world by wrapping fetch and XMLHttpRequest. That defines the boundary:

| Covered | Not covered | |---|---| | Page fetch and XMLHttpRequest | Requests from a service worker | | Any URL the page requests | Main-process requests (Node http/https, Electron net) | | Pages served over http(s):// | Pages loaded from file:// (see below) | | | WebSocket traffic | | | Header manipulation of real traffic (deferred; see below) |

Header manipulation of real requests and responses is not in this version. Authored headers apply only to mock responses, which are synthesized in the page. Rewriting headers on traffic that actually reaches the network requires network-layer interception, because browser-controlled headers (Cookie, Origin, Referer, Host) cannot be set from page context.

Build

npm install
npm run build      # type-checks, then emits an unpacked extension into dist/
npm test           # unit tests

The build output is an unpacked directory. Electron cannot load packaged .crx archives.

Use as a dependency

Install, then load it from your Electron main process — the same shape as electron-chrome-extensions:

const { app, BrowserWindow, session } = require('electron');
const { loadExtension } = require('api-mock-extension');

app.whenReady().then(async () => {
  const ses = session.fromPartition('persist:main');

  // Must run on every app start: Electron installs extensions per session and
  // does not remember them across exits. Requires a persistent session.
  await loadExtension(ses);

  const win = new BrowserWindow({ webPreferences: { session: ses } });
  await win.loadURL('https://my-app.example.com');
  win.webContents.openDevTools();
});

ESM works too:

import { loadExtension, extensionPath } from 'api-mock-extension';

API

| Export | | |---|---| | loadExtension(session, opts?) | loads the extension into an Electron session | | extensionPath() | absolute path to the unpacked dist/ directory | | extensionVersion() | version from the built manifest | | manifestVersion | always 2 |

The package has no dependencies and never imports electron — you pass the session in, so it works with any Electron version.

loadExtension refuses a build that is not Manifest V2 with an explanatory error. That guard is not theoretical: Electron accepts a Manifest V3 manifest and registers it without any error, then never starts the background service worker, leaving the extension silently inert.

CLI

npx api-mock-extension info   # manifest_version, version, and path
npx api-mock-extension path   # just the path

Load into a host Electron app

Electron installs extensions per session and does not remember them across app exits, so this must run on every start. It also requires a persistent session — in-memory sessions cannot load extensions.

const { app, BrowserWindow, session } = require('electron');

// Electron <= 34 exposes session.loadExtension(). Newer versions add the
// session.extensions.loadExtension() namespace. Handle both.
async function loadUnpackedExtension(ses, dir) {
  if (ses.extensions?.loadExtension) return ses.extensions.loadExtension(dir);
  return ses.loadExtension(dir);
}

app.whenReady().then(async () => {
  const hostSession = session.fromPartition('persist:my-app');
  await loadUnpackedExtension(hostSession, '/absolute/path/to/dist');

  const window = new BrowserWindow({ webPreferences: { session: hostSession } });
  await window.loadURL('https://my-app.example.com');
  window.webContents.openDevTools();
});

Verified against Electron 33.2.0. The extension declares Manifest V2 because Electron does not support MV3 background service workers; Chromium logs an MV2 deprecation notice on load, which is expected.

Open DevTools and select the API Mock panel.

The page must be served over http, not file://

Electron does not give file:// pages content scripts or DevTools extensions. Load your app from http(s):// or a custom protocol. On a file:// URL the API Mock tab will not appear in DevTools and no interception happens — the extension loads fine, but Chromium never attaches it to the page. See electron#24011.

Try it with the bundled harness

npm run harness          # simple: build, serve the page, load dist/
npm run harness:dev      # same, plus an Extensions menu with a folder picker
npm run smoke            # headless: verify Electron accepts the manifest
npm run verify           # headless: verify the extension loads and mounts

harness:dev — loading extensions at runtime

npm run harness:dev behaves like harness but adds a menu, which is the better dev loop because Electron forgets extensions on exit and a fixed path in code means editing code to point somewhere else.

| Menu item | Shortcut | | |---|---|---| | Extensions ▸ Load Unpacked Extension… | Cmd+Shift+O | folder picker | | Extensions ▸ Reload All Extensions | Cmd+Shift+R | after a rebuild | | Extensions ▸ Reopen DevTools | Cmd+Alt+I | | | Page ▸ Reload Page | Cmd+R | |

It loads dist/ on start, remembers folders you pick and restores them next run, and lists each loaded extension with its id, path, Reveal in Finder, and Remove.

Two behaviors worth knowing:

  • DevTools is closed and reopened after any extension change. A devtools_page extension registers with the DevTools frontend only when DevTools opens, so loading one while DevTools is already open leaves the panel missing — which looks identical to the extension failing.
  • A non-Manifest-V2 folder is caught with a specific message. Electron accepts a Manifest V3 extension without error and registers it, but its background service worker never starts, leaving it silently inert. The picker validates the manifest first and explains this rather than letting you hit that trap.

harness serves the test page over http://127.0.0.1 on an ephemeral port and gives you buttons for fetch, async XHR, synchronous XHR, a failing request, and a 25-request burst. The server binds to loopback only and is part of the development harness, not the extension.

verify is the useful one in CI: it asserts that background.js, injected.js, devtools.html, and devtools.js all load, that the panel mounts a healthy UI, and that nothing logs an error.

Detecting whether mocking is active

The proxy publishes a marker on the page, so you can check from any console:

window.__API_MOCK__   // { version, origin } when installed, undefined otherwise

Content-Security-Policy

A strict CSP on your page does not block this extension. Verified against default-src 'self'; script-src 'self' with no chrome-extension: allowance: the page-world proxy still injects and intercepts, because Chromium exempts extension-injected resources from the page's policy. You do not need to relax your CSP to use this tool.

The harness serves that same strict policy, so the behavior is exercised on every npm run verify.

Harness files use the .cjs extension because this package sets "type": "module"; a .js file calling require() would be parsed as ESM and fail to load.

Writing rules

A rule matches on HTTP method + URL pattern.

  • * matches zero or more characters, including /
  • The pattern must match the whole URL, not a prefix
  • Scheme and host compare case-insensitively; path and query are case-sensitive

| Pattern | Matches | |---|---| | https://api.example.com/* | any path on that host | | */api/users | that exact path on any host | | https://api.example.com/users* | /users, and /users?id=1 | | * | everything |

Behavior worth knowing

  • First match wins. Rules are ordered, and exactly one applies per request. Reorder with the arrows in the rule list.
  • A mock response beats a URL rewrite. If a rule has both, the mock is returned and no network request is made. The editor flags this.
  • A rewritten URL is not re-matched. It is sent as-is, so rules cannot chain (and cannot loop). Method, headers, and body carry through unchanged; only the URL changes.
  • Latency delays delivery. The request is sent immediately; the response is held.
  • Synchronous XHR gets mocks but not latency, since delaying it would freeze the page.
  • Content type comes from the Content-Type header row, which drives the body editor mode and JSON validation.
  • The master switch defaults to off on first install, then remembers your choice.

Limits

| Thing | Limit | |---|---| | Rule name | 1–100 chars | | Mock status | 100–599 | | Mock headers | 50 | | Mock body | 1 MiB | | Latency | 0–300000 ms | | Log entries retained | 1000 | | Captured request body | 64 KiB, then truncated |

Response bodies are not captured in the log. Mocked bodies are already in the rule; real ones are in the Network panel.

Architecture

Four surfaces, because each runs in a different execution context:

DevTools panel (React)  ──write──>  chrome.storage.local  <──read/onChanged──  content script
       │                                                                              │
       └──── runtime port ────>  background relay  <──── runtime message ─────────────┘
                                 (log events only)                                    │
                                                                            postMessage
                                                                                      │
                                                                         injected page proxy
                                                                      (the only interception point)

chrome.storage.local is the source of truth for rules. The background page is a log relay and holds no rule state. src/shared/ is platform-agnostic — it imports neither chrome.* nor window — so the matcher runs identically in the page and in tests.

Design docs live in aidlc-docs/.