Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/prompts/add-search-engine.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Add a new search engine

Add support for a new search engine to the SearchEngineSwitcher extension.

## Information needed before starting

Please provide the following before making any changes:

1. **Engine name** — display name (e.g. "Brave Search") and the constant name to use (e.g. `BraveSearch` / `"BRAVESEARCH"`)
2. **Search URL** — the base URL including the query parameter, e.g. `https://search.brave.com/search?q=`
3. **Icon URL** — a stable URL to a favicon/icon (SVG or PNG). Note which domain it is served from, as it may differ from the search domain and require an additional `web_accessible_resources` entry in the manifest.
4. **Form action suffix** — inspect the search results page and find what `document.forms[n].action` ends with (e.g. `/search`, `/`). Run `Array.from(document.forms).map(f => f.action)` in DevTools to find it.
5. **DOM depth** — the number of `.parentElement` calls needed from the `input[name="q"]` to reach the container element that should be wrapped. Run the following in DevTools on a results page to find it:
```js
const input = document.querySelector('input[name="q"]');
let el = input;
for (let i = 0; i < 5; i++) {
el = el?.parentElement;
console.log(i + 1, el);
}
```
6. **Does the page re-render the search bar after initial load?** — if yes, a `MutationObserver` is required to re-inject icons after they are removed. You can check by searching, watching DevTools Elements panel for mutations around the search input, or simply noticing that icons appear then disappear.
7. **Any special layout needs?** — e.g. does the search bar shrink when icons are added? Does the wrapper need `inline-flex` instead of `flex`? Does the icons container need `alignItems: center`?

## What will be changed

The following files require edits — do them all in one pass:

- `source/utils/searchEngineConfig.ts` — add exported constant, expand `SearchEngineName` union, add config entry
- `source/types/storage.ts` — import new constant, add to `searchEngineSettings` and `searchEngineOrder` in `defaultStorage`
- `source/manifest.json` — add URL pattern to `host_permissions`, `content_scripts.matches`, and `web_accessible_resources.matches` (if the icon is on a different domain, add that domain to `web_accessible_resources.matches` too)
- `source/ContentScript/index.ts` — import new constant, add `embedHtmlOn<Engine>` function, add `case` to `injectHtml`
- `source/Popup/features/searchEngines/useSearchEngineSettings.ts` — add new engine to the `useState` initial value (must stay in sync with `defaultStorage`)
- `README.md` — add the new engine to the supported search engines list

## MutationObserver guidance

Only add a `MutationObserver` if the page re-renders the search bar after load (question 6 above). If one is needed, use this pattern (see `embedHtmlOnBrave` or `embedHtmlOnEcosia` for reference):

```ts
const applyDOMChanges = () => {
// ... find form, input, container
if (searchAreaContainer.parentElement?.id !== 'searchengineswitcher-container') {
// ... inject wrapper and icons
}
};

const mutationObserver = new MutationObserver(() => {
if (!document.getElementById('searchengineswitcher-container')) {
applyDOMChanges();
}
});

applyDOMChanges();
mutationObserver.observe(document.body, {childList: true, subtree: true});
```

If no observer is needed (Google, Bing, DDG pattern), inject directly without any guard or observer.

## After implementing

Run `npm run build` to verify both Chrome and Firefox builds succeed before committing.
122 changes: 122 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# AGENTS.md

## Project Overview

SearchEngineSwitcher is a cross-browser web extension (Chrome & Firefox) that lets users quickly switch between search engines by embedding clickable icons next to the search bar on supported search engine pages.

Supported search engines: **Google**, **DuckDuckGo**, **Ecosia**, **Bing**.

The extension uses Manifest V3, is built with Vite + React + TypeScript, and targets both Chrome and Firefox from a single codebase.

## Repository Structure

```
source/ # All source code
├── manifest.json # Browser extension manifest (uses __chrome__/__firefox__ prefixes for browser-specific keys)
├── Background/index.ts # Background script / service worker — seeds default settings on install
├── ContentScript/index.ts # Content script — detects the current search engine and injects links to the others
├── Popup/ # Extension popup UI (React)
│ ├── popup.html # Popup entry HTML
│ ├── index.tsx # React root mount
│ ├── App.tsx # Main popup component (Grommet UI)
│ ├── useMediaPreference.ts # Hook for dark/light mode detection
│ └── features/searchEngines/ # Search engine toggle list components + hook
├── types/storage.ts # StorageSchema type and defaults
├── utils/
│ ├── searchEngineConfig.ts # Search engine definitions (URLs, icons, IDs)
│ └── storage.ts # Typed wrappers around browser.storage.local
└── public/assets/icons/ # Extension icons (favicon-*.png)

extension/ # Build output (git-tracked for deployment)
├── chrome/ # Chrome build artifacts
├── firefox/ # Firefox build artifacts
├── chrome.zip # Production Chrome package
└── firefox.xpi # Production Firefox package

.github/workflows/build.yml # CI: build on push/PR to main, deploy on version tags
```

## Tech Stack

- **Language:** TypeScript (strict, ESNext target)
- **UI Framework:** React 19 with Grommet component library and styled-components
- **Build Tool:** Vite 7 with custom plugins for IIFE content scripts, manifest generation, and zip packaging
- **Browser API:** `webextension-polyfill` for cross-browser compatibility
- **Linting:** ESLint 9 (flat config) with TypeScript, React, and Prettier plugins
- **Node version:** 20+ (see `.nvmrc` for exact version)

## Development

### Setup

```sh
npm install
```

### Dev (watch mode)

```sh
npm run dev:chrome # Build Chrome extension with file watching
npm run dev:firefox # Build Firefox extension with file watching
```

Then load the extension locally:
- **Chrome:** Load `extension/chrome` as an unpacked extension at `chrome://extensions`
- **Firefox:** Load `extension/firefox/manifest.json` as a temporary add-on at `about:debugging`

### Production Build

```sh
npm run build # Builds both Chrome and Firefox
npm run build:chrome # Chrome only → extension/chrome.zip
npm run build:firefox # Firefox only → extension/firefox.xpi
```

### Linting

```sh
npm run lint # Check for lint errors
npm run lint:fix # Auto-fix lint errors
```

## Architecture Notes

### Content Script (IIFE)

The content script (`source/ContentScript/index.ts`) is built as an **IIFE** bundle via a custom Vite plugin (`buildIIFEScripts` in `vite.config.ts`) because Manifest V3 content scripts injected via the manifest cannot use ES module imports. This is separate from the main Vite build.

### Manifest Generation

The manifest at `source/manifest.json` uses `__chrome__` and `__firefox__` prefixes for browser-specific keys (e.g., `__chrome__service_worker`, `__firefox__scripts`). The `vite-plugin-wext-manifest` plugin strips these prefixes to produce the correct manifest for each target browser. The version is pulled from `package.json` automatically.

### Storage

Settings are stored in `browser.storage.local`. The schema is defined in `source/types/storage.ts` and consists of:
- `searchEngineSettings` — a record of each search engine's enabled/disabled status
- `searchEngineOrder` — the display order of search engines

The background script seeds defaults on first install. The typed `getStorage`/`setStorage` helpers in `source/utils/storage.ts` always merge with defaults to handle missing keys gracefully.

### Adding a New Search Engine

1. Add the new engine constant and config entry in `source/utils/searchEngineConfig.ts` (baseUrl, iconUrl, id, displayName).
2. Add the engine to `SearchEngineName` union type and the `defaultStorage` in `source/types/storage.ts`.
3. Add the engine's URL pattern to `host_permissions` and `content_scripts.matches` in `source/manifest.json`.
4. Implement an `embedHtmlOn<Engine>()` function in `source/ContentScript/index.ts` and wire it into `injectHtml()`.

### Popup UI

The popup uses React with Grommet and automatically respects the user's dark/light mode preference. The `useSearchEngineSettings` hook loads settings from storage and provides a `toggleEngine` callback that persists changes immediately.

## CI/CD

The GitHub Actions workflow (`.github/workflows/build.yml`):
- **On push/PR to `main`:** Builds both browser extensions and uploads them as artifacts.
- **On version tags (`v*.*.*`):** Also deploys the `extension/` directory to the `extension` branch via GitHub Pages.

## Code Style

- 2-space indentation, LF line endings (see `.editorconfig`)
- ESLint flat config with TypeScript, React, and Prettier rules
- `console` statements are allowed (`no-console: off`)
- Prefer named exports for types and constants; default exports for React components and config objects
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The following search engines are supported currently:
- DuckDuckGo
- Ecosia
- Bing
- Brave Search

It contains a content_script, responsible for embedding links to the other enabled search engines when the user is browsing on one of the enabled search engines. Here's a picture of what the embedded links look like on Google, as an example. The extension adds clickable icons to the other search engines to the right of the search area.

Expand Down
Loading