Skip to content

Commit 86aaa81

Browse files
SYM01claude
andauthored
fix: resolve multiple bugs and code quality issues (#57)
* fix: resolve multiple bugs and code quality issues - Fix critical `in` operator bug in refreshProxy() — `proxyType in ["system", "direct"]` always returned false; replaced with `["system", "direct"].includes(proxyType)` - Fix service worker crash: wrap `new URL(details.url)` in try/catch in background.ts - Fix document.body null access in preference.ts using optional chaining - Replace deepClone() JSON.parse/JSON.stringify with structuredClone() for correctness - Fix window.open() called with import.meta.url as target name; use '_blank' instead - Replace .map() with .forEach() for side-effect-only iterations (profile.ts, auth.ts) - Remove leftover debug console.log calls from ThemeSwitcher, AutoSwitchInput, background, PopupPage - Fix i18n typo: "Advance Config" → "Advanced Config" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert: deepClone back to JSON round-trip from structuredClone() structuredClone() throws a DataCloneError on JavaScript Proxy objects, including Vue reactive() and ref() wrappers. The JSON.parse/JSON.stringify approach correctly serializes through the Proxy traps, producing a plain object safe for chrome.storage. Extend the comment to document why structuredClone() is intentionally avoided here, to prevent this mistake in future. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add CLAUDE.md with codebase guidance for Claude Code Provides build/test commands, architecture overview (adapter layer, PAC-via-AST proxy engine, profile system), and critical gotchas like the deepClone JSON round-trip requirement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix an basic authentication issue --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent defedb9 commit 86aaa81

11 files changed

Lines changed: 91 additions & 21 deletions

File tree

CLAUDE.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## What is Proxyverse?
6+
7+
A Manifest V3 browser extension (Chrome, Edge, Firefox) for proxy profile management with auto-switch rules and PAC script support. It is an alternative to Proxy SwitchyOmega.
8+
9+
## Build and Test Commands
10+
11+
```bash
12+
npm run build # Type-check (vue-tsc) + build for Chrome/Edge
13+
npm run build:firefox # Type-check + build for Firefox (transforms manifest)
14+
npm run build:test # Build in test mode (no Sentry, no visualizer)
15+
npm run dev # Vite dev server
16+
npm test # Run vitest in watch mode
17+
npm run coverage # Single run with coverage report
18+
npx vitest run tests/services/proxy/profile2config.test.ts # Run a single test file
19+
```
20+
21+
CI runs `npm run coverage` on PRs to main/develop; `npm run build` + `npm run build:firefox` on pushes and tags.
22+
23+
## Architecture
24+
25+
### Three build entry points
26+
27+
The Vite config defines three entry points that produce the extension bundle:
28+
- **`index.html`** / **`popup.html`** -- Both load `src/main.ts` (Vue app). The Vue router uses hash history: `#/popup` renders `PopupPage`, `#/` renders `ConfigPage` with nested profile/preference routes.
29+
- **`src/background.ts`** -- Service worker. Wires up proxy auth, request stats, and badge indicator. No Vue, no DOM.
30+
31+
### Browser adapter layer (`src/adapters/`)
32+
33+
`BaseAdapter` defines the abstract contract for all browser APIs (storage, proxy, webRequest, tabs, i18n). Concrete implementations: `Chrome`, `Firefox`, `WebBrowser` (dev stub). A singleton `Host` is auto-detected at import time and used everywhere. This is the only layer that touches `chrome.*` or `browser.*` APIs directly.
34+
35+
### Proxy engine (`src/services/proxy/`)
36+
37+
The core complexity lives here:
38+
- **`profile2config.ts`** -- `ProfileConverter` turns a `ProxyProfile` into a `ProxyConfig` (for `chrome.proxy.settings`). For non-PAC profiles and auto-switch profiles, it **generates PAC scripts via AST** using `escodegen`/`acorn` node builders in `scriptHelper.ts`. Auto-switch profiles compose multiple sub-profiles by generating `register()` calls that build a lookup table.
39+
- **`pacSimulator.ts`** -- JS reimplementations of PAC functions (`shExpMatch`, `isInNet`) used to simulate PAC evaluation in-extension (e.g., for tab badge resolution). `isInNet` returns `UNKNOWN` when given a hostname instead of an IP (can't do DNS in extension context).
40+
- **`auth.ts`** -- Resolves proxy auth credentials by walking the profile tree (auto-switch profiles delegate to sub-profiles).
41+
42+
### Profile system (`src/services/profile.ts`)
43+
44+
Profiles are stored in `chrome.storage.local` under key `"profiles"`. Types: `ProfileSimple` (proxy/pac), `ProfilePreset` (system/direct), `ProfileAutoSwitch` (rule-based routing). System profiles `DIRECT` and `SYSTEM` have fixed IDs `"direct"` and `"system"`.
45+
46+
### Config import/export (`src/services/config/schema/`)
47+
48+
Schema definitions for importing/exporting profile configurations using `io-ts` for runtime type validation.
49+
50+
## Critical Gotcha: `deepClone()` must use JSON round-trip
51+
52+
`deepClone()` in `src/services/utils.ts` uses `JSON.parse(JSON.stringify(obj))`. **Do not replace with `structuredClone()`** -- Vue's reactive Proxy objects throw `DataCloneError` under `structuredClone()`. The JSON round-trip serializes through Vue's Proxy traps and produces plain objects safe for `chrome.storage`.
53+
54+
## Firefox build differences
55+
56+
The `vite.config.ts` `TRANSFORMER_CONFIG` rewrites `manifest.json` at build time for Firefox:
57+
- Replaces `background.service_worker` with `background.scripts` array
58+
- Removes `version_name`
59+
- Adds `browser_specific_settings.gecko`
60+
61+
## Path alias
62+
63+
`@/` maps to `src/` (configured in both `vite.config.ts` and `tsconfig.json`).
64+
65+
## i18n
66+
67+
Translation strings live in `public/_locales/{locale}/messages.json`. Translations are managed via Transifex.

public/_locales/en/messages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
"message": "Bypass List"
101101
},
102102
"config_section_advance": {
103-
"message": "Advance Config"
103+
"message": "Advanced Config"
104104
},
105105
"config_reference_bypass_list": {
106106
"message": "Learn more about bypass list"

src/background.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ProxyAuthProvider {
3434
static requests: Record<string, number> = {};
3535

3636
static onCompleted(
37-
details: WebResponseDetails | WebRequestErrorOccurredDetails
37+
details: WebResponseDetails | WebRequestErrorOccurredDetails,
3838
) {
3939
if (ProxyAuthProvider.requests[details.requestId]) {
4040
delete ProxyAuthProvider.requests[details.requestId];
@@ -43,7 +43,7 @@ class ProxyAuthProvider {
4343

4444
static onAuthRequired(
4545
details: WebAuthenticationChallengeDetails,
46-
asyncCallback?: (response: BlockingResponse) => void
46+
asyncCallback?: (response: BlockingResponse) => void,
4747
): BlockingResponse | undefined {
4848
if (!details.isProxy) {
4949
asyncCallback && asyncCallback({});
@@ -60,10 +60,10 @@ class ProxyAuthProvider {
6060
getAuthInfos(details.challenger.host, details.challenger.port).then(
6161
(authInfos) => {
6262
const auth = authInfos.at(
63-
ProxyAuthProvider.requests[details.requestId]
63+
ProxyAuthProvider.requests[details.requestId],
6464
);
6565
if (!auth) {
66-
asyncCallback && asyncCallback({ cancel: true });
66+
asyncCallback && asyncCallback({});
6767
return;
6868
}
6969

@@ -75,7 +75,7 @@ class ProxyAuthProvider {
7575
},
7676
});
7777
return;
78-
}
78+
},
7979
);
8080
}
8181
}
@@ -96,12 +96,14 @@ class StatsProvider {
9696
// this.stats.addFailedRequest(details);
9797
// TODO: update indicator
9898
const proxySetting = await getCurrentProxySetting();
99-
console.log("onResponseStarted", details);
10099
if (details.tabId > 0 && proxySetting.activeProfile) {
101-
const ret = await findProfile(
102-
proxySetting.activeProfile,
103-
new URL(details.url)
104-
);
100+
let parsedUrl: URL;
101+
try {
102+
parsedUrl = new URL(details.url);
103+
} catch {
104+
return;
105+
}
106+
const ret = await findProfile(proxySetting.activeProfile, parsedUrl);
105107

106108
StatsProvider.stats.setCurrentProfile(details.tabId, ret);
107109

src/components/configs/AutoSwitchInput.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ const getConditionInputRule = (type: AutoSwitchType): FieldRule<string> => {
102102
case "url":
103103
return {
104104
validator: async (value: string, cb: (message?: string) => void) => {
105-
console.log("test");
106105
let u;
107106
try {
108107
u = new URL(value || "");

src/components/controls/ThemeSwitcher.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const onDarkModeChanged = (newMode: DarkMode) => {
2828
};
2929
3030
const toggleDarkMode = async () => {
31-
console.log(await currentDarkMode());
3231
switch (await currentDarkMode()) {
3332
case DarkMode.Dark:
3433
onDarkModeChanged(DarkMode.Light);

src/pages/PopupPage.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,12 @@ onMounted(async () => {
4040
4141
const jumpTo = (to: RouteLocationRaw) => {
4242
const path = router.resolve(to).fullPath;
43-
window.open(`/index.html#${path}`, import.meta.url);
44-
// window.open(router.resolve(to).href, import.meta.url)
43+
window.open(`/index.html#${path}`, "_blank");
4544
};
4645
4746
// actions
4847
const setProxyByProfile = async (val: ProxyProfile) => {
4948
try {
50-
console.log(toRaw(val));
5149
await setProxy(toRaw(val));
5250
activeProfile.value = toRaw(val);
5351
} catch (e: any) {

src/services/preference.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ export async function changeDarkMode(newMode: DarkMode) {
5353

5454
switch (newMode) {
5555
case DarkMode.Dark:
56-
document && document.body.setAttribute("arco-theme", "dark");
56+
document?.body?.setAttribute("arco-theme", "dark");
5757
break;
5858
case DarkMode.Light:
59-
document && document.body.removeAttribute("arco-theme");
59+
document?.body?.removeAttribute("arco-theme");
6060
break;
6161
}
6262
}

src/services/profile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async function overwriteProfiles(profiles: ProfilesStorage) {
107107
// Deep clone to remove any Proxy objects before saving
108108
const clonedProfiles = deepClone(profiles);
109109
await Host.set(keyProfileStorage, clonedProfiles);
110-
onProfileUpdateListeners.map((cb) => cb(profiles));
110+
onProfileUpdateListeners.forEach((cb) => cb(profiles));
111111
}
112112

113113
/**

src/services/proxy/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class ProfileAuthProvider {
5050
];
5151

5252
// check if there's any matching host and port
53-
auths.map((item) => {
53+
auths.forEach((item) => {
5454
if (!item) return;
5555

5656
if (

src/services/proxy/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export async function refreshProxy() {
7575
const newProfile = await getProfile(current.activeProfile.profileID);
7676

7777
// if it's preset profiles, then do nothing
78-
if (!newProfile || current.activeProfile.proxyType in ["system", "direct"]) {
78+
if (!newProfile || ["system", "direct"].includes(current.activeProfile.proxyType)) {
7979
return;
8080
}
8181

0 commit comments

Comments
 (0)