npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@magicelk235/viaduct

v1.3.1

Published

Convert a Chrome extension into a Safari Web Extension (macOS/iOS) ready for local dev or CI/CD.

Readme

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░
░░░░░░░░░░▓▓▓▒▒▒ ░░░░░░░░░░░▒▓▒▒▒▒▒ ░░░░░░░░░░░▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒░░░░░░░░
░░░░░░░░▓▓▒▒▒▒     ░░░░░░░░▓▒▒▒▒▒     ░░░░░░░░▒▒▒▒▒▒     ▒▒▒░░░░░░    
░░░░░░░▓▒▒▒▒▒       ░░░░░░▒▒▒▒▒▒       ░░░░░░▒▒▒▒▒▒       ░░░░░░░     
░░░░░░▓▒▒▒▒▒        ░░░░░░▒▒▒▒▒        ░░░░░░▒▒▒▒▒▒       ░░░░░░      
░░░░░░▓▒▒▒▒▒        ░░░░░░▒▒▒▒▒        ░░░░░░▒▒▒▒▒▒       ░░░░░░      
░░░░░░▒▒▒▒▒▒        ░░░░░░▒▒▒▒▒        ░░░░░░▒▒▒▒▒░       ░░░░░░      
░░░░░░▒▒▒▒▒▒        ░░░░░░▒▒▒▒▒        ░░░░░░▒▒▒░░░       ░░░░░░      
░░░░░░▒▒▒▒▒▒        ░░░░░░▒▒▒▒▒        ░░░░▒░▒░░░░░       ░░░▒▒░      
▒▒▒▒▒▒▒▒▒▒▒▒        ▒▒▒▒▒░▒▒▒▒░        ▒▒▒▒▒▒░░░░░░       ▒▒▒▒▒▒      
▒▒▒▒▒▒▒▒▒░░░        ▒▒▒▒▒▒▒▒░░░        ▒▒▒▒▒▒░░░░░░       ▒▒▒▒▒▒      
▒▒▒▒▒▒▒▒░░░░        ▒▒▒▒▒▒░░░░░        ▒▒▒▒▒▒░░░░░░       ▒▒▒▒▒▒      

Viaduct

A command-line tool that converts a Google Chrome extension into a Safari Web Extension ready for local development, ad-hoc testing, or an Xcode/TestFlight build. It extracts the extension, analyzes and rewrites the manifest for Safari, injects a runtime compatibility shim for unsupported chrome.* APIs, and drives Apple's safari-web-extension-packager and xcodebuild to produce a signed app.

What it does

  • Accepts a .zip, .crx, .xpi, an unpacked extension directory, or a URL — a Chrome Web Store link or a direct .crx/.zip download link (fetched automatically). Archive type is detected by magic bytes, so a mislabeled file (e.g. a CRX renamed to .zip) is still handled correctly.
  • Detects MV2 vs MV3 and reports incompatibilities before converting, with a human report (CONVERSION_REPORT.md) and a machine-readable --analyze --json feed (counts, autoFixed/blocking totals, a convertible verdict matching the real conversion gate, per-issue list, removed permissions, bundle id/name).
  • Rewrites the manifest for Safari:
    • Removes Chrome-only keys (update_url, key, minimum_chrome_version).
    • Strips permissions Safari does not implement (for example tabGroups, offscreen, sidePanel, debugger).
    • Converts an MV3 service worker to a non-persistent background page and forces persistent: false on MV2 backgrounds too (Safari rejects a persistent MV3 background: "a manifest_version >= 3 must be non-persistent"); strips background.type: "module" (a known cause of silent popup failures).
    • Injects browser_specific_settings.safari with a minimum version (default 15.4, override with --min-safari) and no maximum cap (an 18.* cap hides the extension on Safari 18+ and Safari 26).
    • Flags icons Safari cannot render (non-PNG), content_scripts using world: "MAIN" (Safari 18.4+ only), and a missing App Store description.
    • Flags hardcoded chrome-extension://<id>/ URLs in JS/CSS/HTML (Safari uses a different per-install origin); suggests chrome.runtime.getURL() instead.
    • Validates commands keyboard shortcuts: flags chords with no primary modifier (Safari silently drops them) and ChromeOS-only modifiers like Search that have no Safari equivalent.
    • Validates _locales and __MSG_*__ placeholders: flags an unresolvable name/description reference that would show as a literal placeholder.
    • Flags URL match patterns left in permissions under MV3 (a common migration mistake): Safari ignores them there, so they belong in host_permissions.
    • Validates the version string: flags a missing, non-numeric, or out-of-range version that Apple's CFBundleShortVersionString rejects (the build fails).
    • Auto-wires a default_popup when the action has none.
  • Generates and injects a compatibility shim into content scripts and every extension HTML page (popup, options, side panel). The shim:
    • Routes storage.sync to storage.local (Safari has no iCloud sync).
    • Stubs sidePanel, identity, notifications, tabGroups, debugger, and offscreen so module evaluation does not throw and blank the page. The sidePanel fallback opens the panel page the extension actually configured (manifest side_panel.default_path or a setOptions({path}) call), not a hardcoded guess.
    • Completes chrome.i18n (backfills detectLanguage/getUILanguage/ getAcceptLanguages without clobbering Safari's native getMessage), so code that calls the Safari-missing detectLanguage degrades to und instead of throwing.
    • Makes keyboard-shortcut management work without chrome://extensions/ shortcuts (which Safari lacks): chrome.commands.getAll() is rebuilt from the manifest so an extension's own shortcut UI is populated, and a navigation to chrome://extensions/shortcuts (or chrome://settings) is swallowed instead of opening a broken tab. Shortcuts are edited in Safari → Settings → Extensions; the analyzer warns when source hardcodes such a link.
  • Auto-sizes side-panel pages wired as the action popup so the popup is not a collapsed, tiny window.
  • Stages a clean copy that drops dev cruft (*.map, *.ts, README, lockfiles, store metadata) while preserving any file the manifest declares as a runtime asset — so a web-accessible LICENSE.txt or served .map is never dropped and 404'd in Safari.
  • Packages the extension into an Xcode project, patches bundle identifiers, and optionally builds an ad-hoc or team-signed app.
  • Verifies the bundle identifier of the COMPILED .appex, not just the project files, so the wrong extension is never registered with Safari.
  • Optionally moves the built host app into ~/Applications (no intermediate copy) and registers it with Safari (--install), so the extension persists across Safari restarts when team-signed.

Requirements

  • macOS with a full Xcode install (not just the Command Line Tools) for the packaging and build steps. xcrun safari-web-extension-packager and xcodebuild ship with Xcode.
  • Node.js 18 or newer.
  • No runtime dependencies; TypeScript is the only dev dependency.

Run the built-in toolchain check at any time:

viaduct --doctor

Install

npm install -g @magicelk235/viaduct
viaduct <input> [options]

The command is viaduct. macOS only (needs Xcode — see Requirements).

Build from source

npm install
npm run build

This compiles src/ to dist/. The CLI entry point is dist/cli.js.

Run it directly with Node:

node dist/cli.js <input> [options]

Or link it as a global command:

npm link
viaduct <input> [options]

Usage

Convert straight from a Chrome Web Store link (the CRX is downloaded for you):

viaduct "https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm"

A direct .crx or .zip download URL works too:

viaduct "https://example.com/my-extension.crx"

Analyze an extension and report issues without converting:

viaduct ./my-extension.zip --analyze

Each issue is tagged so you can tell what needs your attention from what the converter already handles:

  • [auto-fixed] — the manifest rewrite resolves it; nothing to do.
  • [shimmed] — Safari rejects the API/permission, but the injected runtime shim emulates it, so the feature keeps working. Migrate for real only if the shim's documented limitation matters to you.

The summary line and the --analyze --json payload both carry autoFixed and shimmed counts (disjoint), so CI can see at a glance how much the converter absorbed.

Stage for Safari 18+ "Add Temporary Extension" (no Xcode, fastest iteration):

viaduct ./my-extension.zip --temp-load

Then in Safari: Settings, Advanced, enable "Show features for web developers"; Settings, Developer, enable "Allow Unsigned Extensions"; Develop menu, "Add Temporary Extension", and select the staged folder. Temporary extensions must be re-added after each Safari restart.

Generate an Xcode project without building:

viaduct ./my-extension.zip --no-build

Full conversion and ad-hoc build (CI/TestFlight-safe clean copy):

viaduct ./my-extension.zip --ci

The default (without --ci) symlinks resources for live development edits; use --ci to clean-copy resources into the project.

Options

-o, --output <dir>      Output directory (default: ./<AppName>_Safari)
    --bundle-id <id>    Reverse-DNS bundle id (default: com.viaduct.<app>)
    --app-name <name>   Host app name (default: extension name)
    --min-safari <ver>  Safari strict_min_version (default: 15.4; use 18.4 for world:MAIN)
    --platforms <p>     all | macos | ios            (default: macos)
    --ci                Clean-copy resources (CI/TestFlight-safe)
    --temp-load         Stage only, for Safari 18 "Add Temporary Extension"
    --zip               Also emit a distributable .zip of the staged extension
    --clean             Wipe the output directory before staging
    --no-build          Generate the Xcode project but do not run xcodebuild
    --open-xcode        Open the generated .xcodeproj in Xcode when done
    --install           Install the built app to ~/Applications + register w/ Safari
    --install-dir <dir> Install target directory (default: ~/Applications)
    --uninstall <name>  Remove the installed <name>.app + unregister it
    --no-safari-restart With --install, don't quit/relaunch Safari or set the toggle
    --team [<id>]       Sign with an Apple Team ID; --team auto (or plain --install)
                        auto-detects it from Xcode. Omit for ad-hoc signing.
    --no-shim           Do not generate/inject the compatibility shim
    --no-oauth-bridge   Do not wire the Safari OAuth/externally_connectable bridge
    --keep-module       Keep background.type:"module" (default strips it)
    --force             Convert despite blocking errors
    --strict            Treat warnings as blocking too (CI gate)
    --analyze           Analyze and report only (also previews the manifest rewrites)
    --json              With --analyze, print a machine-readable JSON report
    --report <file>     With --analyze, also write the report to <file> (.json if --json, else Markdown)
    --doctor            Verify xcrun/packager/xcodebuild availability
-q, --quiet             Suppress progress messages (warnings/errors still print)
-v, --verbose           Verbose output
-h, --help              Show this help
    --version           Print the viaduct version and exit

Installing a built app

Let the tool install for you. It moves the built app into ~/Applications (no duplicate copy left behind), registers it with LaunchServices, and launches it once so Safari registers the extension:

viaduct ./my-extension.zip --install

Then enable the extension in Safari, Settings, Extensions.

To remove a previously installed app, unregister it from LaunchServices and delete it from the install directory:

viaduct --uninstall <AppName>                       # ~/Applications
viaduct --uninstall <AppName> --install-dir <dir>   # custom directory

Persisting across Safari restarts (team signing)

How the extension persists depends on how it was signed:

  • Ad-hoc (no --team): Safari only loads it while "Allow Unsigned Extensions" (Develop menu) is on, and that setting resets every time Safari restarts. With --install the tool sets the toggle and bounces Safari for you; pass --no-safari-restart to skip that.
  • Team-signed (--team): signed with a real Apple Developer certificate, so Safari loads it without the unsigned toggle and it survives quitting Safari.

--team auto (or plain --install) auto-detects your Team ID from Xcode, so you do not need to know or type it:

viaduct ./my-extension.zip --install            # auto-detects the team
viaduct ./my-extension.zip --install --team auto # same, explicit
viaduct ./my-extension.zip --install --team V8K8L3ZSD5  # exact id

Auto-detection reads the team cached by Xcode (IDEProvisioningTeamByIdentifier in com.apple.dt.Xcode); it requires an Apple account signed into Xcode. If no team is found, the build falls back to ad-hoc signing.

A free personal Apple team works, but its provisioning profile expires about every 7 days — re-run the command to re-sign. A paid Developer Program account lasts about a year.

If you prefer to install manually, copy the printed app path yourself:

cp -R "<AppName>_Safari/<AppName>.app" ~/Applications/
open "~/Applications/<AppName>.app"

Limitations

  • APIs with no Safari equivalent are stubbed so the extension loads, but the underlying feature does not work. The analyzer reports each one with a suggested remediation.
  • Extensions that authenticate with chrome.identity or a hardcoded chrome-extension:// OAuth redirect cannot complete login. The OAuth client is registered on the provider's server against the original Chrome extension identity and scheme, which Safari cannot reproduce. This requires the provider to register a Safari redirect or a hosted HTTPS callback flow; it cannot be fixed by conversion alone.
  • storage.sync is mapped to storage.local; data persists but does not sync across devices.
  • Native messaging (connectNative/sendNativeMessage) has no Chrome-style host manifest or host binary in Safari — messages route to the containing macOS app. The analyzer flags it; you implement the response in the app's SafariWebExtensionHandler (beginRequest).
  • declarativeNetRequest rules with a modifyHeaders action crash Safari's WebKit rule loader, so the tool strips them (both static rulesets and dynamic updateSessionRules/updateDynamicRules calls). Header-rewriting use cases — for example a CORS bypass — are not converted; use a native-messaging proxy instead. The tool also warns when static rulesets use regexFilter (Safari supports a limited regex subset and silently drops rules it cannot compile) or when enabled rules exceed the count Safari honors (the overflow is ignored).