feat(send): browser extension that clips pages into Readest as EPUBs (#4266)

Replaces the URL-only placeholder extension with a full MV3 page-clipper
that builds a self-contained EPUB on the user's machine and uploads it
to the inbox.

- Captures the rendered DOM in a content script, then runs Readability,
  asset bundling, and EPUB build through the shared
  `convertToEpub({kind: 'page'})` pipeline inside a Chrome offscreen
  document (the SW lacks DOMParser).
- Uploads the resulting EPUB directly from the offscreen page to the
  new `POST /api/send/inbox/file` endpoint — keeps the bytes in one
  realm because `runtime.sendMessage` JSON-serialises ArrayBuffer to
  `{}` between extension contexts.
- Adds a long-lived Port + ping handshake between SW, offscreen, and
  the on-demand capture content script so neither idle-eviction nor
  load-order races can hang the popup.
- Localised popup, badge feedback, key-as-content i18n (`_('English source')`)
  with an extract script that seeds locale stubs from i18n-langs.json
  and writes a static-imports map for the runtime. All 33 locales
  fully translated.
- Server: `pages/api/send/inbox/file.ts` accepts a raw EPUB body
  (Content-Type: application/epub+zip), enforces the inbox pending cap,
  stores to the existing send-inbox R2 bucket as `kind='file'`.
  `assetBundler` now sets `credentials: 'include'` in the non-Tauri
  branch so the extension SW carries paywalled-CDN cookies.
- 47 vitest cases for the extension shell (upload, badge, auth, lazy,
  popup state machine, auth-bridge token sync) + 8 cases for the new
  server endpoint. CI's `test_web_app` invokes both via the extended
  `test:pr:web` plus a `build-browser-ext` step that catches webpack
  alias / Tauri-stub regressions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-05-22 20:45:29 +08:00
committed by GitHub
parent 9ad43aa8b2
commit f6dfd09d82
115 changed files with 5187 additions and 215 deletions
+4 -1
View File
@@ -14,17 +14,20 @@
"build-web:vinext": "dotenv -e .env.web -- vinext build",
"start-web:vinext": "dotenv -e .env.web -- vinext start",
"build-tauri": "dotenv -e .env.tauri -- next build",
"build-browser-ext": "pnpm --filter @readest/send-to-readest-extension build",
"dev-android": "tauri android build -t aarch64 -- --features devtools && adb install -r src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk",
"dev-ios": "tauri ios build -- --features devtools && ideviceinstaller install src-tauri/gen/apple/build/arm64/Readest.ipa",
"i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs",
"i18n:extract:ext": "pnpm --filter @readest/send-to-readest-extension i18n:extract",
"lint": "tsgo --noEmit && biome lint . && pnpm lint:lua",
"lint:lua": "node ../readest.koplugin/scripts/lint-koplugin.mjs",
"test:lua": "node ../readest.koplugin/scripts/test-koplugin.mjs",
"test": "dotenv -e .env -e .env.test.local -- vitest",
"test:extension": "pnpm test extensions/send-to-readest/ --run",
"test:coverage": "dotenv -e .env -e .env.test.local -- vitest run --coverage",
"test:browser": "dotenv -e .env -e .env.test.local -- vitest run --config vitest.browser.config.mts",
"test:tauri": "bash scripts/test-tauri.sh",
"test:pr:web": "pnpm test -- --watch=false && pnpm test:browser",
"test:pr:web": "pnpm test -- --watch=false && pnpm test:browser && pnpm test:extension",
"test:pr:tauri": "bash scripts/test-tauri.sh",
"test:all": "pnpm test -- --watch=false && pnpm test:browser && bash scripts/test-tauri.sh",
"test:e2e": "wdio run wdio.conf.ts",