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
| Browser | Build command | Output directory |
|---|---|---|
| Chrome | task build:chrome | dist/chrome/ |
| Firefox | task build:firefox | dist/firefox/ |
| Safari | task build:safari | dist/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:
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/dir | Context |
|---|---|
src/entrypoints/background.ts | Service worker |
src/entrypoints/content.ts | Content 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:
| Signal | Browser |
|---|---|
browser.runtime.getBrowserInfo is a function | Firefox |
globalThis.safari?.extension is defined | Safari |
| Neither | Chrome |
import { platform } from '@src/platform';
if (platform.isFirefox) {
// Firefox-specific path
}Known API differences
Background execution model
| Browser | Manifest key | Notes |
|---|---|---|
| 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:safariscript handles this viaxcrun 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:
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
task build:watch:chrome
# In Chrome: chrome://extensions → Developer mode → Load unpacked → dist/chrome/Firefox
task build:watch:firefox
# Then either:
task webext:run:firefox
# Or: about:debugging → Load Temporary Add-on → dist/firefox/manifest.jsonSafari
task build:safari
task build:safari:swift
# Open the Xcode project under xcode/, build, and enable in Safari → Settings → ExtensionsTo run the converter manually instead:
xcrun safari-web-extension-converter dist/safari \
--project-location ./xcode \
--app-name skeeditor \
--bundle-identifier agency.self.skeeditor \
--swiftTo allow unsigned extensions during development: Safari → Settings → Advanced → Show features for web developers → Developer → Allow unsigned extensions.
Minimum supported versions
| Browser | Minimum version | Key requirement |
|---|---|---|
| Chrome | 120 | MV3 service worker stability |
| Firefox | 125 | MV3 + Intl.Segmenter support |
| Safari | 17 (macOS 14/Sonoma) | Baseline WebExtensions MV3 |