├── .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 |
{
123 | [config.servers[index], config.servers[index - 1]] = [config.servers[index - 1], config.servers[index]]
124 | this.syncConfig(config)
125 | }}'>
126 |
127 |
128 | `
129 | )}
130 |
131 |
{
132 | const input = this.renderRoot.querySelector('#nav-' + index) as Textfield
133 | input.disabled = false
134 | }}'>
135 |
136 |
137 |
138 |
{
139 | config.servers.splice(index, 0, {
140 | accepts: [],
141 | denys: [],
142 | listen: [],
143 | server: config.servers[index].server
144 | })
145 | this.syncConfig(config)
146 | }}'>
147 |
148 |
149 |
150 | ${when(
151 | config.servers.length > 1,
152 | () => html`
153 |
{
154 | config.servers.splice(index, 1)
155 | if (index > 0 && this.selected >= index) {
156 | this.selected--
157 | }
158 |
159 | this.syncConfig(config)
160 | }}'>
161 |
162 |
163 | `
164 | )}
165 |
166 |
167 |
168 |
{
170 | const input = this.renderRoot.querySelector('#nav-' + index) as Textfield
171 | config.servers[index].server = input.value
172 | this.syncConfig(config)
173 | }}'
174 | @focusout='${(_:Event):void => {
175 | const input = this.renderRoot.querySelector('#nav-' + index) as Textfield
176 | input.disabled = true
177 | }}'
178 | >
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 | {
238 | const changed = this.renderRoot.querySelector('#list-' + selected + '-' + type + '-' + index) as Textfield
239 | this.typeToList(config, type, selected)[index] = changed.value
240 |
241 | this.syncConfig(config)
242 | }}'>
243 |
244 |
245 | {
247 | this.typeToList(config, type, selected).splice(index, 1)
248 | this.syncConfig(config)
249 | }}'>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 |
--------------------------------------------------------------------------------