Read this before writing code.
AGENTS.mdholds the non-negotiable engineering principles (SOLID / high cohesion & low coupling, TDD/BDD-first, root-cause fixes, scope discipline) and the architecture quick-map — those are not repeated here. This file is the concrete development spec: the commands, structure, coding style, UI/theme rules, testing mechanics, i18n, and commit/PR workflow you follow while implementing. For deep internals seedocs/ARCHITECTURE.md.
pnpm install # install deps (preinstall enforces pnpm)
pnpm run dev # dev build (source maps); load dist/ext as unpacked extension
pnpm run dev:noMap # dev build w/o source maps (incognito)
pnpm run build # production Rspack build
pnpm run pack # package the extension (requires dist/scriptcat.pem)
pnpm test # all tests (Vitest)
pnpm test -- --run path/to/file.test.ts # single test file
pnpm run coverage
pnpm run typecheck # tsc --noEmit
pnpm run test:e2e:install # install Playwright Chromium (first run only)
pnpm run test:e2e # Playwright (e2e/*.spec.ts, 1 worker)
pnpm run lint # tsc --noEmit + eslint
pnpm run lint-fix # prettier --write + tsc --noEmit + eslint --fixNo standalone format script — formatting is part of lint-fix and runs through prettier --write. Husky
pre-commit runs prettier --check and pnpm run typecheck plus ESLint for staged JS/TS files, and also runs
pnpm run test:ci when committing on main or release/*.
After pnpm run dev, load dist/ext as an unpacked extension. The browser hot-reloads page changes, but edits to manifest.json, service_worker, offscreen, or sandbox require reloading the extension.
Core entry points live in src (service_worker.ts, content.ts, inject.ts, offscreen.ts, sandbox.ts). UI pages are in src/pages, with shared UI in src/pages/components and state in src/pages/store. Reusable domain code is in src/pkg; app services are in src/app; templates are in src/template; assets and translations are in src/assets and src/locales. Workspace packages live in packages, including browser mocks and filesystem adapters. Unit tests are colocated as *.test.ts/*.test.tsx or placed in tests; E2E specs are in e2e.
@App/* → src/*, @Packages/* → packages/*, @Tests/* → tests/*
Use strict TypeScript, React JSX runtime, 2-space indentation, semicolons, double quotes, trailing commas where valid, and a 120-column Prettier width. Prefer aliases from tsconfig.json: @App/*, @Packages/*, and @Tests/*. ESLint requires type-only imports, allows _-prefixed unused variables, warns on literal JSX text, and enforces chrome.runtime.lastError checks. Use pnpm run lint-fix for mechanical fixes.
- Comments in Simplified Chinese.
- Code-review responses in Chinese.
- UI default English (global users).
- Template literals:
${i}, not${i.toString()}.
React 19 + shadcn/ui (Radix UI primitives, "new-york" style) + Tailwind CSS v4 + React Router. Pages in
src/pages/; shared primitives in src/pages/components/ui/ (config in components.json).
- Compose styles with Tailwind utility classes joined via
cn()(src/pkg/utils/cn.ts— clsx + tailwind-merge); avoid inlinestyle={{}}. Build component variants withclass-variance-authority; icons fromlucide-react. - Hover/focus visuals → CSS pseudo-classes (
hover:,focus:), not React state. State is for data/logic. - Theme (light/dark/auto) — managed by
src/pages/components/theme-provider.tsxand applied as the.darkclass ondocument.documentElement(src/pages/common.tssets the initial class before React mounts to avoid a flash). Every UI change must work in both themes:- Use the design-system CSS variables defined in
src/index.css(bg-background,text-foreground,border-border,text-primary,bg-primary-background,text-muted-foreground, …) — they auto-adapt per theme. - Use Tailwind's
dark:variant for dark-only overrides (@custom-variant darkinsrc/index.css). - No hard-coded colors.
- Use the design-system CSS variables defined in
- Design system — the full color-token reference (light/dark values), component palette, layout &
responsive patterns, motion guidance, state patterns, and a new-page recipe live in
DESIGN.md. Read it before building a new page, dialog, or block.
The TDD/BDD-first principle (write failing tests before implementation; fix code not tests) lives in
AGENTS.md→ Engineering Principles. This section is the mechanics.
Vitest + happy-dom, 850ms timeout. Chrome APIs mocked via @Packages/chrome-extension-mock (tests/vitest.setup.ts). MockMessage available for message-system tests.
- Write failing tests before implementation; co-locate
*.test.ts/*.test.tsxnext to source (or place intests). - BDD-style Chinese
describe/ittitles. Usedescribe.concurrent()/it.concurrent()where independent. - Single file:
pnpm test -- --run path/to/file.test.ts. - Playwright tests are
*.spec.tsfiles ine2e; they run with one worker and retain failure artifacts. Run targeted tests while iterating, then runpnpm run lintplus the relevant full suite before a PR.
A test earns its place by exercising our own logic and failing on a real regression. Don't write the "tests nothing" kinds below — and clean them up when you find them (delete the test; don't touch business logic):
- Tautology — asserting a constant equals its own literal definition (source
const FOO = [Type.BAR], testexpect(FOO).toEqual([Type.BAR])). - Genuine duplicate — a whole file/block near-verbatim identical to another, differing only by irrelevant suffixes.
- Redundant — when the caller's tests already cover a callee fully, skip the callee's standalone unit test.
- Pure pass-through render —
render(<Comp prop={x} />)that only assertsxshows up, with no branching / variant mapping / derived logic in the component. - Testing the mock or framework, not our code — configuring a
vi.fn()then asserting it returned what it was fed; asserting a third-party lib's or the JS language's own semantics. - Mislabeled — the test name claims a behavior the body never triggers (e.g. claims to test abort but never calls abort). Worse than no test: it gives false confidence.
- File-content assertion that belongs in a lint rule — reading a source file and grepping its text for a token/string is a mechanical convention; express it as an ESLint rule, not a unit test.
Conversely, keep these — they look thin but carry real value:
- One branch of a conditional (
showLabeldefault vs hidden, optional prop present vs absent, compact vs non-compact). - Variant → design-token mapping (CVA
tone="success"→text-success-fg) and accessibility derivation (title→aria-label). instanceof/nameguards on customErrorsubclasses, security-blocklist completeness, and similar regression guards.- The only coverage of a component / sub-component — deleting it removes coverage, not noise.
Verify each against the source before deleting. Many "looks meaningless" tests actually exercise a real branch; judging in bulk from a scan over-flags heavily. Confirm the behavior genuinely exists / is covered elsewhere before removing anything.
- Keep
tests/vitest.setup.tslightweight. Shared setup should only install global browser/chrome mocks; heavier feature helpers belong in opt-in test utilities. - For files that use one fixed UI language, prefer
initTestLanguage()fromtests/initTestLanguage.tsinbeforeAllover repeatedinitLanguage()calls inside every test. Tests that intentionally switch languages should keep explicit language setup. - Prefer shared DOM helpers such as
mockMatchMedia()fromtests/mockMatchMedia.tsover copying local browser stubs into every page test. - To spot setup/import regressions without running the full suite, run one small file and read Vitest's timing breakdown, for example:
pnpm exec vitest run --test-timeout=850 --no-coverage --reporter=verbose src/pkg/utils/url-utils.test.tsTo verify a change works end-to-end without growing the suite — drive the real built extension with a throwaway scratch script — see
VERIFICATION.md. That is lightweight verification, not the committed test suite owned by this section.
i18next, 7 locales (src/locales/: en-US, zh-CN, zh-TW, ja-JP, de-DE, vi-VN, ru-RU); extension strings in src/assets/_locales/. ESLint react/jsx-no-literals: warn enforces translation. Each locale is split by namespace into multiple *.json files (common.json, popup.json, script.json, …), re-exported via the locale's index.ts and merged in src/locales/locales.ts. defaultNS is common; keys in any other namespace need the ns: prefix (e.g. t("script:tags")). For localization, edit the relevant namespace *.json under src/locales/<locale>/; new locales must also be registered in src/locales/locales.ts.
Before translating, read docs/translation/README.md — the translation/localization guide (terminology rules + per-locale terminology-<locale>.md specs).
Do not commit secrets, local certificates, build output, coverage, Playwright reports, test results, or local .env changes.
Commits must be single-purpose and start with a gitmoji emoji — use the actual emoji character, not the :code: text form, for example git commit -m "🐛 fix template matching" or git commit -m "✨ add script filter". The leading emoji drives release changelog grouping (see the release skill), so pick the one that matches the change:
| Emoji | Use for |
|---|---|
| ✨ | New feature |
| 🐛 / 🚑 | Bug fix / urgent hotfix |
| ⚡️ | Performance improvement |
| ♻️ | Refactor / compatibility |
| 🎨 / 💄 | Code structure / UI & styling |
| 🔒 | Security |
| ⬆️ | Dependency bump |
| ✅ | Tests |
| 📄 | Docs |
| 🔧 / 👷 / 💚 | Config / CI / CI fix |
| 🔖 | Release / version bump |
Work from a feature branch or fork and open PRs against main. Chinese PR titles are preferred for changelog generation.
Use .github/pull_request_template.md (checklist + description + screenshots). Include a problem/solution summary, linked issues (close #123 / fix #123), test results, and screenshots or recordings for UI changes.
Review policy: review all modified files (including .md/.json); PR description is context only — judge from the diff. Verify every code path touched.