Safari Web Extension Conversion Steps | Generated by AI

Home 2026.05

Here’s the full step-by-step breakdown of the conversion:


1. Project Reconnaissance

First, I inspected the entire codebase to understand the extension:

Files inventoried (27 total):

Identified Chrome-only APIs: | API | Found In | Issue | |—|—|—| | chrome.storage.sync | settings.js (lines 19, 67) | Not supported in Safari — must be chrome.storage.local | | chrome.extension.getBackgroundPage() | popup.js (line 2) | Works in Safari ✓ | | chrome.tabs.*, chrome.windows.* | tabmanager.js | Fully supported in Safari Web Extensions ✓ | | browser_action in manifest | manifest.json | Safari supports this ✓ | | Default whitelist chrome://* | settings.js | Chrome-only scheme, invalid in Safari |


2. Source Code Fixes

Three targeted edits before running the converter:

Fix 1: chrome.storage.syncchrome.storage.local

Safari Web Extensions only support chrome.storage.local. They do not have access to Chrome’s cloud-sync infrastructure.

js/settings.js line 19:

- chrome.storage.sync.get(keys, function (items) {
+ chrome.storage.local.get(keys, function (items) {

js/settings.js line 67:

- chrome.storage.sync.set(items, fx);
+ chrome.storage.local.set(items, fx);

Why this matters: If left as sync, Safari would throw a runtime error on init(), the extension would fail silently, and no settings would load or save.

Fix 2: Default whitelist

- whiteList: ['chrome://*']
+ whiteList: []

Why: chrome://* is a Chrome-specific URL scheme. Safari uses safari-extension:// and other schemes. Having a non-matching default pattern is confusing and useless.

Fix 3: README

Rewrote the Install section to show the Safari build process instead of Chrome Web Store instructions.


3. Ran the Safari Web Extension Converter

The tool safari-web-extension-converter ships with Xcode at:

/Applications/Xcode.app/Contents/Developer/usr/bin/safari-web-extension-converter

Command:

xcrun safari-web-extension-converter safari-tabs-killer \
  --app-name "TabsKiller" \
  --bundle-identifier "com.lzw.tabskiller" \
  --macos-only \
  --force \
  --no-open \
  --copy-resources \
  --no-prompt

Flag rationale:

Flag Why
--app-name "TabsKiller" Names the macOS app wrapper
--bundle-identifier "com.lzw.tabskiller" Reverse-domain ID for code signing
--macos-only Only generate macOS target (not iOS/iPadOS)
--copy-resources Copy extension files into project (not symlink)
--force Overwrite if output dir exists
--no-open Don’t launch Xcode automatically
--no-prompt Non-interactive mode

What the converter generated:


4. Build Validation & Fix

First build failed:

error: Embedded binary's bundle identifier is not prefixed with the parent app's bundle identifier.

Root cause: The converter set different bundle identifiers with mismatched case:

Target Bundle ID
Host app com.lzw.TabsKiller
Extension com.lzw.tabskiller.Extension

Apple requires: extension bundle ID = parent bundle ID + suffix (e.g. com.lzw.TabsKiller.Extension)

Fix: Patched project.pbxproj to align case:

com.lzw.tabskiller.Extension  →  com.lzw.TabsKiller.Extension

Second build: BUILD SUCCEEDED


5. Final Project Structure

TabsKiller/
├── .gitignore                            # Added (Xcode build artifacts)
├── TabsKiller.xcodeproj/                 # Xcode project
├── TabsKiller/                           # macOS host app (Swift)
│   ├── AppDelegate.swift
│   ├── ViewController.swift
│   ├── Main.storyboard
│   ├── Assets.xcassets/                  # App icons
│   ├── Info.plist
│   └── Resources/                        # App shell resources
└── TabsKiller Extension/                 # The actual extension
    ├── SafariWebExtensionHandler.swift   # Native ↔ JS bridge
    ├── Info.plist                        # Safari extension registration
    └── Resources/                        # Ported extension code
        ├── manifest.json                 # Unchanged (works as-is)
        ├── background.html / background.js
        ├── popup.html / popup.js
        ├── js/                           # tabmanager.js, settings.js (patched)
        ├── css/                          # popup.css
        └── img/                          # icons

6. How Safari Web Extensions Work Here (Architecture)

┌──────────────────────────────────────────────────────────┐
│  TabsKiller.app (macOS native app, Cocoa)               │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │  TabsKiller Extension.appex (Safari plugin)      │   │
│  │  ┌────────────────────────────────────────────┐  │   │
│  │  │  SafariWebExtensionHandler.swift            │  │   │
│  │  │  (NSExtensionRequestHandling)              │  │   │
│  │  └────────────────────────────────────────────┘  │   │
│  │                                                   │   │
│  │  ┌────────────────────────────────────────────┐  │   │
│  │  │  Resources/ (Web Extension JS code)        │  │   │
│  │  │  - background.html + JS                    │  │   │
│  │  │  - popup.html + JS                          │  │   │
│  │  │  Safari loads these into WebKit context    │  │   │
│  │  └────────────────────────────────────────────┘  │   │
│  └──────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────┘
         │
         ▼
    Safari.app  ──→ Tabs Killer toolbar button
                    ──→ Background page runs continuously
                    ──→ Popup opens on click

The native macOS app is a thin wrapper — it just registers the extension with Safari on launch. All the actual tab-killing logic runs in Safari’s Web Extension JavaScript context, unchanged from the Chrome version (with the two API fixes applied).


Back Donate