Skip to main content
Package your existing web extension into a native Safari app on macOS, with no separate Xcode project to maintain by hand.
Safari is supported on macOS only and requires the full Xcode app. The build → convert → xcodebuild → open pipeline covers build and dev; preview and start are not available, and there is no live reload yet.
Use --browser=safari to turn the same extension you ship to Chrome and Firefox into a Safari App Extension. Extension.js bundles your code, runs Apple’s safari-web-extension-converter, compiles the generated app with xcodebuild, and walks you through enabling it.

Requirements

Safari is macOS-only and needs the full Xcode app, not just the Command Line Tools. The converter (safari-web-extension-converter) and xcodebuild ship inside Xcode.app.
If Xcode is missing, extension build/dev --browser=safari fail fast, before bundling, with guidance instead of a late, confusing error.

What it produces

extension build --browser=safari creates, next to your project: The pipeline runs end to end: bundle → convert → xcodebuild, plus open the app → guided enable in dev (or with build --open). A plain build stops after packaging and prints the open command instead.
The app name and bundle identifier are derived from your manifest name (for example, React Sidebar Example → bundle id dev.extensionjs.React-Sidebar-Example). The project targets macOS by default.
The generated dev.extensionjs.* bundle id is a development placeholder. If you plan to distribute your app, set a bundle id you own from the first build. Changing it later makes Safari treat the extension as a brand-new identity (users lose their enable state and data).

App identity and packaging options

Both dev and build accept identity overrides (Safari targets only): The same options can live in extension.config.js (CLI flags win):
Changing the bundle id, app name, or manifest regenerates the Xcode project on the next run (see the regeneration warning below).

Building the web-extension bundle on other platforms

extension build --browser=safari works on Linux and Windows too: it produces the complete dist/safari web-extension bundle and skips the Xcode packaging step with a warning. This lets CI build the payload anywhere and a Mac (or a macOS runner) do the convert + xcodebuild part later. dev --browser=safari still requires macOS with Xcode, because a Safari dev loop without packaging has nothing to run.

Enabling the extension in Safari

Local builds are ad-hoc signed (no Apple Developer account required), so Safari needs you to opt in once. When the app opens (dev, or build --open), Extension.js prints these steps and confirms when macOS has registered the extension:
  1. Safari ▸ Settings ▸ Advanced ▸ check “Show features for web developers”.
  2. Safari ▸ Develop ▸ Allow Unsigned Extensions (this resets every time Safari restarts).
  3. Safari ▸ Settings ▸ Extensions ▸ turn on your extension.
“Allow Unsigned Extensions” resets each time you launch Safari. Re-enable it after restarting Safari during development. A signed build (Apple Developer ID) avoids this step and is part of the distribution workflow.

Developing with dev

extension dev --browser=safari runs a watch loop:
  • First compile: full package: convert, build, open the app, and print the enable steps.
  • On every save: incremental xcodebuild resync (typically a couple of seconds) that updates the app’s resources from the freshly rebuilt dist/safari.
Safari has no live-reload channel like Chromium or Firefox, so after a rebuild refresh the page (or toggle the extension) in Safari to pick up changes.

When the Xcode project regenerates

The Xcode project is generated once and reused for resyncs. It is regenerated (the converter runs again) when your manifest.json changes semantically, when the app name / bundle id / platform changes, or when you pass --force-regenerate. Regeneration replaces the project: customizations made in Xcode (entitlements, capabilities, added files or targets) are discarded. Only these signing settings are preserved automatically: DEVELOPMENT_TEAM, CODE_SIGN_STYLE, and PROVISIONING_PROFILE_SPECIFIER. Extension.js warns before every regeneration of an existing project; if you customized the project in Xcode, back it up first. Delete dist/safari-xcode for a clean slate.

Debugging in Safari

Safari doesn’t support the --logs centralized logger (there is no automation channel), but Web Inspector covers every extension context:
  • Background/service worker: Safari ▸ Develop ▸ Web Extension Background Content ▸ your extension.
  • Popup/options/sidebar pages: open the surface, then right-click ▸ Inspect Element (or Develop ▸ your Mac ▸ the page).
  • Content scripts: inspect the host page, where extension script contexts appear in the Sources tab under Extension Scripts.
If a build fails, the CLI prints the tail of the failing xcrun/xcodebuild output. Set EXTENSION_AUTHOR_MODE=true to stream the full tool output live. The converter’s compatibility warnings (manifest keys Safari doesn’t support) are surfaced as warnings during packaging.

Engine target

safari has an engine alias, webkit-based, that parallels chromium-based and gecko-based:

Command support

preview and start exist to launch your extension in a running browser. Safari requires the manual, security-gated enable step above, so those commands point you to build instead.

Limitations

  • Packaging is macOS-only. The Xcode step needs macOS with the full Xcode app. (build on other platforms still produces dist/safari; dev requires macOS.)
  • No live reload. Rebuilds are fast, but you refresh in Safari to apply them.
  • Manual one-time enable. Allowing unsigned extensions and toggling the extension on are Safari security controls and cannot be automated.
  • Local builds are ad-hoc signed. Distribution signing, notarization, and App Store submission are a separate step beyond this workflow (see below).
  • macOS target only. iOS app generation is not produced by this workflow today.

After dev: shipping to the App Store

The Safari workflow above ends with an ad-hoc–signed local app. Distributing it (to the Mac App Store, or as a notarized direct download) is a separate pipeline that Extension.js plans to offer through the extension.dev platform. Until that lands, follow Apple’s own guides: Two things from the Extension.js side make that path smooth. Do them early:
  1. Set your own --bundle-id (reverse-DNS, a domain you own) from the first build. Bundle id is the extension’s identity on Apple platforms.
  2. Set your DEVELOPMENT_TEAM in Xcode once: it survives project regeneration automatically, along with CODE_SIGN_STYLE and PROVISIONING_PROFILE_SPECIFIER.

Best practices

  • Build other targets normally: Safari is additive, so keep iterating in chromium/firefox and run --browser=safari when you want to validate Safari.
  • Use browser-specific fields for true behavioral differences. Safari resolves the chromium-family prefixes (chromium:, chrome:, edge:), and safari:/webkit: prefixed keys win over them on Safari targets, for both --browser=safari and --browser=webkit-based.
  • Keep the generated project unless you need a clean slate, because regenerating discards Xcode-side customizations beyond the preserved signing settings.

Next steps