Meet Safari Web Extensions

Written by Kuba Suder

Description: When you create a Safari Web Extension, you can help people get common online tasks done more quickly and efficiently. We’ll show you how to build a new Safari Web Extension and host it on the App Store, as well as how to use the safari-web-extension-converter tool to migrate existing extensions from other web browsers like Chrome, Firefox, or Edge with very little effort.

Existing extension ecosystem:

  • content blockers (iOS & macOS)
  • share extensions - can run JS on the currently opened web page and return data to the extension
  • Safari app extensions on macOS

If you’re a web developer and don’t want to learn Swift to build an extension, or you have an existing extension for Chrome/Firefox/etc., you can now use the new Safari Web Extensions API.

Safari Web Extensions:

  • extensions built primarily using HTML, JS and CSS, like legacy Safari extensions
  • API compatible with other browsers (the WebExtensions standard)
  • improved user privacy controls
  • extensions are sold through the App Store
  • some WebExtensions APIs are missing, so provide feedback if you want something added

Like other extensions, Web Extensions must be packaged inside a native Mac app. Xcode 12 is required to build them.

A command-line tool is provided which wraps an exising web extension (e.g. for Chrome/Firefox) into a new app:


xcrun safari-web-extension-converter […] /path
  • lets you know if any features are not available
  • the largest icon in the manifest is used as the app icon (it’s recommended to include 512×512 and 1024×1024 icons)

To create a new extension from scratch, create a “Safari Extension App” project or add a “Safari Extension” target, and choose Type = Safari Web Extension.

Extension privacy

  • If your extension needs access to specific sites, the user will be asked for permission to run it on that site for one day or always
  • Optional permissions: you can include the URL pattern under optional_permissions key and then ask for access using browser.permissions.request(…) at the moment when you require access
  • The Safari preferences window page of your extension shows information about what kind of access was granted to the extension
  • It’s best to use the activeTab permission, which grants access to the currently open page when the user interacts with your extension in some way
  • The hostname of the extension changes every time Safari is launched in order to prevent fingerprinting
    • use browser.runtime.getURL("/path/to/resource") to create URLs to assets

Debugging

  • Access the background page through the Develop menu
  • Content scripts are visible in the Sources tab for the page
    • to run JS in the console in the context of a content script, choose the script from the pulldown menu in the corner
  • Don’t rely on code being executed when the page loads, since the extension may not have permission to run yet at this point

Communicating between components

Content script ⭢ background page:

browser.runtime.sendMessage()
browser.runtime.onMessage.addListener()

Background page ⭢ extension:

browser.runtime.sendNativeMessage()

The message is handled by the SafariWebExtensionHandler.beginRequest(with context:) delegate method (requires the nativeMessaging permission).

Extension ⭢ background page:

App ⭢ background page:

App ⭤ extension:

Shared NSUserDefaults from an app group, or NSXPCConnection

This note was originally published at mackuba.eu.

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Kuba Suder

Kuba Suder

Independent Mac & iOS developer. Sometimes freelancing, mostly working on my own stuff these days.