Skip to content

build(extension): add TARGET-driven dual builds with Firefox manifest#2000

Open
ManthanNimodiya wants to merge 2 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/extension-firefox-build
Open

build(extension): add TARGET-driven dual builds with Firefox manifest#2000
ManthanNimodiya wants to merge 2 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/extension-firefox-build

Conversation

@ManthanNimodiya

@ManthanNimodiya ManthanNimodiya commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1998, only the last commit(s) are new here, the earlier ones will disappear once the base PR merges.

TARGET=chrome|firefox vite builds → dist/chrome/dist/firefox.
Manifests move to manifests/manifest.{chrome,firefox}.json (vite copies the right one per target; a unit test keeps them in lockstep).

Firefox manifest: event-page background, options_ui, no offscreen/tabCapture, browser_specific_settings.gecko (id, min 128). New scripts: build:firefox, dev:firefox, run:firefox/lint:firefox (web-ext). e2e stays Chromium-only against dist/chrome. web-ext lint: 0 errors.

Greptile Summary

This PR adds target-specific extension builds for Chrome and Firefox. The main changes are:

  • Chrome and Firefox builds now write to separate dist subdirectories.
  • Browser manifests moved into manifests/ with a new Firefox manifest.
  • The recorder page replaces the old offscreen page name.
  • Vite config now injects a build target and copies the selected manifest.
  • Firefox helper scripts and manifest parity tests were added.

Confidence Score: 4/5

The Chrome release package and Firefox recorder startup paths can break with the new target layout.

  • The default build now creates two target folders, while Chrome packaging still treats dist as one extension root.
  • The Chrome release workflow still reads the manifest from the old public path.
  • Firefox runtime paths can still call Chrome-only offscreen APIs from the shared recorder host.

apps/chrome-extension/package.json, apps/chrome-extension/manifests/manifest.chrome.json, apps/chrome-extension/src/background/recorder-host.ts

Important Files Changed

Filename Overview
apps/chrome-extension/package.json Adds dual-target build scripts, but the default build now changes the shape consumed by Chrome release packaging.
apps/chrome-extension/manifests/manifest.chrome.json Moves the Chrome manifest into the new manifest directory while existing release automation still expects the old public path.
apps/chrome-extension/manifests/manifest.firefox.json Adds the Firefox manifest with event-page background settings and Chrome-only permissions removed.
apps/chrome-extension/src/background/recorder-host.ts Extracts recorder host lifecycle code, but the shared helper still uses Chrome-only offscreen APIs.
apps/chrome-extension/src/background/service-worker.ts Switches recorder messaging to the new host helper and uses the computed extension protocol for sender checks.
apps/chrome-extension/src/platform/capabilities.ts Adds target feature flags for Chrome and Firefox capabilities.
apps/chrome-extension/src/platform/extension-protocol.ts Adds a small helper for Chrome versus Firefox extension URL protocols.
apps/chrome-extension/src/recorder/recorder.ts Updates recorder pipeline selection for streaming uploads and resumes the audio context after creation.
apps/chrome-extension/vite.config.ts Adds target-aware output directories, manifest copying, and the renamed recorder entry.
apps/chrome-extension/vite.shared.ts Centralizes target parsing, output directory selection, and Vite define injection.

Comments Outside Diff (1)

  1. apps/chrome-extension/manifests/manifest.chrome.json, line 1 (link)

    P1 Publish Workflow Reads Old Manifest

    Moving the Chrome manifest out of public/manifest.json leaves the Chrome release workflow's version read pointing at a path that no longer exists. The package step fails before upload when it tries to read the removed public manifest.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/chrome-extension/manifests/manifest.chrome.json
    Line: 1
    
    Comment:
    **Publish Workflow Reads Old Manifest**
    
    Moving the Chrome manifest out of `public/manifest.json` leaves the Chrome release workflow's version read pointing at a path that no longer exists. The package step fails before upload when it tries to read the removed public manifest.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/chrome-extension/package.json:7
**Chrome Package Contains Both Targets**

When the Chrome publish workflow runs the default `build`, this script now creates both `dist/chrome` and `dist/firefox`. The existing package step zips `dist` as the extension root, so the upload contains target subdirectories and Firefox artifacts instead of a valid Chrome extension layout.

### Issue 2 of 3
apps/chrome-extension/manifests/manifest.chrome.json:1
**Publish Workflow Reads Old Manifest**

Moving the Chrome manifest out of `public/manifest.json` leaves the Chrome release workflow's version read pointing at a path that no longer exists. The package step fails before upload when it tries to read the removed public manifest.

### Issue 3 of 3
apps/chrome-extension/src/background/recorder-host.ts:9-16
**Firefox Reaches Offscreen APIs**

The shared background script imports this helper for both targets, but `hasRecorderHost()` and `ensureRecorderHost()` call Chrome-only offscreen APIs without checking the target. In the Firefox build, normal paths like starting a recording, probing the mic, enumerating devices, or connecting camera preview can reach `chrome.runtime.getContexts` or `chrome.offscreen.createDocument`, producing a runtime error instead of opening a Firefox recorder page.

Reviews (1): Last reviewed commit: "build(extension): add TARGET-driven dual..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

"scripts": {
"build": "rm -rf dist && vite build && vite build --config vite.content.config.ts && vite build --config vite.content-overlay.config.ts",
"dev": "rm -rf dist && (trap 'kill 0' INT TERM; vite build --watch --config vite.content.config.ts --mode development & vite build --watch --config vite.content-overlay.config.ts --mode development & vite build --watch --mode development & wait)",
"build": "pnpm build:chrome && pnpm build:firefox",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Chrome Package Contains Both Targets

When the Chrome publish workflow runs the default build, this script now creates both dist/chrome and dist/firefox. The existing package step zips dist as the extension root, so the upload contains target subdirectories and Firefox artifacts instead of a valid Chrome extension layout.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/package.json
Line: 7

Comment:
**Chrome Package Contains Both Targets**

When the Chrome publish workflow runs the default `build`, this script now creates both `dist/chrome` and `dist/firefox`. The existing package step zips `dist` as the extension root, so the upload contains target subdirectories and Firefox artifacts instead of a valid Chrome extension layout.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +9 to +16
const getRecorderContexts = async () => {
const recorderUrl = chrome.runtime.getURL(RECORDER_URL);
return new Promise<Array<{ documentUrl?: string }>>((resolve) => {
chrome.runtime.getContexts(
{
contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT],
documentUrls: [recorderUrl],
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Firefox Reaches Offscreen APIs

The shared background script imports this helper for both targets, but hasRecorderHost() and ensureRecorderHost() call Chrome-only offscreen APIs without checking the target. In the Firefox build, normal paths like starting a recording, probing the mic, enumerating devices, or connecting camera preview can reach chrome.runtime.getContexts or chrome.offscreen.createDocument, producing a runtime error instead of opening a Firefox recorder page.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/background/recorder-host.ts
Line: 9-16

Comment:
**Firefox Reaches Offscreen APIs**

The shared background script imports this helper for both targets, but `hasRecorderHost()` and `ensureRecorderHost()` call Chrome-only offscreen APIs without checking the target. In the Firefox build, normal paths like starting a recording, probing the mic, enumerating devices, or connecting camera preview can reach `chrome.runtime.getContexts` or `chrome.offscreen.createDocument`, producing a runtime error instead of opening a Firefox recorder page.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +46
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

// The two manifests are maintained by hand; these assertions keep the parts
// that must not drift (version, entry points, content scripts, resources) in
// lockstep and pin the deliberate per-browser differences.

type Manifest = {
manifest_version: number;
name: string;
short_name: string;
version: string;
homepage_url: string;
icons: Record<string, string>;
action: unknown;
background: {
service_worker?: string;
scripts?: string[];
type?: string;
};
permissions: string[];
host_permissions: string[];
content_scripts: unknown[];
web_accessible_resources: Array<{
resources: string[];
matches: string[];
use_dynamic_url?: boolean;
}>;
options_page?: string;
options_ui?: { page: string };
browser_specific_settings?: {
gecko?: {
id?: string;
strict_min_version?: string;
};
};
};

const loadManifest = (target: "chrome" | "firefox"): Manifest =>
JSON.parse(
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apps/chrome-extension is type: module, so __dirname won’t exist if Vitest executes this test as ESM. Safer to derive it from import.meta.url.

Suggested change
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
// The two manifests are maintained by hand; these assertions keep the parts
// that must not drift (version, entry points, content scripts, resources) in
// lockstep and pin the deliberate per-browser differences.
type Manifest = {
manifest_version: number;
name: string;
short_name: string;
version: string;
homepage_url: string;
icons: Record<string, string>;
action: unknown;
background: {
service_worker?: string;
scripts?: string[];
type?: string;
};
permissions: string[];
host_permissions: string[];
content_scripts: unknown[];
web_accessible_resources: Array<{
resources: string[];
matches: string[];
use_dynamic_url?: boolean;
}>;
options_page?: string;
options_ui?: { page: string };
browser_specific_settings?: {
gecko?: {
id?: string;
strict_min_version?: string;
};
};
};
const loadManifest = (target: "chrome" | "firefox"): Manifest =>
JSON.parse(
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),
);
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// The two manifests are maintained by hand; these assertions keep the parts
// that must not drift (version, entry points, content scripts, resources) in
// lockstep and pin the deliberate per-browser differences.
const __dirname = dirname(fileURLToPath(import.meta.url));
type Manifest = {
manifest_version: number;
name: string;
short_name: string;
version: string;
homepage_url: string;
icons: Record<string, string>;
action: unknown;
background: {
service_worker?: string;
scripts?: string[];
type?: string;
};
permissions: string[];
host_permissions: string[];
content_scripts: unknown[];
web_accessible_resources: Array<{
resources: string[];
matches: string[];
use_dynamic_url?: boolean;
}>;
options_page?: string;
options_ui?: { page: string };
browser_specific_settings?: {
gecko?: {
id?: string;
strict_min_version?: string;
};
};
};
const loadManifest = (target: "chrome" | "firefox"): Manifest =>
JSON.parse(
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),
);

const getRecorderContexts = async () => {
const recorderUrl = chrome.runtime.getURL(RECORDER_URL);
return new Promise<Array<{ documentUrl?: string }>>((resolve) => {
chrome.runtime.getContexts(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file assumes chrome.runtime.getContexts + chrome.offscreen.createDocument exist. Since this PR adds a Firefox build (and the manifest drops the offscreen permission), it might be worth adding a Firefox-specific implementation here (or at least a feature-detect + clearer error) so sendOffscreen(...) doesn’t end up throwing a TypeError at runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant