Skip to content

Cross-Browser Platform

Skeeditor targets Chrome, Firefox, and Safari using a shared src/ codebase. Browser-specific differences are isolated in src/platform/<browser>/ shims. The manifest is generated per-browser by WXT from wxt.config.ts — no separate overlay files are required.


Build targets

BrowserBuild commandOutput directory
Chrometask build:chromedist/chrome/
Firefoxtask build:firefoxdist/firefox/
Safaritask build:safaridist/safari/

task build is an alias for Chrome; task build:all builds every browser target.


Browser API

All extension contexts import browser from wxt/browser:

ts
import { browser } from 'wxt/browser';

WXT re-exports webextension-polyfill under this import path and automatically injects the polyfill for Chromium at build time. No manual polyfill setup is needed — you don't need to import it as the first statement, add a separate polyfill-only content script, or worry about manifest script ordering.

Entrypoints live under src/entrypoints/ and are discovered by WXT convention:

Entrypoint file/dirContext
src/entrypoints/background.tsService worker
src/entrypoints/content.tsContent script
src/entrypoints/popup/Action popup
src/entrypoints/options/Options page

Shared logic lives under src/shared/; platform-specific shims live under src/platform/<browser>/.

In unit/integration tests, wxt/browser is aliased to test/mocks/wxt-browser.ts, which proxies through globalThis.browser populated by test/mocks/browser-apis.ts.


Platform detection (src/platform/detect.ts)

Use feature detection, never navigator.userAgent. The detectPlatform() function uses API presence as the signal:

SignalBrowser
browser.runtime.getBrowserInfo is a functionFirefox
globalThis.safari?.extension is definedSafari
NeitherChrome
ts
import { platform } from '@src/platform';

if (platform.isFirefox) {
  // Firefox-specific path
}

Known API differences

Background execution model

BrowserManifest keyNotes
Chrome"service_worker": "…"Non-persistent, wakes on events
Firefox"scripts": ["…"]Non-persistent background script
Safari"service_worker": "…"Non-persistent, mirrors Chrome

Never store in-memory state between background wake cycles. Use browser.storage.local for any data that must survive the background being unloaded.

browser.identity

Not available on Firefox or Safari. Skeeditor uses browser.tabs.create for the OAuth redirect tab — this works cross-browser.

Side panel / sidebar

  • Chrome 114+: browser.sidePanel (not currently used by Skeeditor)
  • Firefox: browser.sidebarAction (different API, Firefox-only)
  • Safari: no equivalent

webRequest blocking mode

Replaced by declarativeNetRequest in Manifest V3 on Chrome and Safari. Firefox MV3 still supports webRequest blocking, but skeeditor does not use either API.

Safari limitations

  • Minimum version: macOS 14+ (Sonoma), Safari 17+
  • The extension must ship as a macOS app wrapper (Xcode project). The build:safari script handles this via xcrun safari-web-extension-converter.
  • Check Apple's Safari release notes before using any new WebExtension API.

Manifest

The manifest is generated by WXT from the manifest factory function in wxt.config.ts. Browser-specific fields are expressed with conditional spreads:

ts
manifest: ctx => ({
  permissions: ['storage', 'tabs', 'alarms'],
  host_permissions: [
    'https://bsky.app/*',
    'https://*.bsky.network/*',
    'https://docs.skeeditor.link/*',
    'https://slingshot.microcosm.blue/*',
  ],
  ...(ctx.browser === 'firefox' && {
    browser_specific_settings: {
      gecko: { id: '[email protected]', strict_min_version: '125.0' },
    },
  }),
});

WXT writes the final manifest.json to dist/<browser>/manifest.json during each build. There is no manifests/ directory or scripts/merge-manifest.ts — those belong to the previous (pre-WXT) build system.


Dev workflow

Chrome

sh
task build:watch:chrome
# In Chrome: chrome://extensions → Developer mode → Load unpacked → dist/chrome/

Firefox

sh
task build:watch:firefox
# Then either:
task webext:run:firefox
# Or: about:debugging → Load Temporary Add-on → dist/firefox/manifest.json

Safari

sh
task build:safari
task build:safari:swift
# Open the Xcode project under xcode/, build, and enable in Safari → Settings → Extensions

To run the converter manually instead:

sh
xcrun safari-web-extension-converter dist/safari \
  --project-location ./xcode \
  --app-name skeeditor \
  --bundle-identifier agency.self.skeeditor \
  --swift

To allow unsigned extensions during development: Safari → Settings → Advanced → Show features for web developers → Developer → Allow unsigned extensions.


Minimum supported versions

BrowserMinimum versionKey requirement
Chrome120MV3 service worker stability
Firefox125MV3 + Intl.Segmenter support
Safari17 (macOS 14/Sonoma)Baseline WebExtensions MV3

Released under the MIT License.