From e87ca52a692c05e2106abbdd2f5ea7524e26ea9a Mon Sep 17 00:00:00 2001 From: Alexander Shutau Date: Thu, 6 Jan 2022 22:04:54 +0000 Subject: [PATCH 1/4] Try conversion to JS --- src/api/{chrome.ts => chrome.js} | 32 +- src/api/{fetch.ts => fetch.js} | 19 +- src/api/{index.ts => index.js} | 23 +- .../{config-manager.ts => config-manager.js} | 144 +++++---- src/background/{devtools.ts => devtools.js} | 273 +++++++++------- src/background/{extension.ts => extension.js} | 296 ++++++++++-------- src/{defaults.ts => defaults.js} | 16 +- tasks/ts2js.js | 183 +++++++++++ 8 files changed, 671 insertions(+), 315 deletions(-) rename src/api/{chrome.ts => chrome.js} (66%) rename src/api/{fetch.ts => fetch.js} (60%) rename src/api/{index.ts => index.js} (76%) rename src/background/{config-manager.ts => config-manager.js} (58%) rename src/background/{devtools.ts => devtools.js} (55%) rename src/background/{extension.ts => extension.js} (70%) rename src/{defaults.ts => defaults.js} (81%) create mode 100644 tasks/ts2js.js diff --git a/src/api/chrome.ts b/src/api/chrome.js similarity index 66% rename from src/api/chrome.ts rename to src/api/chrome.js index 56b7512bf458..85337791058c 100644 --- a/src/api/chrome.ts +++ b/src/api/chrome.js @@ -1,24 +1,31 @@ +// @ts-check import {MessageType} from '../utils/message'; -import type {Message} from '../definitions'; import {readResponseAsDataURL} from '../utils/network'; import {callFetchMethod} from './fetch'; +/** @typedef {import('../definitions').Message} Message */ + if (!window.chrome) { - window.chrome = {} as any; + window.chrome = /** @type {any} */({}); } if (!chrome.runtime) { - chrome.runtime = {} as any; + chrome.runtime = /** @type {any} */({}); } -const messageListeners = new Set<(message: Message) => void>(); +/** @type {Set<(message: Message) => void>} */ +const messageListeners = new Set(); -async function sendMessage(...args: any[]) { +/** + * @param {...any} args + */ +async function sendMessage(...args) { if (args[0] && args[0].type === MessageType.CS_FETCH) { const {id} = args[0]; try { const {url, responseType} = args[0].data; const response = await callFetchMethod(url); - let text: string; + /** @type {string} */ + let text; if (responseType === 'data-url') { text = await readResponseAsDataURL(response); } else { @@ -32,13 +39,16 @@ async function sendMessage(...args: any[]) { } } -function addMessageListener(callback: (data: any) => void) { +/** + * @param {(data: any) => void} callback + */ +function addMessageListener(callback) { messageListeners.add(callback); } if (typeof chrome.runtime.sendMessage === 'function') { const nativeSendMessage = chrome.runtime.sendMessage; - chrome.runtime.sendMessage = (...args: any[]) => { + chrome.runtime.sendMessage = (/** @type {any[]} */...args) => { sendMessage(...args); nativeSendMessage.apply(chrome.runtime, args); }; @@ -47,14 +57,14 @@ if (typeof chrome.runtime.sendMessage === 'function') { } if (!chrome.runtime.onMessage) { - chrome.runtime.onMessage = {} as any; + chrome.runtime.onMessage = /** @type {any} */({}); } if (typeof chrome.runtime.onMessage.addListener === 'function') { const nativeAddListener = chrome.runtime.onMessage.addListener; - chrome.runtime.onMessage.addListener = (...args: any[]) => { + chrome.runtime.onMessage.addListener = (/** @type {any[]} */...args) => { addMessageListener(args[0]); nativeAddListener.apply(chrome.runtime.onMessage, args); }; } else { - chrome.runtime.onMessage.addListener = (...args: any[]) => addMessageListener(args[0]); + chrome.runtime.onMessage.addListener = (/** @type {any[]} */...args) => addMessageListener(args[0]); } diff --git a/src/api/fetch.ts b/src/api/fetch.js similarity index 60% rename from src/api/fetch.ts rename to src/api/fetch.js index 06c66434c59b..24f55486091a 100644 --- a/src/api/fetch.ts +++ b/src/api/fetch.js @@ -1,4 +1,5 @@ -const throwCORSError = async (url: string) => { +// @ts-check +const throwCORSError = async (/** @type {string} */url) => { return Promise.reject(new Error( [ 'Embedded Dark Reader cannot access a cross-origin resource', @@ -11,11 +12,15 @@ const throwCORSError = async (url: string) => { )); }; -type Fetcher = (url: string) => Promise; +/** @typedef {(url: string) => Promise} Fetcher */ -let fetcher: Fetcher = throwCORSError; +/** @type {Fetcher} */ +let fetcher = throwCORSError; -export function setFetchMethod(fetch: Fetcher) { +/** + * @param {Fetcher} fetch + */ +export function setFetchMethod(fetch) { if (fetch) { fetcher = fetch; } else { @@ -23,6 +28,10 @@ export function setFetchMethod(fetch: Fetcher) { } } -export async function callFetchMethod(url: string) { +/** + * @param {string} url + * @returns {Promise} + */ +export async function callFetchMethod(url) { return await fetcher(url); } diff --git a/src/api/index.ts b/src/api/index.js similarity index 76% rename from src/api/index.ts rename to src/api/index.js index 55b33f5da8f1..ad9a8b785252 100644 --- a/src/api/index.ts +++ b/src/api/index.js @@ -1,12 +1,15 @@ +// @ts-check import './chrome'; import {setFetchMethod as setFetch} from './fetch'; import {DEFAULT_THEME} from '../defaults'; -import type {Theme, DynamicThemeFix} from '../definitions'; import ThemeEngines from '../generators/theme-engines'; import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../inject/dynamic-theme'; import {collectCSS} from '../inject/dynamic-theme/css-collection'; import {isMatchMediaChangeEventListenerSupported} from '../utils/platform'; +/** @typedef {import('../definitions').DynamicThemeFix} DynamicThemeFix */ +/** @typedef {import('../definitions').Theme} Theme */ + let isDarkReaderEnabled = false; const isIFrame = (() => { try { @@ -17,7 +20,11 @@ const isIFrame = (() => { } })(); -export function enable(themeOptions: Partial = {}, fixes: DynamicThemeFix = null) { +/** + * @param {Partial} themeOptions + * @param {DynamicThemeFix} fixes + */ +export function enable(themeOptions = {}, fixes = null) { const theme = {...DEFAULT_THEME, ...themeOptions}; if (theme.engine !== ThemeEngines.dynamicTheme) { @@ -38,8 +45,8 @@ export function disable() { const darkScheme = matchMedia('(prefers-color-scheme: dark)'); let store = { - themeOptions: null as Partial, - fixes: null as DynamicThemeFix, + themeOptions: /** @type {Partial} */(null), + fixes: /** @type {DynamicThemeFix} */(null), }; function handleColorScheme() { @@ -50,7 +57,11 @@ function handleColorScheme() { } } -export function auto(themeOptions: Partial | false = {}, fixes: DynamicThemeFix = null) { +/** + * @param {Partial | false} themeOptions + * @param {DynamicThemeFix} fixes + */ +export function auto(themeOptions = {}, fixes = null) { if (themeOptions) { store = {themeOptions, fixes}; handleColorScheme(); @@ -69,7 +80,7 @@ export function auto(themeOptions: Partial | false = {}, fixes: DynamicTh } } -export async function exportGeneratedCSS(): Promise { +export async function exportGeneratedCSS() { return await collectCSS(); } diff --git a/src/background/config-manager.ts b/src/background/config-manager.js similarity index 58% rename from src/background/config-manager.ts rename to src/background/config-manager.js index 029d765364fe..a99fb710cfeb 100644 --- a/src/background/config-manager.ts +++ b/src/background/config-manager.js @@ -1,14 +1,22 @@ +// @ts-check import {readText} from './utils/network'; import {parseArray} from '../utils/text'; import {getDuration} from '../utils/time'; import {indexSitesFixesConfig} from '../generators/utils/parse'; -import type {InversionFix, StaticTheme, DynamicThemeFix} from '../definitions'; -import type {SitePropsIndex} from '../generators/utils/parse'; -import type {ParsedColorSchemeConfig} from '../utils/colorscheme-parser'; import {ParseColorSchemeConfig} from '../utils/colorscheme-parser'; import {logWarn} from '../utils/log'; import {DEFAULT_COLORSCHEME} from '../defaults'; +/** @typedef {{name?: string; local: boolean; localURL?: string; remoteURL?: string}} Config */ +/** @typedef {import('../definitions').DynamicThemeFix} DynamicThemeFix */ +/** @typedef {import('../definitions').InversionFix} InversionFix */ +/** @typedef {import('../definitions').StaticTheme} StaticTheme */ +/** + * @template T + * @typedef {import('../generators/utils/parse').SitePropsIndex} SitePropsIndex + */ +/** @typedef {import('../utils/colorscheme-parser').ParsedColorSchemeConfig} ParsedColorSchemeConfig */ + const CONFIG_URLs = { darkSites: { remote: 'https://raw.githubusercontent.com/darkreader/darkreader/master/src/config/dark-sites.config', @@ -33,45 +41,51 @@ const CONFIG_URLs = { }; const REMOTE_TIMEOUT_MS = getDuration({seconds: 10}); -interface Config { - name?: string; - local: boolean; - localURL?: string; - remoteURL?: string; -} - export default class ConfigManager { - DARK_SITES?: string[]; - DYNAMIC_THEME_FIXES_INDEX?: SitePropsIndex; - DYNAMIC_THEME_FIXES_RAW?: string; - INVERSION_FIXES_INDEX?: SitePropsIndex; - INVERSION_FIXES_RAW?: string; - STATIC_THEMES_INDEX?: SitePropsIndex; - STATIC_THEMES_RAW?: string; - COLOR_SCHEMES_RAW?: ParsedColorSchemeConfig; + /** @type {string[]} */ + DARK_SITES; + /** @type {SitePropsIndex} */ + DYNAMIC_THEME_FIXES_INDEX; + /** @type {string} */ + DYNAMIC_THEME_FIXES_RAW; + /** @type {SitePropsIndex} */ + INVERSION_FIXES_INDEX; + /** @type {string} */ + INVERSION_FIXES_RAW; + /** @type {SitePropsIndex} */ + STATIC_THEMES_INDEX; + /** @type {string} */ + STATIC_THEMES_RAW; + /** @type {ParsedColorSchemeConfig} */ + COLOR_SCHEMES_RAW; raw = { - darkSites: null as string, - dynamicThemeFixes: null as string, - inversionFixes: null as string, - staticThemes: null as string, - colorSchemes: null as string, + darkSites: /** @type {string} */(null), + dynamicThemeFixes: /** @type {string} */(null), + inversionFixes: /** @type {string} */(null), + staticThemes: /** @type {string} */(null), + colorSchemes: /** @type {string} */(null), }; overrides = { - darkSites: null as string, - dynamicThemeFixes: null as string, - inversionFixes: null as string, - staticThemes: null as string, + darkSites: /** @type {string} */(null), + dynamicThemeFixes: /** @type {string} */(null), + inversionFixes: /** @type {string} */(null), + staticThemes: /** @type {string} */(null), }; - private async loadConfig({ + /** + * @param {Config} config + * @returns {Promise} + */ + async #loadConfig({ name, local, localURL, remoteURL, - }: Config) { - let $config: string; + }) { + /** @type {string} */ + let $config; const loadLocal = async () => await readText({url: localURL}); if (local) { $config = await loadLocal(); @@ -89,30 +103,42 @@ export default class ConfigManager { return $config; } - private async loadColorSchemes({local}: Config) { - const $config = await this.loadConfig({ + /** + * @param {Config} config + * @returns {Promise} + */ + async #loadColorSchemes({local}) { + const $config = await this.#loadConfig({ name: 'Color Schemes', local, localURL: CONFIG_URLs.colorSchemes.local, remoteURL: CONFIG_URLs.colorSchemes.remote, }); this.raw.colorSchemes = $config; - this.handleColorSchemes(); + this.#handleColorSchemes(); } - private async loadDarkSites({local}: Config) { - const sites = await this.loadConfig({ + /** + * @param {Config} config + * @returns {Promise} + */ + async #loadDarkSites({local}) { + const sites = await this.#loadConfig({ name: 'Dark Sites', local, localURL: CONFIG_URLs.darkSites.local, remoteURL: CONFIG_URLs.darkSites.remote, }); this.raw.darkSites = sites; - this.handleDarkSites(); + this.#handleDarkSites(); } - private async loadDynamicThemeFixes({local}: Config) { - const fixes = await this.loadConfig({ + /** + * @param {Config} config + * @returns {Promise} + */ + async #loadDynamicThemeFixes({local}) { + const fixes = await this.#loadConfig({ name: 'Dynamic Theme Fixes', local, localURL: CONFIG_URLs.dynamicThemeFixes.local, @@ -122,8 +148,12 @@ export default class ConfigManager { this.handleDynamicThemeFixes(); } - private async loadInversionFixes({local}: Config) { - const fixes = await this.loadConfig({ + /** + * @param {Config} config + * @returns {Promise} + */ + async #loadInversionFixes({local}) { + const fixes = await this.#loadConfig({ name: 'Inversion Fixes', local, localURL: CONFIG_URLs.inversionFixes.local, @@ -133,8 +163,12 @@ export default class ConfigManager { this.handleInversionFixes(); } - private async loadStaticThemes({local}: Config) { - const themes = await this.loadConfig({ + /** + * @param {Config} config + * @returns {Promise} + */ + async #loadStaticThemes({local}) { + const themes = await this.#loadConfig({ name: 'Static Themes', local, localURL: CONFIG_URLs.staticThemes.local, @@ -144,17 +178,21 @@ export default class ConfigManager { this.handleStaticThemes(); } - async load(config: Config) { + /** + * @param {Config} config + * @returns {Promise} + */ + async load(config) { await Promise.all([ - this.loadColorSchemes(config), - this.loadDarkSites(config), - this.loadDynamicThemeFixes(config), - this.loadInversionFixes(config), - this.loadStaticThemes(config), + this.#loadColorSchemes(config), + this.#loadDarkSites(config), + this.#loadDynamicThemeFixes(config), + this.#loadInversionFixes(config), + this.#loadStaticThemes(config), ]).catch((err) => console.error('Fatality', err)); } - private handleColorSchemes() { + #handleColorSchemes() { const $config = this.raw.colorSchemes; const {result, error} = ParseColorSchemeConfig($config); if (error) { @@ -165,26 +203,26 @@ export default class ConfigManager { this.COLOR_SCHEMES_RAW = result; } - private handleDarkSites() { + #handleDarkSites() { const $sites = this.overrides.darkSites || this.raw.darkSites; this.DARK_SITES = parseArray($sites); } handleDynamicThemeFixes() { const $fixes = this.overrides.dynamicThemeFixes || this.raw.dynamicThemeFixes; - this.DYNAMIC_THEME_FIXES_INDEX = indexSitesFixesConfig($fixes); + this.DYNAMIC_THEME_FIXES_INDEX = indexSitesFixesConfig($fixes); this.DYNAMIC_THEME_FIXES_RAW = $fixes; } handleInversionFixes() { const $fixes = this.overrides.inversionFixes || this.raw.inversionFixes; - this.INVERSION_FIXES_INDEX = indexSitesFixesConfig($fixes); + this.INVERSION_FIXES_INDEX = indexSitesFixesConfig($fixes); this.INVERSION_FIXES_RAW = $fixes; } handleStaticThemes() { const $themes = this.overrides.staticThemes || this.raw.staticThemes; - this.STATIC_THEMES_INDEX = indexSitesFixesConfig($themes); + this.STATIC_THEMES_INDEX = indexSitesFixesConfig($themes); this.STATIC_THEMES_RAW = $themes; } } diff --git a/src/background/devtools.ts b/src/background/devtools.js similarity index 55% rename from src/background/devtools.ts rename to src/background/devtools.js index babeb2bb8870..7510d9d19a5a 100644 --- a/src/background/devtools.ts +++ b/src/background/devtools.js @@ -1,48 +1,48 @@ +// @ts-check import {logInfo, logWarn} from '../utils/log'; import {parseInversionFixes, formatInversionFixes} from '../generators/css-filter'; import {parseDynamicThemeFixes, formatDynamicThemeFixes} from '../generators/dynamic-theme'; import {parseStaticThemes, formatStaticThemes} from '../generators/static-theme'; -import type ConfigManager from './config-manager'; import {isFirefox} from '../utils/platform'; +/** @typedef {import('./config-manager').default} ConfigManager */ + // TODO(bershanskiy): Add support for reads/writes of multiple keys at once for performance. // TODO(bershanskiy): Popup UI heeds only hasCustom*Fixes() and nothing else. Consider storing that data separatelly. -interface DevToolsStorage { - get(key: string): Promise; - set(key: string, value: string): void; - remove(key: string): void; - has(key: string): Promise; - setDataIsMigratedForTesting(value: boolean): void; -} +/** @typedef {{get(key: string): Promise; set(key: string, value: string): void; remove(key: string): void; has(key: string): Promise; setDataIsMigratedForTesting(value: boolean): void}} DevToolsStorage */ -declare const __DEBUG__: boolean; +const __DEBUG__ = /*@replace-start:__DEBUG__*/false/*@replace-end:__DEBUG__*/; -class PersistentStorageWrapper implements DevToolsStorage { +/** + * @implements {DevToolsStorage} + */ +class PersistentStorageWrapper { // Cache information within background context for future use without waiting. - private cache: {[key: string]: string} = {}; + /** @type {{[key: string]: string}} */ + #cache = {}; // TODO(bershanskiy): remove migrated and migrateFromLocalStorage after migration end. // Part 1 of 2. - private dataIsMigrated = false; + #dataIsMigrated = false; - setDataIsMigratedForTesting(value: boolean) { + setDataIsMigratedForTesting(/** @type {boolean} */value) { if (__DEBUG__) { - this.dataIsMigrated = value; + this.#dataIsMigrated = value; } } // This function moves DevTools data from loclStorage to chrome.storage.local. // This function is run on every backgroun context invocation, but it has effect only on the // first run. - private async migrateFromLocalStorage() { + async #migrateFromLocalStorage() { // In MV3 world we can't access localStorage, so we can not migrate anything. // Bail out and consider data migrated. if (typeof localStorage === 'undefined') { - this.dataIsMigrated = true; + this.#dataIsMigrated = true; return; } - return new Promise((resolve) => { + return new Promise((resolve) => { chrome.storage.local.get([ DevTools.KEY_DYNAMIC, DevTools.KEY_FILTER, @@ -54,7 +54,7 @@ class PersistentStorageWrapper implements DevToolsStorage { } // If storage contains at least one relevant record, we consider data migrated. if (data[DevTools.KEY_DYNAMIC] || data[DevTools.KEY_FILTER] || data[DevTools.KEY_STATIC]) { - this.dataIsMigrated = true; + this.#dataIsMigrated = true; this.cache = data; resolve(); return; @@ -66,12 +66,12 @@ class PersistentStorageWrapper implements DevToolsStorage { [DevTools.KEY_STATIC]: localStorage.getItem(DevTools.KEY_STATIC), }; - chrome.storage.local.set(this.cache, () => { + chrome.storage.local.set(this.#cache, () => { if (chrome.runtime.lastError) { console.error('DevTools failed to migrate data', chrome.runtime.lastError); resolve(); } - this.dataIsMigrated = true; + this.#dataIsMigrated = true; // Clean up localStorage after migration localStorage.removeItem(DevTools.KEY_DYNAMIC); localStorage.removeItem(DevTools.KEY_FILTER); @@ -82,22 +82,26 @@ class PersistentStorageWrapper implements DevToolsStorage { }); } - async get(key: string) { - if (!this.dataIsMigrated) { - await this.migrateFromLocalStorage(); + /** + * @param {string} key + * @returns {Promise} + */ + async get(key) { + if (!this.#dataIsMigrated) { + await this.#migrateFromLocalStorage(); } - if (key in this.cache) { - return this.cache[key]; + if (key in this.#cache) { + return this.#cache[key]; } - return new Promise((resolve) => { + return new Promise((resolve) => { chrome.storage.local.get(key, (result) => { // If cache received a new value (from call to set()) // before we retreived the old value from storage, // return the new value. - if (key in this.cache) { + if (key in this.#cache) { logInfo(`Key ${key} was written to during read operation.`); - resolve(this.cache[key]); + resolve(this.#cache[key]); return; } @@ -107,14 +111,18 @@ class PersistentStorageWrapper implements DevToolsStorage { return; } - this.cache[key] = result.key; + this.#cache[key] = result.key; resolve(result.key); }); }); } - set(key: string, value: string) { - this.cache[key] = value; + /** + * @param {string} key + * @param {string} value + */ + set(key, value) { + this.#cache[key] = value; chrome.storage.local.set({[key]: value}, () => { if (chrome.runtime.lastError) { console.error('Failed to write DevTools data', chrome.runtime.lastError); @@ -122,8 +130,11 @@ class PersistentStorageWrapper implements DevToolsStorage { }); } - remove(key: string) { - this.cache[key] = undefined; + /** + * @param {string} key + */ + remove(key) { + this.#cache[key] = undefined; chrome.storage.local.remove(key, () => { if (chrome.runtime.lastError) { console.error('Failed to delete DevTools data', chrome.runtime.lastError); @@ -131,19 +142,29 @@ class PersistentStorageWrapper implements DevToolsStorage { }); } - async has(key: string) { + /** + * @param {string} key + */ + async has(key) { return Boolean(await this.get(key)); } } -class LocalStorageWrapper implements DevToolsStorage { +/** + * @implements {DevToolsStorage} + */ +class LocalStorageWrapper { setDataIsMigratedForTesting() { if (__DEBUG__) { logWarn('Unexpected call to setDataIsMigratedForTesting'); } } - async get(key: string) { + /** + * @param {string} key + * @returns {Promise} + */ + async get(key) { try { return localStorage.getItem(key); } catch (err) { @@ -152,7 +173,11 @@ class LocalStorageWrapper implements DevToolsStorage { } } - set(key: string, value: string) { + /** + * @param {string} key + * @param {string} value + */ + set(key, value) { try { localStorage.setItem(key, value); } catch (err) { @@ -160,7 +185,10 @@ class LocalStorageWrapper implements DevToolsStorage { } } - remove(key: string) { + /** + * @param {string} key + */ + remove(key) { try { localStorage.removeItem(key); } catch (err) { @@ -168,7 +196,10 @@ class LocalStorageWrapper implements DevToolsStorage { } } - async has(key: string) { + /** + * @param {string} key + */ + async has(key) { try { return localStorage.getItem(key) != null; } catch (err) { @@ -178,38 +209,63 @@ class LocalStorageWrapper implements DevToolsStorage { } } -class TempStorage implements DevToolsStorage { +/** + * @implements {DevToolsStorage} + */ +class TempStorage { setDataIsMigratedForTesting() { if (__DEBUG__) { logWarn('Unexpected call to setDataIsMigratedForTesting'); } } - map = new Map(); + /** @type {Map} */ + map = new Map(); - async get(key: string) { + /** + * @param {string} key + * @returns {Promise} + */ + async get(key) { return this.map.get(key); } - set(key: string, value: string) { + /** + * @param {string} key + * @param {string} value + */ + set(key, value) { this.map.set(key, value); } - remove(key: string) { + /** + * @param {string} key + */ + remove(key) { this.map.delete(key); } - async has(key: string) { + /** + * @param {string} key + */ + async has(key) { return this.map.has(key); } } export default class DevTools { - private config: ConfigManager; - private onChange: () => void; - private store: DevToolsStorage; - - constructor(config: ConfigManager, onChange: () => void) { + /** @type {ConfigManager} */ + #config; + /** @type {() => void} */ + #onChange; + /** @type {DevToolsStorage} */ + #store; + + /** + * @param {ConfigManager} config + * @param {() => void} onChange + */ + constructor(config, onChange) { // Firefox don't seem to like using storage.local to store big data on the background-extension. // Disabling it for now and defaulting back to localStorage. if (typeof chrome.storage.local !== 'undefined' && chrome.storage.local !== null && !isFirefox) { @@ -219,9 +275,9 @@ export default class DevTools { } else { this.store = new TempStorage(); } - this.config = config; - this.loadConfigOverrides(); - this.onChange = onChange; + this.#config = config; + this.#loadConfigOverrides(); + this.#onChange = onChange; } // TODO(bershanskiy): make private again once PersistentStorageWrapper removes migration logic. @@ -230,124 +286,127 @@ export default class DevTools { static KEY_FILTER = 'dev_inversion_fixes'; static KEY_STATIC = 'dev_static_themes'; - setDataIsMigratedForTesting(value: boolean) { - this.store.setDataIsMigratedForTesting(value); + /** + * @param {boolean} value + */ + setDataIsMigratedForTesting(value) { + this.#store.setDataIsMigratedForTesting(value); } - private async loadConfigOverrides() { - this.config.overrides.dynamicThemeFixes = await this.getSavedDynamicThemeFixes() || null; - this.config.overrides.inversionFixes = await this.getSavedInversionFixes() || null; - this.config.overrides.staticThemes = await this.getSavedStaticThemes() || null; + async #loadConfigOverrides() { + this.#config.overrides.dynamicThemeFixes = await this.#getSavedDynamicThemeFixes() || null; + this.#config.overrides.inversionFixes = await this.#getSavedInversionFixes() || null; + this.#config.overrides.staticThemes = await this.#getSavedStaticThemes() || null; } - private async getSavedDynamicThemeFixes() { - return this.store.get(DevTools.KEY_DYNAMIC); + async #getSavedDynamicThemeFixes() { + return this.#store.get(DevTools.KEY_DYNAMIC); } - private saveDynamicThemeFixes(text: string) { - this.store.set(DevTools.KEY_DYNAMIC, text); + #saveDynamicThemeFixes(/** @type {string} */text) { + this.#store.set(DevTools.KEY_DYNAMIC, text); } async hasCustomDynamicThemeFixes() { - return this.store.has(DevTools.KEY_DYNAMIC); + return this.#store.has(DevTools.KEY_DYNAMIC); } async getDynamicThemeFixesText() { - const $fixes = await this.getSavedDynamicThemeFixes(); - const fixes = $fixes ? parseDynamicThemeFixes($fixes) : parseDynamicThemeFixes(this.config.DYNAMIC_THEME_FIXES_RAW); + const $fixes = await this.#getSavedDynamicThemeFixes(); + const fixes = $fixes ? parseDynamicThemeFixes($fixes) : parseDynamicThemeFixes(this.#config.DYNAMIC_THEME_FIXES_RAW); return formatDynamicThemeFixes(fixes); } resetDynamicThemeFixes() { - this.store.remove(DevTools.KEY_DYNAMIC); - this.config.overrides.dynamicThemeFixes = null; - this.config.handleDynamicThemeFixes(); - this.onChange(); + this.#store.remove(DevTools.KEY_DYNAMIC); + this.#config.overrides.dynamicThemeFixes = null; + this.#config.handleDynamicThemeFixes(); + this.#onChange(); } - applyDynamicThemeFixes(text: string) { + applyDynamicThemeFixes(/** @type {string} */text) { try { const formatted = formatDynamicThemeFixes(parseDynamicThemeFixes(text)); - this.config.overrides.dynamicThemeFixes = formatted; - this.config.handleDynamicThemeFixes(); - this.saveDynamicThemeFixes(formatted); - this.onChange(); + this.#config.overrides.dynamicThemeFixes = formatted; + this.#config.handleDynamicThemeFixes(); + this.#saveDynamicThemeFixes(formatted); + this.#onChange(); return null; } catch (err) { return err; } } - private async getSavedInversionFixes() { - return this.store.get(DevTools.KEY_FILTER); + async #getSavedInversionFixes() { + return this.#store.get(DevTools.KEY_FILTER); } - private saveInversionFixes(text: string) { - this.store.set(DevTools.KEY_FILTER, text); + #saveInversionFixes(/** @type {string} */text) { + this.#store.set(DevTools.KEY_FILTER, text); } async hasCustomFilterFixes() { - return this.store.has(DevTools.KEY_FILTER); + return this.#store.has(DevTools.KEY_FILTER); } async getInversionFixesText() { - const $fixes = await this.getSavedInversionFixes(); - const fixes = $fixes ? parseInversionFixes($fixes) : parseInversionFixes(this.config.INVERSION_FIXES_RAW); + const $fixes = await this.#getSavedInversionFixes(); + const fixes = $fixes ? parseInversionFixes($fixes) : parseInversionFixes(this.#config.INVERSION_FIXES_RAW); return formatInversionFixes(fixes); } resetInversionFixes() { - this.store.remove(DevTools.KEY_FILTER); - this.config.overrides.inversionFixes = null; - this.config.handleInversionFixes(); - this.onChange(); + this.#store.remove(DevTools.KEY_FILTER); + this.#config.overrides.inversionFixes = null; + this.#config.handleInversionFixes(); + this.#onChange(); } - applyInversionFixes(text: string) { + applyInversionFixes(/** @type {string} */text) { try { const formatted = formatInversionFixes(parseInversionFixes(text)); - this.config.overrides.inversionFixes = formatted; - this.config.handleInversionFixes(); - this.saveInversionFixes(formatted); - this.onChange(); + this.#config.overrides.inversionFixes = formatted; + this.#config.handleInversionFixes(); + this.#saveInversionFixes(formatted); + this.#onChange(); return null; } catch (err) { return err; } } - private async getSavedStaticThemes() { - return this.store.get(DevTools.KEY_STATIC); + async #getSavedStaticThemes() { + return this.#store.get(DevTools.KEY_STATIC); } - private saveStaticThemes(text: string) { - this.store.set(DevTools.KEY_STATIC, text); + #saveStaticThemes(/** @type {string} */text) { + this.#store.set(DevTools.KEY_STATIC, text); } async hasCustomStaticFixes() { - return this.store.has(DevTools.KEY_STATIC); + return this.#store.has(DevTools.KEY_STATIC); } async getStaticThemesText() { - const $themes = await this.getSavedStaticThemes(); - const themes = $themes ? parseStaticThemes($themes) : parseStaticThemes(this.config.STATIC_THEMES_RAW); + const $themes = await this.#getSavedStaticThemes(); + const themes = $themes ? parseStaticThemes($themes) : parseStaticThemes(this.#config.STATIC_THEMES_RAW); return formatStaticThemes(themes); } resetStaticThemes() { - this.store.remove(DevTools.KEY_STATIC); - this.config.overrides.staticThemes = null; - this.config.handleStaticThemes(); - this.onChange(); + this.#store.remove(DevTools.KEY_STATIC); + this.#config.overrides.staticThemes = null; + this.#config.handleStaticThemes(); + this.#onChange(); } - applyStaticThemes(text: string) { + applyStaticThemes(/** @type {string} */text) { try { const formatted = formatStaticThemes(parseStaticThemes(text)); - this.config.overrides.staticThemes = formatted; - this.config.handleStaticThemes(); - this.saveStaticThemes(formatted); - this.onChange(); + this.#config.overrides.staticThemes = formatted; + this.#config.handleStaticThemes(); + this.#saveStaticThemes(formatted); + this.#onChange(); return null; } catch (err) { return err; diff --git a/src/background/extension.ts b/src/background/extension.js similarity index 70% rename from src/background/extension.ts rename to src/background/extension.js index 1a40eeb7bd9d..1c91444b4219 100644 --- a/src/background/extension.ts +++ b/src/background/extension.js @@ -1,7 +1,7 @@ +// @ts-check import ConfigManager from './config-manager'; import DevTools from './devtools'; import IconManager from './icon-manager'; -import type {ExtensionAdapter} from './messenger'; import Messenger from './messenger'; import Newsmaker from './newsmaker'; import TabManager from './tab-manager'; @@ -15,7 +15,6 @@ import createCSSFilterStylesheet from '../generators/css-filter'; import {getDynamicThemeFixesFor} from '../generators/dynamic-theme'; import createStaticStylesheet from '../generators/static-theme'; import {createSVGFilterStylesheet, getSVGFilterMatrixValue, getSVGReverseFilterMatrixValue} from '../generators/svg-filter'; -import type {ExtensionData, FilterConfig, News, Shortcuts, UserSettings, TabInfo, TabData} from '../definitions'; import {isSystemDarkModeEnabled} from '../utils/media-query'; import {isFirefox, isMV3, isThunderbird} from '../utils/platform'; import {MessageType} from '../utils/message'; @@ -24,83 +23,95 @@ import {PromiseBarrier} from '../utils/promise-barrier'; import {StateManager} from './utils/state-manager'; import {debounce} from '../utils/debounce'; -interface ExtensionState { - isEnabled: boolean; - wasEnabledOnLastCheck: boolean; - registeredContextMenus: boolean; -} +/** @typedef {import('./messenger').ExtensionAdapter} ExtensionAdapter */ +/** @typedef {import('../definitions').ExtensionData} ExtensionData */ +/** @typedef {{isEnabled: boolean; wasEnabledOnLastCheck: boolean; registeredContextMenus: boolean}} ExtensionState */ +/** @typedef {import('../definitions').FilterConfig} FilterConfig */ +/** @typedef {import('../definitions').News} News */ +/** @typedef {import('../definitions').Shortcuts} Shortcuts */ +/** @typedef {import('../definitions').TabData} TabData */ +/** @typedef {import('../definitions').TabInfo} TabInfo */ +/** @typedef {import('../definitions').UserSettings} UserSettings */ -declare const __DEBUG__: boolean; +const __DEBUG__ = /*@replace-start:__DEBUG__*/false/*@replace-end:__DEBUG__*/; export class Extension { - config: ConfigManager; - devtools: DevTools; - icon: IconManager; - messenger: Messenger; - news: Newsmaker; - tabs: TabManager; - user: UserStorage; - - private isEnabled: boolean = null; - private wasEnabledOnLastCheck: boolean = null; - private registeredContextMenus: boolean = null; - private popupOpeningListener: () => void = null; + /** @type {ConfigManager} */ + config; + /** @type {DevTools} */ + devtools; + /** @type {IconManager} */ + icon; + /** @type {Messenger} */ + messenger; + /** @type {Newsmaker} */ + news; + /** @type {TabManager} */ + tabs; + /** @type {UserStorage} */ + user; + + #isEnabled = /** @type {boolean} */(null); + #wasEnabledOnLastCheck = /** @type {boolean} */(null); + #registeredContextMenus = /** @type {boolean} */(null); + #popupOpeningListener = /** @type {() => void} */(null); // Is used only with Firefox to bypass Firefox bug - private wasLastColorSchemeDark: boolean = null; - private startBarrier: PromiseBarrier = null; - private stateManager: StateManager = null; + #wasLastColorSchemeDark = /** @type {boolean} */(null); + #startBarrier = /** @type {PromiseBarrier} */(null); + #stateManager = /** @type {StateManager} */(null); - private static ALARM_NAME = 'auto-time-alarm'; - private static LOCAL_STORAGE_KEY = 'Extension-state'; + static #ALARM_NAME = 'auto-time-alarm'; + static #LOCAL_STORAGE_KEY = 'Extension-state'; constructor() { this.config = new ConfigManager(); - this.devtools = new DevTools(this.config, async () => this.onSettingsChanged()); - this.messenger = new Messenger(this.getMessengerAdapter()); - this.news = new Newsmaker((news) => this.onNewsUpdate(news)); + this.devtools = new DevTools(this.config, async () => this.#onSettingsChanged()); + this.messenger = new Messenger(this.#getMessengerAdapter()); + this.news = new Newsmaker((news) => this.#onNewsUpdate(news)); this.tabs = new TabManager({ - getConnectionMessage: ({url, frameURL}) => this.getConnectionMessage(url, frameURL), - getTabMessage: this.getTabMessage, - onColorSchemeChange: this.onColorSchemeChange, + getConnectionMessage: ({url, frameURL}) => this.#getConnectionMessage(url, frameURL), + getTabMessage: this.#getTabMessage, + onColorSchemeChange: this.#onColorSchemeChange, }); - this.user = new UserStorage({onRemoteSettingsChange: () => this.onRemoteSettingsChange()}); + this.user = new UserStorage({onRemoteSettingsChange: () => this.#onRemoteSettingsChange()}); this.startBarrier = new PromiseBarrier(); - this.stateManager = new StateManager(Extension.LOCAL_STORAGE_KEY, this, { + this.#stateManager = new StateManager(Extension.#LOCAL_STORAGE_KEY, this, { isEnabled: null, wasEnabledOnLastCheck: null, registeredContextMenus: null, }); - chrome.alarms.onAlarm.addListener(this.alarmListener); + chrome.alarms.onAlarm.addListener(this.#alarmListener); if (chrome.permissions.onRemoved) { chrome.permissions.onRemoved.addListener((permissions) => { - // As far as we know, this code is never actually run because there - // is no browser UI for removing 'contextMenus' permission. - // This code exists for future-proofing in case browsers ever add such UI. + // As far as we know, this code is never actually run because there + // is no browser UI for removing 'contextMenus' permission. + // This code exists for future-proofing in case browsers ever add such UI. if (!permissions.permissions.includes('contextMenus')) { - this.registeredContextMenus = false; + this.#registeredContextMenus = false; } }); } } - private alarmListener = (alarm: chrome.alarms.Alarm): void => { - if (alarm.name === Extension.ALARM_NAME) { - this.handleAutomationCheck(); + #alarmListener = (/** @type {chrome.alarms.Alarm} */alarm) => { + if (alarm.name === Extension.#ALARM_NAME) { + this.#handleAutomationCheck(); } }; - recalculateIsEnabled(): boolean { + recalculateIsEnabled() { if (!this.user.settings) { logWarn('Extension.isEnabled() was called before Extension.user.settings is available.'); return false; } const {automation} = this.user.settings; - let nextCheck: number; + /** @type {number} */ + let nextCheck; switch (automation) { case 'time': - this.isEnabled = isInTimeIntervalLocal(this.user.settings.time.activation, this.user.settings.time.deactivation); + this.#isEnabled = isInTimeIntervalLocal(this.user.settings.time.activation, this.user.settings.time.deactivation); nextCheck = nextTimeInterval(this.user.settings.time.activation, this.user.settings.time.deactivation); break; case 'system': @@ -111,9 +122,9 @@ export class Extension { } if (isFirefox) { // BUG: Firefox background page always matches initial color scheme. - this.isEnabled = this.wasLastColorSchemeDark == null + this.isEnabled = this.#wasLastColorSchemeDark == null ? isSystemDarkModeEnabled() - : this.wasLastColorSchemeDark; + : this.#wasLastColorSchemeDark; } else { this.isEnabled = isSystemDarkModeEnabled(); } @@ -128,23 +139,23 @@ export class Extension { break; } default: - this.isEnabled = this.user.settings.enabled; + this.#isEnabled = this.user.settings.enabled; break; } if (nextCheck) { - chrome.alarms.create(Extension.ALARM_NAME, {when: nextCheck}); + chrome.alarms.create(Extension.#ALARM_NAME, {when: nextCheck}); } - return this.isEnabled; + return this.#isEnabled; } async start() { await this.config.load({local: true}); await this.user.loadSettings(); - if (this.user.settings.enableContextMenus && !this.registeredContextMenus) { + if (this.user.settings.enableContextMenus && !this.#registeredContextMenus) { chrome.permissions.contains({permissions: ['contextMenus']}, (permitted) => { if (permitted) { - this.registerContextMenus(); + this.#registerContextMenus(); } else { logWarn('User has enabled context menus, but did not provide permission.'); } @@ -153,7 +164,7 @@ export class Extension { if (this.user.settings.syncSitesFixes) { await this.config.load({local: false}); } - this.onAppToggle(); + this.#onAppToggle(); logInfo('loaded', this.user.settings); if (isThunderbird) { @@ -163,26 +174,28 @@ export class Extension { } this.user.settings.fetchNews && this.news.subscribe(); - this.startBarrier.resolve(); + this.#startBarrier.resolve(); if (__DEBUG__) { const socket = new WebSocket(`ws://localhost:8894`); socket.onmessage = (e) => { - const respond = (message: {type: string; data?: ExtensionData | string | boolean | {[key: string]: string}; id?: number}) => socket.send(JSON.stringify(message)); + /** @type {(message: {type: string; data?: ExtensionData | string | boolean | {[key: string]: string}; id?: number}) => void} */ + const respond = (message) => socket.send(JSON.stringify(message)); try { - const message: {type: string; data: Partial | boolean | {[key: string]: string}; id: number} = JSON.parse(e.data); + /** @type {{type: string; data: Partial | boolean | {[key: string]: string}; id: number}} */ + const message = JSON.parse(e.data); switch (message.type) { case 'changeSettings': - this.changeSettings(message.data as Partial); + this.changeSettings(/** @type {Partial} */(message.data)); respond({type: 'changeSettings-response', id: message.id}); break; case 'collectData': - this.collectData().then((data) => { + this.#collectData().then((data) => { respond({type: 'collectData-response', id: message.id, data}); }); break; case 'changeLocalStorage': { - const data = message.data as {[key: string]: string}; + const data = /** @type {{[key: string]: string}} */(message.data); for (const key in data) { localStorage[key] = data[key]; } @@ -193,18 +206,20 @@ export class Extension { respond({type: 'getLocalStorage-response', id: message.id, data: localStorage ? JSON.stringify(localStorage) : null}); break; case 'changeChromeStorage': { - const region: 'local' | 'sync' = (message.data as any).region; - chrome.storage[region].set((message.data as any).data, () => respond({type: 'changeChromeStorage-response', id: message.id})); + /** @type {'local' | 'sync'} */ + const region = /** @type {any} */(message.data).region; + chrome.storage[region].set(/** @type {any} */(message.data).data, () => respond({type: 'changeChromeStorage-response', id: message.id})); break; } case 'getChromeStorage': { - const keys = (message.data as any).keys; - const region: 'local' | 'sync' = (message.data as any).region; + const keys = /** @type {any} */(message.data).keys; + /** @type {'local' | 'sync'} */ + const region = /** @type {any} */(message.data).region; chrome.storage[region].get(keys, (data) => respond({type: 'getChromeStorage-response', data, id: message.id})); break; } case 'setDataIsMigratedForTesting': - this.devtools.setDataIsMigratedForTesting(message.data as boolean); + this.devtools.setDataIsMigratedForTesting(/** @type {boolean} */(message.data)); respond({type: 'setDataIsMigratedForTesting-response', id: message.id}); break; } @@ -215,18 +230,21 @@ export class Extension { } } - private getMessengerAdapter(): ExtensionAdapter { + /** + * @returns {ExtensionAdapter} + */ + #getMessengerAdapter() { return { collect: async () => { - return await this.collectData(); + return await this.#collectData(); }, getActiveTabInfo: async () => { if (!this.user.settings) { await this.user.loadSettings(); } - await this.stateManager.loadState(); + await this.#stateManager.loadState(); const url = await this.tabs.getActiveTabURL(); - const info = this.getURLInfo(url); + const info = this.#getURLInfo(url); info.isInjected = await this.tabs.canAccessActiveTab(); return info; }, @@ -235,7 +253,7 @@ export class Extension { setShortcut: ({command, shortcut}) => this.setShortcut(command, shortcut), toggleURL: (url) => this.toggleURL(url), markNewsAsRead: async (ids) => await this.news.markAsRead(...ids), - onPopupOpen: () => this.popupOpeningListener && this.popupOpeningListener(), + onPopupOpen: () => this.#popupOpeningListener && this.#popupOpeningListener(), loadConfig: async (options) => await this.config.load(options), applyDevDynamicThemeFixes: (text) => this.devtools.applyDynamicThemeFixes(text), resetDevDynamicThemeFixes: () => this.devtools.resetDynamicThemeFixes(), @@ -246,16 +264,17 @@ export class Extension { }; } - private onCommandInternal = async (command: string, frameURL?: string) => { - if (this.startBarrier.isPending()) { - await this.startBarrier.entry(); + /** @type {(command: string, frameURL?: string) => Promise} */ + #onCommandInternal = async (command, frameURL) => { + if (this.#startBarrier.isPending()) { + await this.#startBarrier.entry(); } - this.stateManager.loadState(); + this.#stateManager.loadState(); switch (command) { case 'toggle': logInfo('Toggle command entered'); this.changeSettings({ - enabled: !this.isEnabled, + enabled: !this.#isEnabled, automation: '', }); break; @@ -281,14 +300,15 @@ export class Extension { // 75 is small enough to not notice it, and still catches when someone // is holding down a certain shortcut. - onCommand = debounce(75, this.onCommandInternal); + onCommand = debounce(75, this.#onCommandInternal); - private registerContextMenus() { + #registerContextMenus() { const onCommandToggle = async () => this.onCommand('toggle'); - const onCommandAddSite = async (data: chrome.contextMenus.OnClickData) => this.onCommand('addSite', data.frameUrl); + /** @type {(data: chrome.contextMenus.OnClickData) => Promise} */ + const onCommandAddSite = async (data) => this.onCommand('addSite', data.frameUrl); const onCommandSwitchEngine = async () => this.onCommand('switchEngine'); chrome.contextMenus.removeAll(() => { - this.registeredContextMenus = false; + this.#registeredContextMenus = false; chrome.contextMenus.create({ id: 'DarkReader-top', title: 'Dark Reader' @@ -318,31 +338,38 @@ export class Extension { title: msgSwitchEngine || 'Switch engine', onclick: onCommandSwitchEngine, }); - this.registeredContextMenus = true; + this.#registeredContextMenus = true; }); }); } - private async getShortcuts() { + async #getShortcuts() { const commands = await getCommands(); - return commands.reduce((map, cmd) => Object.assign(map, {[cmd.name]: cmd.shortcut}), {} as Shortcuts); + return commands.reduce((map, cmd) => Object.assign(map, {[cmd.name]: cmd.shortcut}), /** @type {Shortcuts} */({})); } - setShortcut(command: string, shortcut: string) { + /** + * @param {string} command + * @param {string} shortcut + */ + setShortcut(command, shortcut) { setShortcut(command, shortcut); } - private async collectData(): Promise { + /** + * @returns {Promise} + */ + async #collectData() { if (!this.user.settings) { await this.user.loadSettings(); } - await this.stateManager.loadState(); + await this.#stateManager.loadState(); return { - isEnabled: this.isEnabled, + isEnabled: this.#isEnabled, isReady: true, settings: this.user.settings, news: await this.news.getLatest(), - shortcuts: await this.getShortcuts(), + shortcuts: await this.#getShortcuts(), colorScheme: this.config.COLOR_SCHEMES_RAW, devtools: { dynamicFixesText: await this.devtools.getDynamicThemeFixesText(), @@ -355,7 +382,7 @@ export class Extension { }; } - private onNewsUpdate(news: News[]) { + #onNewsUpdate(/** @type {News[]} */news) { if (!this.icon) { this.icon = new IconManager(); } @@ -369,29 +396,37 @@ export class Extension { this.icon.hideBadge(); } - private getConnectionMessage(url: string, frameURL: string) { + /** + * @param {string} url + * @param {string} frameURL + * @returns {TabData | Promise} + */ + #getConnectionMessage(url, frameURL) { if (this.user.settings) { - return this.getTabMessage(url, frameURL); + return this.#getTabMessage(url, frameURL); } - return new Promise((resolve) => { - this.user.loadSettings().then(() => resolve(this.getTabMessage(url, frameURL))); + return new Promise((resolve) => { + this.user.loadSettings().then(() => resolve(this.#getTabMessage(url, frameURL))); }); } - private onColorSchemeChange = ({isDark}: {isDark: boolean}) => { + /** + * @param {{isDark: boolean}} data + */ + #onColorSchemeChange = ({isDark}) => { if (isFirefox) { this.wasLastColorSchemeDark = isDark; } if (this.user.settings.automation !== 'system') { return; } - this.handleAutomationCheck(); + this.#handleAutomationCheck(); }; - private handleAutomationCheck = () => { + #handleAutomationCheck = () => { if (this.user.settings.automationBehaviour === 'Scheme') { this.recalculateIsEnabled(); - if (this.isEnabled) { + if (this.#isEnabled) { // Dark this.changeSettings({theme: {...this.user.settings.theme, ...{mode: 1}}}); } else { @@ -400,27 +435,27 @@ export class Extension { } } else { // Toggle on/off - this.handleAutoCheck(); + this.#handleAutoCheck(); } }; - private async handleAutoCheck() { + async #handleAutoCheck() { if (!this.user.settings) { await this.user.loadSettings(); } - await this.stateManager.loadState(); + await this.#stateManager.loadState(); this.recalculateIsEnabled(); - const isEnabled = this.isEnabled; - if (this.wasEnabledOnLastCheck === null || this.wasEnabledOnLastCheck !== isEnabled) { - this.wasEnabledOnLastCheck = isEnabled; - this.onAppToggle(); + const isEnabled = this.#isEnabled; + if (this.#wasEnabledOnLastCheck === null || this.#wasEnabledOnLastCheck !== isEnabled) { + this.#wasEnabledOnLastCheck = isEnabled; + this.#onAppToggle(); this.tabs.sendMessage(); - this.reportChanges(); - this.stateManager.saveState(); + this.#reportChanges(); + this.#stateManager.saveState(); } } - changeSettings($settings: Partial) { + changeSettings(/** @type {Partial} */$settings) { const prev = {...this.user.settings}; this.user.set($settings); @@ -434,12 +469,12 @@ export class Extension { (prev.location.latitude !== this.user.settings.location.latitude) || (prev.location.longitude !== this.user.settings.location.longitude) ) { - this.onAppToggle(); + this.#onAppToggle(); } if (prev.syncSettings !== this.user.settings.syncSettings) { this.user.saveSyncSetting(this.user.settings.syncSettings); } - if (this.isEnabled && $settings.changeBrowserTheme != null && prev.changeBrowserTheme !== $settings.changeBrowserTheme) { + if (this.#isEnabled && $settings.changeBrowserTheme != null && prev.changeBrowserTheme !== $settings.changeBrowserTheme) { if ($settings.changeBrowserTheme) { setWindowTheme(this.user.settings.theme); } else { @@ -452,30 +487,30 @@ export class Extension { if (prev.enableContextMenus !== this.user.settings.enableContextMenus) { if (this.user.settings.enableContextMenus) { - this.registerContextMenus(); + this.#registerContextMenus(); } else { chrome.contextMenus.removeAll(); } } - this.onSettingsChanged(); + this.#onSettingsChanged(); } - setTheme($theme: Partial) { + setTheme(/** @type {Partial} */$theme) { this.user.set({theme: {...this.user.settings.theme, ...$theme}}); - if (this.isEnabled && this.user.settings.changeBrowserTheme) { + if (this.#isEnabled && this.user.settings.changeBrowserTheme) { setWindowTheme(this.user.settings.theme); } - this.onSettingsChanged(); + this.#onSettingsChanged(); } - private async reportChanges() { - const info = await this.collectData(); + async #reportChanges() { + const info = await this.#collectData(); this.messenger.reportChanges(info); } - toggleURL(url: string) { + toggleURL(/** @type {string} */url) { const isInDarkList = isURLInList(url, this.config.DARK_SITES); const siteList = isInDarkList ? this.user.settings.siteListEnabled.slice() : @@ -508,13 +543,13 @@ export class Extension { // Handle config changes // - private onAppToggle() { + #onAppToggle() { if (!this.icon) { this.icon = new IconManager(); } this.recalculateIsEnabled(); - if (this.isEnabled) { + if (this.#isEnabled) { this.icon.setActive(); if (this.user.settings.changeBrowserTheme) { setWindowTheme(this.user.settings.theme); @@ -527,19 +562,19 @@ export class Extension { } } - private async onSettingsChanged() { + async #onSettingsChanged() { if (!this.user.settings) { await this.user.loadSettings(); } - await this.stateManager.loadState(); - this.wasEnabledOnLastCheck = this.isEnabled; + await this.#stateManager.loadState(); + this.#wasEnabledOnLastCheck = this.#isEnabled; this.tabs.sendMessage(); - this.saveUserSettings(); - this.reportChanges(); - this.stateManager.saveState(); + this.#saveUserSettings(); + this.#reportChanges(); + this.#stateManager.saveState(); } - private onRemoteSettingsChange() { + #onRemoteSettingsChange() { // TODO: Requires proper handling and more testing // to prevent cycling across instances. } @@ -550,7 +585,11 @@ export class Extension { // //---------------------- - private getURLInfo(url: string): TabInfo { + /** + * @param {string} url + * @returns {TabInfo} + */ + #getURLInfo(url) { const {DARK_SITES} = this.config; const isInDarkList = isURLInList(url, DARK_SITES); const isProtected = !canInjectScript(url); @@ -562,9 +601,10 @@ export class Extension { }; } - private getTabMessage = (url: string, frameURL: string): TabData => { - const urlInfo = this.getURLInfo(url); - if (this.isEnabled && isURLEnabled(url, this.user.settings, urlInfo)) { + /** @type {(url: string, frameURL: string) => TabData} */ + #getTabMessage = (url, frameURL) => { + const urlInfo = this.#getURLInfo(url); + if (this.#isEnabled && isURLEnabled(url, this.user.settings, urlInfo)) { const custom = this.user.settings.customThemes.find(({url: urlList}) => isURLInList(url, urlList)); const preset = custom ? null : this.user.settings.presets.find(({urls}) => isURLInList(url, urls)); const theme = custom ? custom.theme : preset ? preset.theme : this.user.settings.theme; @@ -628,7 +668,7 @@ export class Extension { //------------------------------------- // User settings - private async saveUserSettings() { + async #saveUserSettings() { await this.user.saveSettings(); logInfo('saved', this.user.settings); } diff --git a/src/defaults.ts b/src/defaults.js similarity index 81% rename from src/defaults.ts rename to src/defaults.js index 335778f12b51..e39f66ee1cf6 100644 --- a/src/defaults.ts +++ b/src/defaults.js @@ -1,8 +1,11 @@ -import type {ParsedColorSchemeConfig} from './utils/colorscheme-parser'; -import type {Theme, UserSettings} from './definitions'; +// @ts-check import ThemeEngines from './generators/theme-engines'; import {isMacOS, isWindows} from './utils/platform'; +/** @typedef {import('./definitions').Theme} Theme */ +/** @typedef {import('./definitions').UserSettings} UserSettings */ +/** @typedef {import('./utils/colorscheme-parser').ParsedColorSchemeConfig} ParsedColorSchemeConfig */ + export const DEFAULT_COLORS = { darkScheme: { background: '#181a1b', @@ -14,7 +17,8 @@ export const DEFAULT_COLORS = { }, }; -export const DEFAULT_THEME: Theme = { +/** @type {Theme} */ +export const DEFAULT_THEME = { mode: 1, brightness: 100, contrast: 100, @@ -36,7 +40,8 @@ export const DEFAULT_THEME: Theme = { darkColorScheme: 'Default', }; -export const DEFAULT_COLORSCHEME: ParsedColorSchemeConfig = { +/** @type {ParsedColorSchemeConfig} */ +export const DEFAULT_COLORSCHEME = { light: { Default: { backgroundColor: DEFAULT_COLORS.lightScheme.background, @@ -51,7 +56,8 @@ export const DEFAULT_COLORSCHEME: ParsedColorSchemeConfig = { }, }; -export const DEFAULT_SETTINGS: UserSettings = { +/** @type {UserSettings} */ +export const DEFAULT_SETTINGS = { enabled: true, fetchNews: true, theme: DEFAULT_THEME, diff --git a/tasks/ts2js.js b/tasks/ts2js.js new file mode 100644 index 000000000000..32cd521bb7be --- /dev/null +++ b/tasks/ts2js.js @@ -0,0 +1,183 @@ +// @ts-check +const {exec} = require('child_process'); +// const fs = require('fs'); +const globby = require('globby'); +const {readFile, writeFile} = require('./utils'); + +/** + * @param {string} command + * @returns {Promise} + */ +function run(command) { + return new Promise((resolve, reject) => { + exec(command, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result.trim()); + } + }); + }); +} + +function getParenthesesRange(/** @type {string} */input, {open = '(', close = ')', srart = 0} = {}) { + const length = input.length; + let depth = 0; + let firstOpenIndex = -1; + for (let i = srart; i < length; i++) { + if (depth === 0) { + const openIndex = input.indexOf(open, i); + if (openIndex < 0) { + break; + } + firstOpenIndex = openIndex; + depth++; + i = openIndex; + } else { + const closingIndex = input.indexOf(close, i); + if (closingIndex < 0) { + break; + } + const openIndex = input.indexOf(open, i); + if (openIndex < 0 || closingIndex < openIndex) { + depth--; + if (depth === 0) { + return {start: firstOpenIndex, end: closingIndex + 1}; + } + i = closingIndex; + } else { + depth++; + i = openIndex; + } + } + } + return null; +} + +/** + * @param {string} aPath + * @param {string} bPath + * @returns {number} + */ +function compareByPath(aPath, bPath) { + const [a, b] = [aPath, bPath].map((path) => { + const slash = path.lastIndexOf('/'); + return {dir: path.substring(0, slash), file: path.substring(slash + 1)}; + }); + return a.dir.localeCompare(b.dir) || a.file.localeCompare(b.file); +} + +async function getFiles() { + return (await globby(['src/**/*.ts', 'src/**/*.tsx'])) + .sort(compareByPath) + .filter((file) => !file.endsWith('.d.ts')); +} + +const simpleTypes = [ + 'string', + 'number', + 'boolean', +]; + +/** + * @param {string} content + * @returns {string} + */ +function convert(content) { + const lines = content.split('\n'); + /** @type {string[]} */ + const results = []; + + // Find imports and type imports + /** @type {Array<{name: string; alias: string; path: string}>} */ + const importedTypes = []; + let lastImportIndex = -1; + lines.forEach((ln, i) => { + const isImport = ln.startsWith('import '); + const isTypeImport = ln.startsWith('import type '); + if (isTypeImport) { + const path = ln.substring(ln.indexOf(`'`) + 1, ln.lastIndexOf(`'`)); + ln + .substring(ln.indexOf('{') + 1, ln.indexOf('}')) + .split(',') + .map((part) => part.trim()) + .map((part) => { + const splitIndex = part.indexOf(' as '); + if (splitIndex < 0) { + importedTypes.push({name: part, alias: part, path}) + } else { + const name = part.substring(0, splitIndex); + const alias = part.substring(splitIndex + 5).trim(); + importedTypes.push({name, alias, path}); + } + }); + } else if (isImport) { + lastImportIndex = i; + } + }); + importedTypes.sort((a, b) => { + return compareByPath(a.path, b.path) || a.name.localeCompare(b.name); + }); + + results.push('// @ts-check'); + lines.forEach((ln, i) => { + const isImport = ln.startsWith('import '); + const isTypeImport = ln.startsWith('import type '); + if (isTypeImport) { + return; + } + if (isImport) { + results.push(ln); + if (i === lastImportIndex) { + results.push(''); + importedTypes.forEach(({name, alias, path}) => { + results.push(`/** @typedef {import('${path}')${name ? `.${name}` : '.default'}}${alias ? ` ${alias}` : ''} */`); + }); + } + return; + } + + const constTypeMatch = ln.match(/(^(( *).*[A-Za-z0-9_]+ )?(const|let) .*?)\: ([A-Za-z0-9_]+)( \=.*?)$/); + if (constTypeMatch) { + const m = constTypeMatch; + results.push(`${m[3]}/** @type {${m[5]}} */`); + results.push(`${m[1]}${m[6]}`); + return; + } + + const undefinedLetTypeMatch = ln.match(/(^( *)(.*[A-Za-z0-9_]+ )?(const|let) .*?)\: ([A-Za-z0-9_]+);$/); + if (undefinedLetTypeMatch) { + const m = undefinedLetTypeMatch; + results.push(`${m[2]}/** @type {${m[5]}} */`); + results.push(`${m[1]};`); + return; + } + + const constSetTypeMatch = ln.match(/^(( *)(.*? )?(const|let) [A-Za-z0-9_]+ \= new Set)<(.*?)>(\(\);)$/); + if (constSetTypeMatch) { + const m = constSetTypeMatch; + results.push(`${m[2]}/** @type {Set<${m[5]}>} */`); + results.push(`${m[1]}${m[6]}`); + return; + } + + results.push(ln); + }); + + return results.join('\n'); +} + +async function start() { + const files = await getFiles(); + for (const src of files) { + const dest = src.replace(/\.(ts|tsx)$/, '.js'); + console.log(dest); + const content = await readFile(src); + const converted = convert(content); + await run(`git mv ${src} ${dest}`); + await writeFile(dest, converted); + return; + } +} + +start(); From 6a5f547695e4eeff8ef6a196c249debae22297fa Mon Sep 17 00:00:00 2001 From: Alexander Shutau Date: Sat, 8 Jan 2022 17:56:21 +0000 Subject: [PATCH 2/4] Convert TSX to JS --- package-lock.json | 14 +- package.json | 2 +- .../devtools/components/{body.tsx => body.js} | 109 +++++---- src/ui/devtools/{index.tsx => index.js} | 27 ++- tasks/ts2js.js | 218 +++++++++++++----- 5 files changed, 253 insertions(+), 117 deletions(-) rename src/ui/devtools/components/{body.tsx => body.js} (52%) rename src/ui/devtools/{index.tsx => index.js} (61%) diff --git a/package-lock.json b/package-lock.json index 8e77ac3fec0b..f848fea3ee2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "karma-rollup-preprocessor": "7.0.7", "karma-safari-launcher": "1.0.0", "less": "4.1.2", - "malevic": "0.18.6", + "malevic": "0.19.0", "prettier": "2.5.1", "puppeteer-core": "13.0.1", "rollup": "2.62.0", @@ -8264,9 +8264,9 @@ } }, "node_modules/malevic": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.6.tgz", - "integrity": "sha512-Um4XRYJpVDhKjRRteiuHdDmcNbI5gX7URsXC6G+5Tk0Dai2W2RB39kg5C/M32IezNPudT+YsgApBh8JG6fIWrA==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.19.0.tgz", + "integrity": "sha512-InPlV4faMguPte33a9FM2Q9c/UPwinfRVR3IydSUdReFDRJi+5aThl8ckIjT7rPQ3ztNAY2cac8+pVZg2rGztA==", "dev": true }, "node_modules/map-age-cleaner": { @@ -18636,9 +18636,9 @@ } }, "malevic": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.6.tgz", - "integrity": "sha512-Um4XRYJpVDhKjRRteiuHdDmcNbI5gX7URsXC6G+5Tk0Dai2W2RB39kg5C/M32IezNPudT+YsgApBh8JG6fIWrA==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.19.0.tgz", + "integrity": "sha512-InPlV4faMguPte33a9FM2Q9c/UPwinfRVR3IydSUdReFDRJi+5aThl8ckIjT7rPQ3ztNAY2cac8+pVZg2rGztA==", "dev": true }, "map-age-cleaner": { diff --git a/package.json b/package.json index ae737773957f..b9ec732fdcc7 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "karma-rollup-preprocessor": "7.0.7", "karma-safari-launcher": "1.0.0", "less": "4.1.2", - "malevic": "0.18.6", + "malevic": "0.19.0", "prettier": "2.5.1", "puppeteer-core": "13.0.1", "rollup": "2.62.0", diff --git a/src/ui/devtools/components/body.tsx b/src/ui/devtools/components/body.js similarity index 52% rename from src/ui/devtools/components/body.tsx rename to src/ui/devtools/components/body.js index e6c0664911d1..3ba9a64f15f2 100644 --- a/src/ui/devtools/components/body.tsx +++ b/src/ui/devtools/components/body.js @@ -1,19 +1,27 @@ +// @ts-check import {m} from 'malevic'; -import {getContext} from 'malevic/dom'; +import {getContext, tags} from 'malevic/dom'; import {withState, useState} from 'malevic/state'; -import {Button, MessageBox, Overlay} from '../../controls'; +import {Button, MessageBox, Overlay as OverlayLegacy} from '../../controls'; import ThemeEngines from '../../../generators/theme-engines'; import {DEVTOOLS_DOCS_URL} from '../../../utils/links'; -import type {ExtWrapper, TabInfo} from '../../../definitions'; import {getCurrentThemePreset} from '../../popup/theme/utils'; import {isFirefox} from '../../../utils/platform'; -type BodyProps = ExtWrapper & {tab: TabInfo}; +/** @typedef {import('../../../definitions').ExtWrapper} ExtWrapper */ +/** @typedef {import('../../../definitions').TabInfo} TabInfo */ -function Body({data, tab, actions}: BodyProps) { +/** @typedef {ExtWrapper & {tab: TabInfo}} BodyProps */ + +const {body, header, img, h1, h3, strong, a, textarea, label, div, p} = tags; +const Overlay = (/** @type {any} */props, /** @type {Array} */...content) => m(OverlayLegacy, props, ...content); + +/** @type {Malevic.Component} */ +function Body({data, tab, actions}) { const context = getContext(); - const {state, setState} = useState({errorText: null as string}); - let textNode: HTMLTextAreaElement; + const {state, setState} = useState({errorText: /** @type {string} */(null)}); + /** @type {HTMLTextAreaElement} */ + let textNode; const previewButtonText = data.settings.previewNewDesign ? 'Switch to old design' : 'Preview new design'; const {theme} = getCurrentThemePreset({data, tab, actions}); @@ -21,21 +29,22 @@ function Body({data, tab, actions}: BodyProps) { ? { header: 'Static Theme Editor', fixesText: data.devtools.staticThemesText, - apply: (text: string) => actions.applyDevStaticThemes(text), + apply: (/** @type {string} */text) => actions.applyDevStaticThemes(text), reset: () => actions.resetDevStaticThemes(), } : theme.engine === ThemeEngines.cssFilter || theme.engine === ThemeEngines.svgFilter ? { header: 'Inversion Fix Editor', fixesText: data.devtools.filterFixesText, - apply: (text: string) => actions.applyDevInversionFixes(text), + apply: (/** @type {string} */text) => actions.applyDevInversionFixes(text), reset: () => actions.resetDevInversionFixes(), } : { header: 'Dynamic Theme Editor', fixesText: data.devtools.dynamicFixesText, - apply: (text: string) => actions.applyDevDynamicThemeFixes(text), + apply: (/** @type {string} */text) => actions.applyDevDynamicThemeFixes(text), reset: () => actions.resetDevDynamicThemeFixes(), }); - function onTextRender(node: HTMLTextAreaElement) { + /** @type {(node: HTMLTextAreaElement) => void} */ + function onTextRender(node) { textNode = node; if (!state.errorText) { textNode.value = wrapper.fixesText; @@ -84,11 +93,13 @@ function Body({data, tab, actions}: BodyProps) { } const dialog = context && context.store.isDialogVisible ? ( - + MessageBox( + { + caption: 'Are you sure you want to remove current changes? You cannot restore them later.', + onOK: reset, + onCancel: hideDialog, + }, + ) ) : null; function reset() { @@ -102,36 +113,42 @@ function Body({data, tab, actions}: BodyProps) { } return ( - -
- -

Developer Tools

-
-

{wrapper.header}

-