├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── manifest.json ├── package.json ├── resources ├── switchyd128.png ├── switchyd16.png └── switchyd48.png ├── src ├── core │ ├── chrome.ts │ ├── config.ts │ ├── main.ts │ ├── pac.ts │ ├── switchyd.ts │ ├── test.ts │ └── trie.ts └── ui.ts ├── tsconfig.json ├── web-dev-server.config.js └── webpack.config.cjs /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "standard" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist 3 | node_modules 4 | 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Zizon Qiu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | switchyd 2 | ---- 3 | Chrome plugins that monitor browser traffic and auto proxy certain sites for specific reason. 4 | 5 | Install 6 | ---- 7 | [Google Chrome Web Store](http://goo.gl/Dw6qb) 8 | 9 | What it does & How it works 10 | ---- 11 | Chrome provide an api that can inspect why some request fails. 12 | What this does is detect certain failures and try to fix it automaticly, by adding sites to proxy list. 13 | 14 | currently,failures like: 15 | - net::ERR_CONNECTION_RESET 16 | - net::ERR_CONNECTION_TIMED_OUT 17 | - net::ERR_TIMED_OUT 18 | - net::ERR_SSL_PROTOCOL_ERROR 19 | 20 | will be regonized fixable. 21 | 22 | Permissions 23 | ---- 24 | the extension require permissions of: 25 | 26 | - webRequest 27 | - proxy 28 | - all_urls 29 | 30 | webRequest and all_urls are required for extension to inspect all traffics. 31 | proxy ,of course, allows the extension to access chrome the proxy functionality. 32 | 33 | License 34 | ---- 35 | MIT license. 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "switchyd", 3 | "version": "0.8.0.6", 4 | "manifest_version": 3, 5 | 6 | "background":{ 7 | "service_worker" : "dist/service-worker.js", 8 | "type": "module" 9 | }, 10 | 11 | "permissions": [ 12 | "storage", 13 | "proxy", 14 | "webRequest" 15 | ], 16 | 17 | "host_permissions": [ 18 | "http://*/*", 19 | "https://*/*", 20 | "ws://*/*", 21 | "wss://*/*" 22 | ], 23 | 24 | "options_ui": { 25 | "page": "index.html", 26 | "open_in_tab": true 27 | }, 28 | 29 | "icons":{ 30 | "16": "resources/switchyd16.png", 31 | "48": "resources/switchyd48.png", 32 | "128": "resources/switchyd128.png" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "switchyd", 3 | "devDependencies": { 4 | "@typescript-eslint/eslint-plugin": "^7.0.2", 5 | "@typescript-eslint/parser": "^7.0.2", 6 | "@web/dev-server": "^0.4.3", 7 | "@web/dev-server-esbuild": "^1.0.2", 8 | "eslint": "^8.56.0", 9 | "eslint-config-google": "^0.14.0", 10 | "eslint-config-standard": "^17.1.0", 11 | "eslint-plugin-import": "^2.29.1", 12 | "eslint-plugin-node": "^11.1.0", 13 | "eslint-plugin-promise": "^6.1.1", 14 | "eslint-plugin-n": "^16.6.2", 15 | "install": "^0.13.0", 16 | "typescript": "^5.3.3", 17 | "webpack": "^5.90.3", 18 | "webpack-cli": "^5.1.4" 19 | }, 20 | "type": "module", 21 | "dependencies": { 22 | "@lit-labs/task": "^3.1.0", 23 | "@spectrum-web-components/button": "^0.41.0", 24 | "@spectrum-web-components/field-label": "^0.41.0", 25 | "@spectrum-web-components/icons-workflow": "^0.41.0", 26 | "@spectrum-web-components/progress-circle": "^0.41.0", 27 | "@spectrum-web-components/sidenav": "^0.41.0", 28 | "@spectrum-web-components/split-view": "^0.41.0", 29 | "@spectrum-web-components/styles": "^0.41.0", 30 | "@spectrum-web-components/tabs": "^0.41.0", 31 | "@spectrum-web-components/textfield": "^0.41.0", 32 | "@spectrum-web-components/theme": "^0.41.0", 33 | "lit": "^3.1.2" 34 | }, 35 | "scripts": { 36 | "prebuild": "tsc", 37 | "build": "webpack --config webpack.config.cjs", 38 | "postbuild": "zip switchyd.zip index.html resources/*.png dist/option-ui.js dist/service-worker.js manifest.json" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/switchyd128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zizon/switchyd/c0c95554afb8a57bc805be7c71fe1f9375a5dbce/resources/switchyd128.png -------------------------------------------------------------------------------- /resources/switchyd16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zizon/switchyd/c0c95554afb8a57bc805be7c71fe1f9375a5dbce/resources/switchyd16.png -------------------------------------------------------------------------------- /resources/switchyd48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zizon/switchyd/c0c95554afb8a57bc805be7c71fe1f9375a5dbce/resources/switchyd48.png -------------------------------------------------------------------------------- /src/core/chrome.ts: -------------------------------------------------------------------------------- 1 | import { RawConfig } from './config.js' 2 | import { ProxyHook, Storage, WebHook } from './switchyd.js' 3 | 4 | declare type saveConfig = { 5 | 'switchyd.config':RawConfig 6 | } 7 | 8 | declare const chrome:{ 9 | webRequest :WebHook 10 | proxy : ProxyHook 11 | storage : { 12 | local : { 13 | set:(items:saveConfig)=>Promise, 14 | get:(key:string)=>Promise 15 | } 16 | } 17 | } 18 | 19 | export function resolveStorage ():Storage { 20 | if (chrome && chrome.storage) { 21 | return { 22 | get: async ():Promise => { 23 | const save = await chrome.storage.local.get('switchyd.config') 24 | if (save['switchyd.config']) { 25 | return save['switchyd.config'] 26 | } 27 | // try local storage 28 | const defaultConfig = { 29 | version: 3, 30 | servers: [{ 31 | accepts: [], 32 | denys: [], 33 | listen: [ 34 | 'net::ERR_CONNECTION_RESET', 35 | 'net::ERR_CONNECTION_TIMED_OUT', 36 | 'net::ERR_SSL_PROTOCOL_ERROR', 37 | 'net::ERR_TIMED_OUT' 38 | ], 39 | server: 'SOCKS5 127.0.0.1:10086' 40 | }] 41 | } 42 | await chrome.storage.local.set({ 'switchyd.config': defaultConfig }) 43 | const config = await chrome.storage.local.get('switchyd.config') 44 | return config['switchyd.config'] 45 | }, 46 | 47 | set: (config:RawConfig):Promise => { 48 | return chrome.storage.local.set({ 'switchyd.config': config }) 49 | } 50 | } 51 | } 52 | 53 | let mockConfig:RawConfig = { 54 | version: 3, 55 | servers: [ 56 | { 57 | accepts: ['www.google.com', 'www.facebook.com'], 58 | denys: ['www.weibo.com', 'www.baidu.com'], 59 | listen: [ 60 | 'net::ERR_CONNECTION_RESET', 61 | 'net::ERR_CONNECTION_TIMED_OUT', 62 | 'net::ERR_SSL_PROTOCOL_ERROR', 63 | 'net::ERR_TIMED_OUT' 64 | ], 65 | server: 'SOCKS5:127.0.0.1:10086' 66 | }, 67 | { 68 | accepts: ['twitter.com', 'github.com'], 69 | denys: ['www.douban.com'], 70 | listen: [ 71 | 'net::ERR_CONNECTION_RESET', 72 | 'net::ERR_CONNECTION_TIMED_OUT', 73 | 'net::ERR_SSL_PROTOCOL_ERROR', 74 | 'net::ERR_TIMED_OUT' 75 | ], 76 | server: 'SOCKS5:127.0.0.2:10086' 77 | } 78 | ] 79 | } 80 | return { 81 | get: ():Promise => { 82 | return Promise.resolve(mockConfig) 83 | }, 84 | set: (config:RawConfig):Promise => { 85 | mockConfig = config 86 | return Promise.resolve() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/core/config.ts: -------------------------------------------------------------------------------- 1 | import { CompileList, Generator, Group } from './pac.js' 2 | import { URLTier } from './trie.js' 3 | 4 | export type ProxyGroup = { 5 | accepts :string[], 6 | denys :string[], 7 | listen:string[], 8 | server: string, 9 | } 10 | 11 | export interface RawConfig { 12 | version :number 13 | servers :ProxyGroup[] 14 | } 15 | 16 | export type RawConfigSyncer = (config:RawConfig) => Promise 17 | 18 | export class Config { 19 | protected raw : RawConfig 20 | protected sync: RawConfigSyncer 21 | 22 | constructor (raw :RawConfig, sync:RawConfigSyncer) { 23 | this.raw = raw 24 | this.sync = sync 25 | } 26 | 27 | public createGeneartor () :Generator { 28 | // recover config 29 | const generatorGroups :Group[] = [] 30 | for (const group of this.raw.servers) { 31 | const genGroup = new Group([group.server]) 32 | group.accepts.map((x) => x.replace(/\$/g, '')).forEach(genGroup.proxyFor.bind(genGroup)) 33 | group.denys.map((x) => x.replace(/\$/g, '')).forEach(genGroup.bypassFor.bind(genGroup)) 34 | generatorGroups.push(genGroup) 35 | } 36 | 37 | return new Generator(generatorGroups) 38 | } 39 | 40 | public jsonify ():string { 41 | return JSON.stringify(this.raw) 42 | } 43 | 44 | public async assignProxyFor (error:string, url:string) : Promise { 45 | for (const group of this.raw.servers) { 46 | if (group.listen.find((x) => x === error)) { 47 | if (CompileList(group.denys).test(url)) { 48 | return Promise.resolve(false) 49 | } else if (CompileList(group.accepts).test(url)) { 50 | // already proxy 51 | return Promise.resolve(false) 52 | } 53 | continue 54 | } 55 | } 56 | 57 | // no existing rule associate with such url, try add 58 | let changed:boolean = false 59 | for (const group of this.raw.servers) { 60 | if (group.listen.find((x) => x === error)) { 61 | group.accepts.push(url) 62 | changed = true 63 | } 64 | } 65 | 66 | if (changed) { 67 | console.log(`change for assignProxyFor(${error},${url})`) 68 | await this.sync(this.compact()) 69 | return true 70 | } 71 | 72 | console.log(`no change for assignProxyFor(${error},${url})`) 73 | return Promise.resolve(false) 74 | } 75 | 76 | protected compact ():RawConfig { 77 | this.raw.servers.forEach((server) => { 78 | [server.accepts, server.denys].forEach((list:string[]):void => { 79 | const tire = new URLTier() 80 | list.forEach(tire.add.bind(tire)) 81 | tire.compact() 82 | list.splice(0, list.length, ...tire.unroll()) 83 | }) 84 | }) 85 | 86 | return this.raw 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/core/main.ts: -------------------------------------------------------------------------------- 1 | import { resolveStorage } from './chrome.js' 2 | import { Switchyd } from './switchyd.js' 3 | 4 | declare const chrome: { 5 | webRequest: any 6 | proxy: any 7 | runtime: { 8 | onInstalled: { 9 | addListener: (callback: (detail: { reason: string }) => void) => any 10 | } 11 | } 12 | } 13 | 14 | chrome.runtime.onInstalled.addListener((details) => { 15 | if (details.reason !== "install" && details.reason !== "update") 16 | return 17 | 18 | new Switchyd(chrome.webRequest, chrome.proxy, resolveStorage()).plug() 19 | }) 20 | -------------------------------------------------------------------------------- /src/core/pac.ts: -------------------------------------------------------------------------------- 1 | import { URLTier } from './trie.js' 2 | 3 | export class Group { 4 | protected bypass : URLTier 5 | protected proxy : URLTier 6 | protected servers :string[] 7 | 8 | constructor (servers : string[]) { 9 | this.bypass = new URLTier() 10 | this.proxy = new URLTier() 11 | if (servers) { 12 | this.servers = servers 13 | } else { 14 | this.servers = [] 15 | } 16 | } 17 | 18 | public proxyFor (url :string): void { 19 | this.proxy.add(url) 20 | this.proxy.compact() 21 | } 22 | 23 | public bypassFor (url :string): void { 24 | this.bypass.add(url) 25 | this.bypass.compact() 26 | } 27 | 28 | public compile ():string { 29 | const proxy = CompileList(this.proxy.unroll()) 30 | const bypass = CompileList(this.bypass.unroll()) 31 | return ` 32 | { 33 | "proxy" : new RegExp(${proxy}), 34 | "bypass" : new RegExp(${bypass}), 35 | "servers" : "${this.serverString()}" 36 | } 37 | ` 38 | } 39 | 40 | protected serverString () : string { 41 | if (this.servers.length > 0) { 42 | return this.servers.join(';').replace(/DIRECT/gi, '') + ';DIRECT;' 43 | } 44 | return 'DIRECT' 45 | } 46 | } 47 | 48 | export class Generator { 49 | protected groups : Group[] 50 | 51 | constructor (groups : Group[]) { 52 | if (groups) { 53 | this.groups = groups 54 | } else { 55 | this.groups = [] 56 | } 57 | } 58 | 59 | public compile ():string { 60 | return ` 61 | "use strict"; 62 | var groups = [ 63 | ${this.groups.map((g) => g.compile()).join(',\n')} 64 | ]; 65 | function FindProxyForURL(url, host) { 66 | for (var i=0; i { 80 | let expr = list.filter((x) => x.trim().length > 0) // filter empty 81 | .map((x) => x.replace(/(\*|\$| )/g, '')) // replace start/end quote and spaceing 82 | .map((x) => x.replace(/^\./, '')) // trim leading . 83 | .map((x) => x.replace(/\./g, '\\.')) // escap dot to compliant RegExp 84 | .map((x) => `(${x}$)`) // grouping with tailing matching 85 | .join('|') 86 | 87 | if (expr.length === 0) { 88 | // matching nothing 89 | expr = '$^' 90 | } 91 | 92 | return new RegExp(expr) 93 | } 94 | -------------------------------------------------------------------------------- /src/core/switchyd.ts: -------------------------------------------------------------------------------- 1 | import { Config, RawConfig } from './config.js' 2 | 3 | interface Listener { 4 | addListener: ( 5 | callback: (details: { 6 | error: string, 7 | url: string, 8 | }) => void, 9 | filter: { 10 | urls: string[], 11 | }, 12 | extraInfoSpec?: any 13 | ) => void 14 | } 15 | 16 | export interface WebHook { 17 | // handlerBehaviorChanged : (callback? :Function) => void 18 | onErrorOccurred: Listener 19 | } 20 | 21 | export interface ProxyHook { 22 | settings: { 23 | set: ( 24 | details: { 25 | value: { 26 | mode: string, 27 | pacScript: { 28 | data: string 29 | }, 30 | } 31 | } 32 | ) => Promise 33 | } 34 | } 35 | 36 | export interface Storage { 37 | set: (config: RawConfig) => Promise 38 | get: () => Promise 39 | } 40 | 41 | export class SwitchydWorker { 42 | protected proxyhook: ProxyHook 43 | protected storage: Storage 44 | 45 | constructor (proxyhook: ProxyHook, storage: Storage) { 46 | this.proxyhook = proxyhook 47 | this.storage = storage 48 | } 49 | 50 | public async applyPAC (): Promise { 51 | const config = await this.loadConfig() 52 | return await this.applyPACWithConfig(config) 53 | } 54 | 55 | public async assignProxyFor (error: string, url: string): Promise { 56 | console.log(`try add ${url} for ${error}`) 57 | const config = await this.loadConfig() 58 | const changed = await config.assignProxyFor(error, url) 59 | if (changed) { 60 | console.log(`try appply for${error},${url}`) 61 | return this.loadConfig() 62 | .then((config: Config) => this.applyPACWithConfig(config)) 63 | } 64 | console.log(`no appply for${error},${url}`) 65 | return await Promise.resolve() 66 | } 67 | 68 | protected async applyPACWithConfig (config: Config): Promise { 69 | const script = config.createGeneartor().compile() 70 | try { 71 | await this.proxyhook.settings.set( 72 | { 73 | value: { 74 | mode: 'pac_script', 75 | pacScript: { 76 | data: script 77 | } 78 | } 79 | } 80 | ) 81 | console.log(`apply ${script}`) 82 | } catch (reason) { 83 | console.warn(`fail to apply pac script:${reason}`) 84 | } 85 | } 86 | 87 | protected async loadConfig (): Promise { 88 | const raw = await this.storage.get() 89 | return new Config(raw, (config: RawConfig) => this.storage.set(config)) 90 | } 91 | } 92 | 93 | export class Switchyd { 94 | protected webhook: WebHook 95 | protected proxyhook: ProxyHook 96 | protected storage: Storage 97 | 98 | constructor (webhook: WebHook, proxyhook: ProxyHook, storage: Storage) { 99 | this.webhook = webhook 100 | this.proxyhook = proxyhook 101 | this.storage = storage 102 | } 103 | 104 | public newWorker (): SwitchydWorker { 105 | return new SwitchydWorker(this.proxyhook, this.storage) 106 | } 107 | 108 | public plug (): void { 109 | // kick first pac 110 | this.newWorker().applyPAC() 111 | 112 | // sync register 113 | this.webhook.onErrorOccurred.addListener( 114 | (details): void => { 115 | // it is run in a service worker 116 | const worker = this.newWorker() 117 | if (details.error === 'net::ERR_NETWORK_CHANGED') { 118 | console.log('network changed,regen PAC script') 119 | worker.applyPAC() 120 | return 121 | } 122 | 123 | // find host start 124 | const start = details.url.indexOf('://') + 3 125 | let end = details.url.indexOf('/', start) 126 | if (end === -1) { 127 | end = details.url.length 128 | } 129 | 130 | const url = details.url.substring(start, end) 131 | worker.assignProxyFor(details.error, url) 132 | }, 133 | { 134 | urls: [ 135 | 'http://*/*', 136 | 'https://*/*', 137 | 'ws://*/*', 138 | 'wss://*/*' 139 | ] 140 | }, 141 | ['extraHeaders'] 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/core/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable require-jsdoc */ 3 | import { Config, RawConfig, RawConfigSyncer } from './config.js' 4 | import { Switchyd } from './switchyd.js' 5 | import { Generator, Group } from './pac.js' 6 | import { URLTier } from './trie.js' 7 | 8 | function testAddURL () { 9 | const trie = new URLTier() 10 | trie.add('www.google.com') 11 | trie.add('www.facebook.com') 12 | trie.add('2.toplevel-1.dot.google.com') 13 | trie.add('1.toplevel-2.dot.google.com') 14 | trie.add('toplevel-2.dot.google.com') 15 | trie.add('toplevel-3.dot.google.com') 16 | trie.compact() 17 | 18 | console.log(trie.unroll()) 19 | } 20 | 21 | function testPac () { 22 | const group = new Group(['socks5://127.0.0.1:10086']) 23 | group.proxyFor('www.google.com') 24 | group.proxyFor('www.facebook.com') 25 | group.proxyFor('2.toplevel-1.dot.google.com') 26 | group.proxyFor('1.toplevel-2.dot.google.com') 27 | group.proxyFor('toplevel-2.dot.google.com') 28 | group.proxyFor('toplevel-3.dot.google.com') 29 | 30 | group.bypassFor('*.co.jp') 31 | 32 | const pac = new Generator([group, group]) 33 | 34 | console.log(pac.compile()) 35 | } 36 | 37 | function testConfig () { 38 | const raw = ` 39 | {"servers":[{"listen":["net::ERR_CONNECTION_RESET","net::ERR_CONNECTION_TIMED_OUT","net::ERR_SSL_PROTOCOL_ERROR","net::ERR_TIMED_OUT"],"accepts":["google.com$","lh3.googleusercontent.com$","twitter.com$","abs.twimg.com$","golang.org$","www5.javmost.com$","static.javhd.com$","avgle.com$","avgle.net$","www.pornhub.com$","a.realsrv.com$","chaturbate.com$","xapi.juicyads.com$","faws.xcity.jp$","i.ytimg.com$","encrypted-tbn0.gstatic.com$","www.imglnke.com$","cdn.qooqlevideo.com$","id.rlcdn.com$","googleads.g.doubleclick.net$","img9.doubanio.com$","feedly.com$","desktop.telegram.org$","static.reuters.com$","news.ycombinator.com$","connect.facebook.net$","dt.adsafeprotected.com$","static.reutersmedia.net$","hw-cdn2.trafficjunky.net$","di.phncdn.com$","www.vfthr.com$","s.amazon-adsystem.com$","t.co$","www.theguardian.com$","confiant-integrations.global.ssl.fastly.net$","imgur.com$","docs.rsshub.app$","external-preview.redd.it$","www.reddit.com$","www.redditstatic.com$","styles.redditmedia.com$","cdn.rawgit.com$","blog.ipfs.io$","en.wikipedia.org$","login.wikimedia.org$","www.youtube.com$","r3---sn-un57en7s.googlevideo.com$","yt3.ggpht.com$","blogspot.com$","www.blogger.com$","www.blogblog.com$","bcp.crwdcntrl.net$","s3t3d2y7.ackcdn.net$","spl.zeotap.com$","pics.dmm.co.jp$","adxadserv.com$","maps.googleapis.com$","prod-fastly-us-east-1.video.pscp.tv$","p4-ac7k666k5mxjy-r2vukgzcrraigxob-275394-i1-v6exp3-ds.metric.ipv6test.com$","p4-ac7k666k5mxjy-r2vukgzcrraigxob-275394-i2-v6exp3-ds.metric.ipv6test.net$","search.xiepp.com$","loadm.exelator.com$","interstitial-07.com$","www.facebook.com$","t.dtscout.com$","xfreehdvideos.com$","bebreloomr.com$","2g1radlamdy3.l4.adsco.re$","dt-secure.videohub.tv$","syndication.exosrv.com$","ml314.com$","global.ib-ibi.com$","cdn-images-1.medium.com$","cdn.substack.com$","cdn.streamroot.io$","pushance.com$","444131a.com$","iqiyi.irs01.com$","omgubuntu.disqus.com$","secure.gravatar.com$","rtb0.doubleverify.com$","www.google.com.tw$","analytics.tiktok.com$","external-content.duckduckgo.com$","github.com$","cafemedia-d.openx.net$","pandg.tapad.com$","192.168.1.100$","www.v2ex.com$","updates.tdesktop.com$","telegram.me$","t.me$","cdn3.dd109.com:65$","apt-mirror.github.io$","cdn-images.mailchimp.com$","api.amplitude.com$","registry.aliyuncs.com$","weibo.com$","shandianzy-com.xktapi.com:5656$","static.trafficmoose.com$","bordeaux.futurecdn.net$","rp.liadm.com$","www.javbus.com$","sdc.cmbchina.com$","bam.nr-data.net$","lit.dev$","developer.chrome.com$","az416426.vo.msecnd.net$","www.youtube-nocookie.com$","www.commonjs.org$","media.theporndude.com$","tn.voyeurhit.com$","www.fembed.com$","jable.tv$","cdn.o333o.com$","app.link$"],"denys":[],"server":"SOCKS5 127.0.0.1:10086"}],"version":3} 40 | ` 41 | const rawConfig: RawConfig = JSON.parse(raw) 42 | const config = new Config(rawConfig, (_:RawConfig):Promise => Promise.resolve()) 43 | console.log(config.createGeneartor().compile()) 44 | } 45 | 46 | function testSwitchyd () { 47 | const raw = ` 48 | {"servers":[{"listen":["net::ERR_CONNECTION_RESET","net::ERR_CONNECTION_TIMED_OUT","net::ERR_SSL_PROTOCOL_ERROR","net::ERR_TIMED_OUT"],"accepts":["google.com$","lh3.googleusercontent.com$","twitter.com$","abs.twimg.com$","golang.org$","www5.javmost.com$","static.javhd.com$","avgle.com$","avgle.net$","www.pornhub.com$","a.realsrv.com$","chaturbate.com$","xapi.juicyads.com$","faws.xcity.jp$","i.ytimg.com$","encrypted-tbn0.gstatic.com$","www.imglnke.com$","cdn.qooqlevideo.com$","id.rlcdn.com$","googleads.g.doubleclick.net$","img9.doubanio.com$","feedly.com$","desktop.telegram.org$","static.reuters.com$","news.ycombinator.com$","connect.facebook.net$","dt.adsafeprotected.com$","static.reutersmedia.net$","hw-cdn2.trafficjunky.net$","di.phncdn.com$","www.vfthr.com$","s.amazon-adsystem.com$","t.co$","www.theguardian.com$","confiant-integrations.global.ssl.fastly.net$","imgur.com$","docs.rsshub.app$","external-preview.redd.it$","www.reddit.com$","www.redditstatic.com$","styles.redditmedia.com$","cdn.rawgit.com$","blog.ipfs.io$","en.wikipedia.org$","login.wikimedia.org$","www.youtube.com$","r3---sn-un57en7s.googlevideo.com$","yt3.ggpht.com$","blogspot.com$","www.blogger.com$","www.blogblog.com$","bcp.crwdcntrl.net$","s3t3d2y7.ackcdn.net$","spl.zeotap.com$","pics.dmm.co.jp$","adxadserv.com$","maps.googleapis.com$","prod-fastly-us-east-1.video.pscp.tv$","p4-ac7k666k5mxjy-r2vukgzcrraigxob-275394-i1-v6exp3-ds.metric.ipv6test.com$","p4-ac7k666k5mxjy-r2vukgzcrraigxob-275394-i2-v6exp3-ds.metric.ipv6test.net$","search.xiepp.com$","loadm.exelator.com$","interstitial-07.com$","www.facebook.com$","t.dtscout.com$","xfreehdvideos.com$","bebreloomr.com$","2g1radlamdy3.l4.adsco.re$","dt-secure.videohub.tv$","syndication.exosrv.com$","ml314.com$","global.ib-ibi.com$","cdn-images-1.medium.com$","cdn.substack.com$","cdn.streamroot.io$","pushance.com$","444131a.com$","iqiyi.irs01.com$","omgubuntu.disqus.com$","secure.gravatar.com$","rtb0.doubleverify.com$","www.google.com.tw$","analytics.tiktok.com$","external-content.duckduckgo.com$","github.com$","cafemedia-d.openx.net$","pandg.tapad.com$","192.168.1.100$","www.v2ex.com$","updates.tdesktop.com$","telegram.me$","t.me$","cdn3.dd109.com:65$","apt-mirror.github.io$","cdn-images.mailchimp.com$","api.amplitude.com$","registry.aliyuncs.com$","weibo.com$","shandianzy-com.xktapi.com:5656$","static.trafficmoose.com$","bordeaux.futurecdn.net$","rp.liadm.com$","www.javbus.com$","sdc.cmbchina.com$","bam.nr-data.net$","lit.dev$","developer.chrome.com$","az416426.vo.msecnd.net$","www.youtube-nocookie.com$","www.commonjs.org$","media.theporndude.com$","tn.voyeurhit.com$","www.fembed.com$","jable.tv$","cdn.o333o.com$","app.link$"],"denys":[],"server":"SOCKS5 127.0.0.1:10086"}],"version":3} 49 | ` 50 | 51 | let singleValue: RawConfig = JSON.parse(raw) 52 | 53 | const engine = new Switchyd( 54 | { 55 | onErrorOccurred: { 56 | addListener: (callback, filter, extra):void => { 57 | const details = { 58 | error: 'net::ERR_CONNECTION_RESET', 59 | url: 'https://123.145.45.35:80' 60 | } 61 | callback(details) 62 | } 63 | } 64 | }, 65 | { 66 | settings: { 67 | set: (details):Promise => { 68 | return new Promise((resolve) => { 69 | // console.log(`apply setting:${details}`) 70 | resolve() 71 | }) 72 | } 73 | } 74 | }, 75 | { 76 | set: (config:RawConfig):Promise => { 77 | singleValue = config 78 | return Promise.resolve() 79 | }, 80 | get: ():Promise => { 81 | return Promise.resolve(singleValue) 82 | } 83 | } 84 | ) 85 | 86 | engine.plug() 87 | } 88 | 89 | // testAddURL() 90 | // testPac() 91 | // testConfig() 92 | testSwitchyd() 93 | -------------------------------------------------------------------------------- /src/core/trie.ts: -------------------------------------------------------------------------------- 1 | export class URLTier { 2 | protected lookup :Map 3 | 4 | constructor () { 5 | this.lookup = new Map() 6 | } 7 | 8 | public add (url:string) :void { 9 | let lookup = this.lookup 10 | for (const key of url.split('.').reverse()) { 11 | let next = lookup.get(key) 12 | if (next) { 13 | lookup = next.lookup 14 | continue 15 | } 16 | 17 | // build new trie 18 | lookup.set(key, next = new URLTier()) 19 | lookup = next.lookup 20 | continue 21 | } 22 | } 23 | 24 | public compact (): void { 25 | this.compactWithLevel(0) 26 | } 27 | 28 | public unroll ():string[] { 29 | return Array.from(this.unrollWithContext([])) 30 | } 31 | 32 | protected unrollWithContext (ctx:string[]):Set { 33 | const collect = new Set() 34 | 35 | // nothing left,just build it 36 | if (this.lookup.size === 0) { 37 | collect.add(ctx.reverse().join('.')) 38 | ctx.reverse() 39 | return collect 40 | } 41 | 42 | for (const [key, child] of this.lookup.entries()) { 43 | ctx.push(key) 44 | child.unrollWithContext(ctx).forEach(collect.add.bind(collect)) 45 | ctx.pop() 46 | } 47 | 48 | return collect 49 | } 50 | 51 | protected compactWithLevel (level:number): void { 52 | // reduce at least 3 url component, 53 | // or just keep it 54 | if (level < 2) { 55 | for (const child of this.lookup.values()) { 56 | child.compactWithLevel(level + 1) 57 | } 58 | return 59 | } 60 | 61 | // reduce this tree 62 | if (this.lookup.size >= 2) { 63 | this.lookup.clear() 64 | this.lookup.set('*', new URLTier()) 65 | }else{ 66 | for (const child of this.lookup.values()) { 67 | child.compactWithLevel(level + 1) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ui.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // switchd cores 3 | import { resolveStorage } from './core/chrome.js' 4 | import { Storage, SwitchydWorker } from './core/switchyd.js' 5 | import { RawConfig } from './core/config.js' 6 | 7 | // lit 8 | import { LitElement, css, html, TemplateResult } from 'lit' 9 | import { customElement, state } from 'lit/decorators.js' 10 | import { Task } from '@lit-labs/task' 11 | import { repeat } from 'lit/directives/repeat.js' 12 | import { when } from 'lit/directives/when.js' 13 | 14 | // styles 15 | import '@spectrum-web-components/theme/theme-light.js' 16 | import '@spectrum-web-components/theme/scale-medium.js' 17 | import '@spectrum-web-components/theme/sp-theme.js' 18 | 19 | // side bar 20 | import '@spectrum-web-components/split-view/sp-split-view.js' 21 | import '@spectrum-web-components/sidenav/sp-sidenav.js' 22 | import '@spectrum-web-components/sidenav/sp-sidenav-heading.js' 23 | import '@spectrum-web-components/sidenav/sp-sidenav-item.js' 24 | import '@spectrum-web-components/tabs/sp-tabs.js' 25 | import '@spectrum-web-components/tabs/sp-tab.js' 26 | import '@spectrum-web-components/tabs/sp-tab-panel.js' 27 | import '@spectrum-web-components/textfield/sp-textfield.js' 28 | import '@spectrum-web-components/button/sp-button.js' 29 | import { Textfield } from '@spectrum-web-components/textfield' 30 | 31 | // icons 32 | import '@spectrum-web-components/icons-workflow/icons/sp-icon-actions.js' 33 | import '@spectrum-web-components/icons-workflow/icons/sp-icon-add.js' 34 | import '@spectrum-web-components/icons-workflow/icons/sp-icon-remove.js' 35 | import '@spectrum-web-components/icons-workflow/icons/sp-icon-arrow-up.js' 36 | 37 | enum ListType { 38 | Proxy = 'Proxy', 39 | Bypass = 'Bypass', 40 | Activate = 'Activate On' 41 | } 42 | 43 | declare const chrome: { 44 | webRequest:any 45 | proxy:any 46 | } 47 | 48 | @customElement('switchyd-setting') 49 | export class SwitchydSetting extends LitElement { 50 | static styles = css` 51 | :host { 52 | .text { 53 | widht: 100%; 54 | text-align: center; 55 | } 56 | } 57 | 58 | .fill { 59 | height: 100vh; 60 | } 61 | 62 | .span { 63 | width: 100%; 64 | } 65 | 66 | .tab-spacing { 67 | min-width: 33%; 68 | text-align: center; 69 | } 70 | 71 | .list-item { 72 | text-align: center; 73 | margin-top: 1%; 74 | } 75 | 76 | sp-button { 77 | vertical-align: middle; 78 | } 79 | 80 | .icons { 81 | display: flex; 82 | justify-content: space-evenly; 83 | } 84 | `; 85 | 86 | dirty:number = 0 87 | 88 | @state() 89 | selected:number = 0 90 | 91 | config:Storage = resolveStorage() 92 | 93 | loadConfig = new Task( 94 | this, 95 | ([_]):Promise => { 96 | return resolveStorage().get() 97 | }, 98 | () => [] 99 | ); 100 | 101 | render () { 102 | return html` 103 | 104 | 105 |
106 | 107 | 108 | ${this.loadConfig.render({ 109 | complete: (config) => html` 110 | ${repeat( 111 | config.servers, 112 | (server) => server.server, 113 | (server, index) => html` 114 | 115 | 116 |
117 |
118 | 119 | ${when( 120 | index !== 0, 121 | () => html` 122 |
126 | 127 |
128 | ` 129 | )} 130 | 131 |
135 | 136 |
137 | 138 |
147 | 148 |
149 | 150 | ${when( 151 | config.servers.length > 1, 152 | () => html` 153 |
161 | 162 |
163 | ` 164 | )} 165 | 166 |
167 | 168 | 179 | 180 |
181 |
182 | ` 183 | )} 184 | ` 185 | })} 186 |
187 |
188 |
189 | 190 |
191 | 192 | 193 | 194 | 195 | 196 | ${this.loadConfig.render({ 197 | complete: (config) => html` 198 | 199 | ${this.rednerList(config, ListType.Proxy, this.selected)} 200 | 201 | 202 | ${this.rednerList(config, ListType.Bypass, this.selected)} 203 | 204 | 205 | ${this.rednerList(config, ListType.Activate, this.selected)} 206 | 207 | ` 208 | })} 209 | 210 | 211 |
212 |
213 |
214 | ` 215 | } 216 | 217 | protected rednerList (config:RawConfig, type:ListType, selected:number):TemplateResult { 218 | const list = (():string[] => { 219 | switch (type) { 220 | case ListType.Proxy: 221 | return config.servers[selected].accepts 222 | case ListType.Bypass: 223 | return config.servers[selected].denys 224 | case ListType.Activate: 225 | return config.servers[selected].listen 226 | } 227 | })() 228 | 229 | return html` 230 |
231 | ${when( 232 | list.length > 0, 233 | // not empty 234 | () => html`${repeat(list, (s, index) => html` 235 |
236 | 243 | 244 | 245 | Remove 250 | { 252 | this.typeToList(config, type, selected).splice(index, 0, 'example.com') 253 | this.syncConfig(config) 254 | }} 255 | >Add 256 |
257 | `)}`, 258 | 259 | // empty 260 | () => html` 261 |
262 | 263 | 264 | { 266 | const changed = this.renderRoot.querySelector('#list-' + selected + '-' + type) as Textfield 267 | if (changed.value.length > 0) { 268 | this.typeToList(config, type, selected).push(changed.value) 269 | this.syncConfig(config) 270 | } 271 | }} 272 | >Add 273 |
274 | ` 275 | )} 276 |
277 | ` 278 | } 279 | 280 | protected typeToList (config:RawConfig, type:ListType, selected:number):string[] { 281 | switch (type) { 282 | case ListType.Proxy: 283 | return config.servers[selected].accepts 284 | case ListType.Bypass: 285 | return config.servers[selected].denys 286 | case ListType.Activate: 287 | return config.servers[selected].listen 288 | } 289 | } 290 | 291 | protected selectServer (event:Event):void { 292 | const selectedValue = this.renderRoot.querySelector('sp-sidenav')?.value 293 | if (selectedValue) { 294 | this.selected = Number.parseInt(selectedValue) 295 | } 296 | } 297 | 298 | protected syncConfig (config:RawConfig):void { 299 | this.config.set(config) 300 | this.requestUpdate() 301 | 302 | if (chrome.proxy) { 303 | new SwitchydWorker(chrome.proxy, this.config).applyPAC() 304 | } 305 | } 306 | } 307 | 308 | declare global { 309 | interface HTMLElementTagNameMap { 310 | 'switchyd-setting': SwitchydSetting, 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | "useDefineForClassFields": false, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | //"module": "commonjs", /* Specify what module code is generated. */ 28 | "module": "esnext", 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | "rootDirs": [ 34 | ".","./node_modules" 35 | ], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files */ 40 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 41 | 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 46 | 47 | /* Emit */ 48 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 49 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 50 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 51 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 53 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 67 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 68 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 69 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 78 | 79 | /* Type Checking */ 80 | "strict": true, /* Enable all strict type-checking options. */ 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 82 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 87 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /web-dev-server.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | watch: true, 3 | appIndex: 'src/ui/main.html', 4 | nodeResolve: { 5 | exportConditions: ['development'] 6 | }, 7 | esbuildTarget: 'auto' 8 | } 9 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'production', 3 | entry: { 4 | 'option-ui': './dist/ui.js', 5 | 'service-worker': './dist/core/main.js' 6 | }, 7 | output: { 8 | filename: '[name].js' 9 | }, 10 | experiments: { 11 | topLevelAwait: true 12 | } 13 | } 14 | --------------------------------------------------------------------------------