feat: add Firefox extension support
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / test (push) Failing after 53s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 52s

- Add Firefox as an install target with native messaging manifest support.
- Generate Firefox-specific extension packages with Gecko metadata and AMO-compatible manifest transforms.
- Keep tab group commands available in Firefox through dynamic tab group API helpers.
- Avoid Firefox linter warnings for static tab group API references and direct eval tokens.
- Add Firefox packaging and installer regression coverage.
- Bump the package and extension version to 0.15.1.
This commit is contained in:
2026-06-14 17:19:25 +02:00
parent 9df5e1bd8f
commit 9b8cefcd72
21 changed files with 225 additions and 62 deletions
+21 -10
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python3
"""Package the Chrome extension.
"""Package the browser extension.
Default builds a testing/unpacked-style archive that keeps manifest.key so the
extension ID stays stable for native messaging. ``--webstore`` writes the same
runtime files but strips ``key`` from manifest.json because the Chrome Web Store
rejects that field.
Chromium extension ID stays stable for native messaging. ``--webstore`` writes
the same runtime files but strips ``key`` from manifest.json because the Chrome
Web Store rejects that field. ``--firefox`` writes a Firefox-friendly archive
with the Gecko extension ID and without Chromium-only manifest keys.
"""
from __future__ import annotations
@@ -26,10 +27,17 @@ RUNTIME_FILES = (
)
RUNTIME_DIRS = ("icons",)
def _read_manifest(webstore: bool) -> dict:
def _read_manifest(webstore: bool, firefox: bool) -> dict:
manifest = json.loads((EXTENSION_DIR / "manifest.json").read_text(encoding="utf-8"))
if webstore:
if webstore or firefox:
manifest.pop("key", None)
if firefox:
manifest["permissions"] = [p for p in manifest.get("permissions", []) if p != "windows"]
manifest["background"] = {"scripts": ["background.js"]}
gecko = manifest.setdefault("browser_specific_settings", {}).setdefault("gecko", {})
gecko["strict_min_version"] = "140.0"
manifest.setdefault("browser_specific_settings", {}).setdefault("gecko_android", {})["strict_min_version"] = "142.0"
gecko["data_collection_permissions"] = {"required": ["none"]}
return manifest
def _copy_tree(src: Path, dst: Path) -> None:
@@ -37,10 +45,12 @@ def _copy_tree(src: Path, dst: Path) -> None:
shutil.rmtree(dst)
shutil.copytree(src, dst)
def package_extension(*, webstore: bool = False, out: Path | None = None) -> Path:
manifest = _read_manifest(webstore)
def package_extension(*, webstore: bool = False, firefox: bool = False, out: Path | None = None) -> Path:
if webstore and firefox:
raise ValueError("--webstore and --firefox are mutually exclusive")
manifest = _read_manifest(webstore, firefox)
version = manifest["version"]
suffix = "webstore" if webstore else "testing"
suffix = "firefox" if firefox else "webstore" if webstore else "testing"
out = out or DIST_DIR / f"browser-cli-extension-{suffix}-v{version}.zip"
staging = DIST_DIR / f"extension-package-{suffix}"
@@ -70,9 +80,10 @@ def package_extension(*, webstore: bool = False, out: Path | None = None) -> Pat
def main() -> None:
parser = argparse.ArgumentParser(description="Package browser-cli extension")
parser.add_argument("--webstore", action="store_true", help="strip manifest.key for Chrome Web Store upload")
parser.add_argument("--firefox", action="store_true", help="build a Firefox-friendly extension zip")
parser.add_argument("--out", type=Path, default=None, help="output zip path")
args = parser.parse_args()
print(package_extension(webstore=args.webstore, out=args.out))
print(package_extension(webstore=args.webstore, firefox=args.firefox, out=args.out))
if __name__ == "__main__":
main()