diff --git a/CLAUDE.md b/CLAUDE.md index b2d58fc..132de3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,25 +43,35 @@ Pure TypeScript ESM library. No browser APIs, no React. Three layers: The public API is curated via `src/index.ts` (21 exports). Anything not exported is internal — never deep-import from this package. -### `@gitmarks/extension-chrome` (`packages/extension-chrome/`) +### `@gitmarks/extension-shared` (`packages/extension-shared/`) -MV3 Chrome extension. Vite + `@crxjs/vite-plugin` build. +Cross-browser source — owns all popup, options, background, and `src/lib/` modules. Both browser shells import from here via the `exports` map (`./background`, `./popup`, `./options`). Uses `browser.*` via `webextension-polyfill`. No framework — vanilla HTML+TS. -- **UI:** vanilla HTML+TS for popup and options pages. No framework. -- **Service worker** (`src/background.ts`): registers `chrome.bookmarks.*` listeners, creates the periodic poll alarm, runs initial reconciliation on cold start when stale. +- **Service worker** (`src/background.ts`): registers `browser.bookmarks.*` listeners, creates the periodic poll alarm, runs initial reconciliation on cold start when stale. - **Pure libs** (`src/lib/`): - - `settings.ts` — Zod-validated `chrome.storage.local` wrapper + - `settings.ts` — Zod-validated `browser.storage.local` wrapper - `machine-id.ts` — 8-char Crockford base32 ID, persisted - - `bookmark-factory.ts` — `{url, title, machineId, nowIso}` → `Bookmark` + - `bookmark-factory.ts` — `{url, title, machineId, nowIso, stripTrackingParams?}` → `Bookmark` - `save-flow.ts` — orchestration; on first-save 404, bootstraps with empty file then retries - `folder-path.ts` — tree node ↔ `"Research/AI"` path conversion - `id-mapping.ts` — bidirectional `{ulid: chromeNodeId}` map - - `suppression.ts` — in-memory URL TTL map (2s) to prevent loop-back - - `apply-remote.ts` — push a `BookmarksFile` state into `chrome.bookmarks` + - `suppression.ts` — in-memory URL + nodeId TTL maps (2s) to prevent loop-back + - `apply-remote.ts` — push a `BookmarksFile` state into `browser.bookmarks` - `reconcile.ts` — merge local tree and remote file by URL on cold start - - `listeners.ts` — `chrome.bookmarks.*` listeners with 500ms global debounce, batched flush + - `listeners.ts` — `browser.bookmarks.*` listeners with 500ms global debounce, batched flush + - `background-core.ts` — dependency-injected `runMaybeReconcile` and `runPollRemoteOnce` (testable orchestration extracted from the SW entry) + - `bookmarks-file.ts` — `BOOKMARKS_PATH` + `updateBookmarksOrBootstrap` shared by save-flow, listeners, reconcile -**Popup save vs. SW save** (architectural decision worth noting): the popup constructs its own `GitHubClient` and calls `saveBookmark` directly in the page context. The service worker handles `chrome.bookmarks.*` events and the poll alarm. The two paths don't talk via `chrome.runtime.sendMessage`. This split is intentional — it makes the popup save reliable (clear page lifecycle) and keeps the SW focused on event-driven work. +**Popup save vs. SW save** (architectural decision worth noting): the popup constructs its own `GitHubClient` and calls `saveBookmark` directly in the page context. The service worker handles `browser.bookmarks.*` events and the poll alarm. The two paths don't talk via `browser.runtime.sendMessage`. This split is intentional — it makes the popup save reliable (clear page lifecycle) and keeps the SW focused on event-driven work. + +### `@gitmarks/extension-chrome` and `@gitmarks/extension-firefox` (shells) + +Each is a thin browser-specific shell over `@gitmarks/extension-shared`: +- Own manifest (Chrome: TS via `@crxjs/vite-plugin defineManifest`; Firefox: literal `manifest.json` copied into `dist/` post-build by `scripts/copy-manifest.mjs`) +- Own Vite config (Chrome: `crx({manifest})` plugin; Firefox: plain multi-entry with `root: "src"` + `outDir: "../dist"`) +- Own entry files that side-effect-import from `@gitmarks/extension-shared/{background,popup,options}` +- Own HTML files (duplicated across shells because Vite needs them as build inputs — known follow-up) +- Chrome owns the Playwright e2e suite; Firefox relies on the manual smoke test in its README ## Testing diff --git a/README.md b/README.md index ce5ec5e..781684b 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ pnpm build # Just one package pnpm --filter @gitmarks/core test -pnpm --filter @gitmarks/extension-chrome test +pnpm --filter @gitmarks/extension-shared test # all extension unit tests live here pnpm --filter @gitmarks/extension-chrome e2e # Playwright + real Chromium ``` diff --git a/packages/extension-chrome/README.md b/packages/extension-chrome/README.md index 3f9348f..099df79 100644 --- a/packages/extension-chrome/README.md +++ b/packages/extension-chrome/README.md @@ -161,19 +161,20 @@ src/ ## Automated tests ```bash -# Unit tests (vitest, jsdom + chrome.* stub) -pnpm --filter @gitmarks/extension-chrome test +# Unit tests live in the shared package (vitest, jsdom + browser.* stub) +pnpm --filter @gitmarks/extension-shared test -# Browser e2e (Playwright + real Chromium with extension loaded) +# Browser e2e (Playwright + real Chromium with extension loaded) is Chrome-only pnpm --filter @gitmarks/extension-chrome e2e -# Type checking +# Type checking — both packages +pnpm --filter @gitmarks/extension-shared typecheck pnpm --filter @gitmarks/extension-chrome typecheck ``` **Coverage:** -Unit tests (97) cover the pure logic — settings, machine ID, bookmark +Unit tests (96) cover the pure logic — settings, machine ID, bookmark factory, save flow, folder path conversion, ID mapping, suppression registry (URL + node ID), apply-remote, reconciliation, the listener batch/debounce/flush algorithm, and the background-core poll/reconcile