Package your existing web extension into a native Safari app on macOS — no separate Xcode project to maintain by hand. UseDocumentation Index
Fetch the complete documentation index at: https://extension.js.org/llms.txt
Use this file to discover all available pages before exploring further.
--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.
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:
| Path | What it is |
|---|---|
dist/safari | The bundled web extension (manifest, scripts, assets). |
dist/safari-xcode | The generated Xcode project (app + Safari extension targets). |
…/Release/<App>.app | The compiled, ad-hoc–signed app that hosts your extension. |
xcodebuild → open the app → guided enable.
name (for example, React Sidebar Example → bundle id dev.extensionjs.React-Sidebar-Example). The project targets macOS by default.
Enabling the extension in Safari
Local builds are ad-hoc signed (no Apple Developer account required), so Safari needs you to opt in once. After the build opens the app, Extension.js prints these steps and confirms when macOS has registered the extension:- Safari ▸ Settings ▸ Advanced ▸ check “Show features for web developers”.
- Safari ▸ Develop ▸ Allow Unsigned Extensions (this resets every time Safari restarts).
- 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
xcodebuildresync (typically a couple of seconds) that updates the app’s resources from the freshly rebuiltdist/safari.
dist/safari-xcode to regenerate it from scratch.
Engine target
safari has an engine alias, webkit-based, that parallels chromium-based and gecko-based:
Command support
| Command | Safari support |
|---|---|
build | ✅ Builds and packages the Safari app. |
dev | ✅ Watch + incremental rebuild (refresh in Safari to apply). |
preview | ❌ Not supported — Safari extensions can’t be auto-loaded into a live browser. |
start | ❌ Not supported — same reason as preview. |
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
- macOS only. Building a Safari app requires macOS with the full Xcode app.
- 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.
- macOS target only. iOS app generation is not produced by this workflow today.
Best practices
- Build other targets normally: Safari is additive — keep iterating in
chromium/firefoxand run--browser=safariwhen you want to validate Safari. - Use browser-specific fields for true behavioral differences via browser-prefixed manifest keys.
- Keep the generated project unless you need a clean slate — regenerating discards Xcode-side customizations.
Next steps
- See all supported browsers.
- Use browser-specific manifest fields.
- Review multi-platform builds.

