extension: Lint after TypeScript migration

This commit is contained in:
relrelb 2021-04-23 21:29:38 +03:00 committed by Mike Welsh
parent cfc57a2e1d
commit 488e4421e9
10 changed files with 201 additions and 67 deletions

View File

@ -1,9 +0,0 @@
{
"env": {
"browser": true,
"webextensions": true
},
"globals": {
"__webpack_public_path__": true
}
}

View File

@ -0,0 +1,17 @@
{
"env": {
"browser": true,
"webextensions": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off"
}
}

View File

@ -1,10 +1,15 @@
import * as utils from "./utils"; import * as utils from "./utils";
type WebResponseHeadersDetails = chrome.webRequest.WebResponseHeadersDetails | browser.webRequest._OnHeadersReceivedDetails; function isSwf(
details:
function isSwf(details: WebResponseHeadersDetails) { | chrome.webRequest.WebResponseHeadersDetails
| browser.webRequest._OnHeadersReceivedDetails
) {
// TypeScript doesn't compile without this explicit type delaration. // TypeScript doesn't compile without this explicit type delaration.
const headers: (chrome.webRequest.HttpHeader | browser.webRequest._HttpHeaders)[] = details.responseHeaders!; const headers: (
| chrome.webRequest.HttpHeader
| browser.webRequest._HttpHeaders
)[] = details.responseHeaders!;
const typeHeader = headers.find( const typeHeader = headers.find(
({ name }) => name.toLowerCase() === "content-type" ({ name }) => name.toLowerCase() === "content-type"
); );
@ -12,8 +17,8 @@ function isSwf(details: WebResponseHeadersDetails) {
return false; return false;
} }
const mime = typeHeader.value! const mime = typeHeader
.toLowerCase() .value!.toLowerCase()
.match(/^\s*(.*?)\s*(?:;.*)?$/)![1]; .match(/^\s*(.*?)\s*(?:;.*)?$/)![1];
// Some sites (e.g. swfchan.net) might (wrongly?) send octet-stream, so check file extension too. // Some sites (e.g. swfchan.net) might (wrongly?) send octet-stream, so check file extension too.
@ -26,7 +31,11 @@ function isSwf(details: WebResponseHeadersDetails) {
return mime === "application/x-shockwave-flash"; return mime === "application/x-shockwave-flash";
} }
function onHeadersReceived(details: WebResponseHeadersDetails) { function onHeadersReceived(
details:
| chrome.webRequest.WebResponseHeadersDetails
| browser.webRequest._OnHeadersReceivedDetails
) {
if (isSwf(details)) { if (isSwf(details)) {
const baseUrl = utils.runtime.getURL("player.html"); const baseUrl = utils.runtime.getURL("player.html");
return { redirectUrl: `${baseUrl}?url=${details.url}` }; return { redirectUrl: `${baseUrl}?url=${details.url}` };
@ -57,7 +66,7 @@ function disable() {
enable(); enable();
} }
utils.storage.onChanged.addListener((changes: any, namespace: string) => { utils.storage.onChanged.addListener((changes, namespace) => {
if ( if (
namespace === "sync" && namespace === "sync" &&
Object.prototype.hasOwnProperty.call(changes, "ruffleEnable") Object.prototype.hasOwnProperty.call(changes, "ruffleEnable")

View File

@ -5,12 +5,15 @@ function camelize(s: string) {
} }
export interface Options { export interface Options {
ruffleEnable: boolean, ruffleEnable: boolean;
ignoreOptout: boolean, ignoreOptout: boolean;
} }
function getBooleanElements() { function getBooleanElements() {
const elements: Record<string, any> = {}; const elements: Record<
string,
{ option: Element; checkbox: HTMLInputElement; label: HTMLLabelElement }
> = {};
for (const option of document.getElementsByClassName("option")) { for (const option of document.getElementsByClassName("option")) {
const [checkbox] = option.getElementsByTagName("input"); const [checkbox] = option.getElementsByTagName("input");
if (checkbox.type !== "checkbox") { if (checkbox.type !== "checkbox") {
@ -23,7 +26,9 @@ function getBooleanElements() {
return elements; return elements;
} }
export async function bindBooleanOptions(onChange?: (options: Options) => void) { export async function bindBooleanOptions(
onChange?: (options: Options) => void
): Promise<void> {
const elements = getBooleanElements(); const elements = getBooleanElements();
// Bind initial values. // Bind initial values.
@ -36,7 +41,7 @@ export async function bindBooleanOptions(onChange?: (options: Options) => void)
// TODO: click/change/input? // TODO: click/change/input?
checkbox.addEventListener("click", () => { checkbox.addEventListener("click", () => {
const value = checkbox.checked; const value = checkbox.checked;
options[key] = value; options[key as keyof Options] = value;
utils.storage.sync.set({ [key]: value }); utils.storage.sync.set({ [key]: value });
}); });
@ -50,7 +55,7 @@ export async function bindBooleanOptions(onChange?: (options: Options) => void)
} }
// Listen for future changes. // Listen for future changes.
utils.storage.onChanged.addListener((changes: Object, namespace: string) => { utils.storage.onChanged.addListener((changes, namespace) => {
if (namespace !== "sync") { if (namespace !== "sync") {
return; return;
} }
@ -60,7 +65,7 @@ export async function bindBooleanOptions(onChange?: (options: Options) => void)
continue; continue;
} }
elements[key].checkbox.checked = option.newValue; elements[key].checkbox.checked = option.newValue;
options[key] = option.newValue; options[key as keyof Options] = option.newValue;
} }
if (onChange) { if (onChange) {

View File

@ -20,7 +20,10 @@
import * as utils from "./utils"; import * as utils from "./utils";
const pendingMessages: ({ resolve(value: any): void, reject(reason?: any): void } | null)[] = []; const pendingMessages: ({
resolve(value: unknown): void;
reject(reason?: unknown): void;
} | null)[] = [];
const uniqueMessageSuffix = Math.floor(Math.random() * 100000000000); const uniqueMessageSuffix = Math.floor(Math.random() * 100000000000);
/** /**
@ -28,7 +31,7 @@ const uniqueMessageSuffix = Math.floor(Math.random() * 100000000000);
* @param {*} data - JSON-serializable data to send to main world. * @param {*} data - JSON-serializable data to send to main world.
* @returns {Promise<*>} JSON-serializable response from main world. * @returns {Promise<*>} JSON-serializable response from main world.
*/ */
function sendMessageToPage(data: any): Promise<any> { function sendMessageToPage(data: unknown): Promise<unknown> {
const message = { const message = {
type: `FROM_RUFFLE${uniqueMessageSuffix}`, type: `FROM_RUFFLE${uniqueMessageSuffix}`,
index: pendingMessages.length, index: pendingMessages.length,
@ -87,6 +90,12 @@ function checkPageOptout(): boolean {
return false; return false;
} }
declare global {
interface Document {
xmlVersion: string | null;
}
}
(async () => { (async () => {
const options = await utils.getOptions(["ruffleEnable", "ignoreOptout"]); const options = await utils.getOptions(["ruffleEnable", "ignoreOptout"]);
const pageOptout = checkPageOptout(); const pageOptout = checkPageOptout();
@ -96,7 +105,7 @@ function checkPageOptout(): boolean {
!window.RufflePlayer && !window.RufflePlayer &&
(options.ignoreOptout || !pageOptout); (options.ignoreOptout || !pageOptout);
utils.runtime.onMessage.addListener((message: any, _sender: any, sendResponse: (options: Object) => void) => { utils.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (shouldLoad) { if (shouldLoad) {
sendMessageToPage(message).then((response) => { sendMessageToPage(message).then((response) => {
sendResponse({ sendResponse({

View File

@ -1 +1,2 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
declare let __webpack_public_path__: string; declare let __webpack_public_path__: string;

View File

@ -1,9 +1,15 @@
import { PublicAPI, SourceAPI, publicPath, Letterbox, LogLevel } from "ruffle-core"; import {
PublicAPI,
SourceAPI,
publicPath,
Letterbox,
LogLevel,
} from "ruffle-core";
const api = PublicAPI.negotiate( const api = PublicAPI.negotiate(
window.RufflePlayer!, window.RufflePlayer!,
"local", "local",
new SourceAPI("local"), new SourceAPI("local")
); );
window.RufflePlayer = api; window.RufflePlayer = api;
__webpack_public_path__ = publicPath(api.config, "local"); __webpack_public_path__ = publicPath(api.config, "local");

View File

@ -22,7 +22,9 @@ const STATUS_COLORS = {
"status_result_disabled": "gray", "status_result_disabled": "gray",
}; };
async function queryTabStatus(listener: (status: keyof typeof STATUS_COLORS) => void) { async function queryTabStatus(
listener: (status: keyof typeof STATUS_COLORS) => void
) {
listener("status_init"); listener("status_init");
let tabs: chrome.tabs.Tab[] | browser.tabs.Tab[]; let tabs: chrome.tabs.Tab[] | browser.tabs.Tab[];
@ -77,7 +79,8 @@ async function queryTabStatus(listener: (status: keyof typeof STATUS_COLORS) =>
optionsChanged(); optionsChanged();
} }
function objectsEqual<T extends Record<string, any>>(x: T, y: T) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
function objectsEqual(x: any, y: any) {
for (const [key, value] of Object.entries(x)) { for (const [key, value] of Object.entries(x)) {
if (y[key] !== value) { if (y[key] !== value) {
return false; return false;
@ -115,14 +118,20 @@ window.addEventListener("DOMContentLoaded", () => {
optionsChanged(); optionsChanged();
}); });
statusIndicator = document.getElementById("status-indicator") as HTMLDivElement; statusIndicator = document.getElementById(
"status-indicator"
) as HTMLDivElement;
statusText = document.getElementById("status-text") as HTMLSpanElement; statusText = document.getElementById("status-text") as HTMLSpanElement;
const optionsButton = document.getElementById("options-button") as HTMLButtonElement; const optionsButton = document.getElementById(
"options-button"
) as HTMLButtonElement;
optionsButton.textContent = utils.i18n.getMessage("open_settings_page"); optionsButton.textContent = utils.i18n.getMessage("open_settings_page");
optionsButton.addEventListener("click", () => utils.openOptionsPage()); optionsButton.addEventListener("click", () => utils.openOptionsPage());
reloadButton = document.getElementById("reload-button") as HTMLButtonElement; reloadButton = document.getElementById(
"reload-button"
) as HTMLButtonElement;
reloadButton.textContent = utils.i18n.getMessage("action_reload"); reloadButton.textContent = utils.i18n.getMessage("action_reload");
reloadButton.addEventListener("click", async () => { reloadButton.addEventListener("click", async () => {
await utils.tabs.reload(activeTab.id!); await utils.tabs.reload(activeTab.id!);

View File

@ -3,7 +3,7 @@ import { PublicAPI, SourceAPI, publicPath } from "ruffle-core";
const api = PublicAPI.negotiate( const api = PublicAPI.negotiate(
window.RufflePlayer!, window.RufflePlayer!,
"extension", "extension",
new SourceAPI("extension"), new SourceAPI("extension")
); );
window.RufflePlayer = api; window.RufflePlayer = api;
__webpack_public_path__ = publicPath(api.config, "extension"); __webpack_public_path__ = publicPath(api.config, "extension");
@ -34,7 +34,11 @@ if (uniqueMessageSuffix) {
const { type, index, data } = event.data; const { type, index, data } = event.data;
if (type === `FROM_RUFFLE${uniqueMessageSuffix}`) { if (type === `FROM_RUFFLE${uniqueMessageSuffix}`) {
// Ping back. // Ping back.
const message = { type: `TO_RUFFLE${uniqueMessageSuffix}`, index, data }; const message = {
type: `TO_RUFFLE${uniqueMessageSuffix}`,
index,
data,
};
window.postMessage(message, "*"); window.postMessage(message, "*");
} }
}); });

View File

@ -1,19 +1,66 @@
import { Options } from "./common";
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
ruffleEnable: true, ruffleEnable: true,
ignoreOptout: false, ignoreOptout: false,
}; };
export let i18n: any; export let i18n: {
export let storage: any; getMessage(name: string): string;
export let tabs: {
reload(tabId: number): Promise<void>,
query(query: chrome.tabs.QueryInfo & browser.tabs._QueryQueryInfo): Promise<chrome.tabs.Tab[] | browser.tabs.Tab[]>,
sendMessage(tabId: number, message: any, options?: chrome.tabs.MessageSendOptions & browser.tabs._SendMessageOptions): Promise<any>,
}; };
export let runtime: any; export let storage: {
export let openOptionsPage: any; local: {
get(keys: string[]): Promise<Record<string, unknown>>;
remove(keys: string[]): Promise<void>;
set(items: Record<string, unknown>): Promise<void>;
};
sync: {
get(keys: string[]): Promise<Record<string, unknown>>;
remove(keys: string[]): Promise<void>;
set(items: Record<string, unknown>): Promise<void>;
};
onChanged: {
addListener(
listener: (
changes:
| Record<string, chrome.storage.StorageChange>
| Record<string, browser.storage.StorageChange>,
areaName: string
) => void
): void;
};
};
export let tabs: {
reload(tabId: number): Promise<void>;
query(
query: chrome.tabs.QueryInfo & browser.tabs._QueryQueryInfo
): Promise<chrome.tabs.Tab[] | browser.tabs.Tab[]>;
sendMessage(
tabId: number,
message: unknown,
options?: chrome.tabs.MessageSendOptions &
browser.tabs._SendMessageOptions
): Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
};
export let runtime: {
onMessage: {
addListener(
listener: (
message: unknown,
sender:
| chrome.runtime.MessageSender
| browser.runtime.MessageSender,
sendResponse: (response?: unknown) => void
) => void
): void;
};
getURL(path: string): string;
};
export let openOptionsPage: () => Promise<void>;
function promisify<T>(func: (callback: (result?: T) => void) => void): Promise<T> { function promisify<T>(
func: (callback: (result?: T) => void) => void
): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
func((result) => { func((result) => {
const error = chrome.runtime.lastError; const error = chrome.runtime.lastError;
@ -37,38 +84,59 @@ if (typeof chrome !== "undefined") {
promisify((cb) => chrome.storage.local.get(keys, cb)), promisify((cb) => chrome.storage.local.get(keys, cb)),
remove: (keys: string[]) => remove: (keys: string[]) =>
promisify((cb) => chrome.storage.local.remove(keys, cb)), promisify((cb) => chrome.storage.local.remove(keys, cb)),
set: (items: object) => set: (items: Record<string, unknown>) =>
promisify((cb) => chrome.storage.local.set(items, cb)), promisify((cb) => chrome.storage.local.set(items, cb)),
}, },
sync: { sync: {
get: (keys: string[]) => promisify((cb) => chrome.storage.sync.get(keys, cb)), get: (keys: string[]) =>
promisify((cb) => chrome.storage.sync.get(keys, cb)),
remove: (keys: string[]) => remove: (keys: string[]) =>
promisify((cb) => chrome.storage.sync.remove(keys, cb)), promisify((cb) => chrome.storage.sync.remove(keys, cb)),
set: (items: object) => set: (items: Record<string, unknown>) =>
promisify((cb) => chrome.storage.sync.set(items, cb)), promisify((cb) => chrome.storage.sync.set(items, cb)),
}, },
onChanged: { onChanged: {
addListener: (listener: (changes: chrome.storage.StorageChange, areaName: string) => void) => addListener: (
chrome.storage.onChanged.addListener(listener), listener: (
changes: Record<string, chrome.storage.StorageChange>,
areaName: string
) => void
) => chrome.storage.onChanged.addListener(listener),
}, },
}; };
tabs = { tabs = {
reload: (tabId: number) => promisify((cb) => chrome.tabs.reload(tabId, undefined, cb)), reload: (tabId: number) =>
query: (query: chrome.tabs.QueryInfo) => promisify((cb) => chrome.tabs.query(query, cb)), promisify((cb) => chrome.tabs.reload(tabId, undefined, cb)),
sendMessage: (tabId: number, message: any, options: chrome.tabs.MessageSendOptions) => query: (query: chrome.tabs.QueryInfo) =>
promisify((cb) => chrome.tabs.sendMessage(tabId, message, options, cb)), promisify((cb) => chrome.tabs.query(query, cb)),
sendMessage: (
tabId: number,
message: unknown,
options: chrome.tabs.MessageSendOptions
) =>
promisify((cb) =>
chrome.tabs.sendMessage(tabId, message, options, cb)
),
}; };
runtime = { runtime = {
onMessage: { onMessage: {
addListener: (listener: (message: any, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => void) => addListener: (
chrome.runtime.onMessage.addListener(listener), listener: (
message: unknown,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: unknown) => void
) => void
) => chrome.runtime.onMessage.addListener(listener),
}, },
getURL: (path: string) => chrome.runtime.getURL(path), getURL: (path: string) => chrome.runtime.getURL(path),
}; };
openOptionsPage = () => chrome.tabs.create({ url: "/options.html" }); openOptionsPage = () =>
promisify((cb: () => void) =>
chrome.tabs.create({ url: "/options.html" }, cb)
);
} else if (typeof browser !== "undefined") { } else if (typeof browser !== "undefined") {
i18n = { i18n = {
getMessage: (name: string) => browser.i18n.getMessage(name), getMessage: (name: string) => browser.i18n.getMessage(name),
@ -78,30 +146,45 @@ if (typeof chrome !== "undefined") {
local: { local: {
get: (keys: string[]) => browser.storage.local.get(keys), get: (keys: string[]) => browser.storage.local.get(keys),
remove: (keys: string[]) => browser.storage.local.remove(keys), remove: (keys: string[]) => browser.storage.local.remove(keys),
set: (items: Record<string, any>) => browser.storage.local.set(items), set: (items: Record<string, unknown>) =>
browser.storage.local.set(items),
}, },
sync: { sync: {
get: (keys: string[]) => browser.storage.sync.get(keys), get: (keys: string[]) => browser.storage.sync.get(keys),
remove: (keys: string[]) => browser.storage.sync.remove(keys), remove: (keys: string[]) => browser.storage.sync.remove(keys),
set: (items: Record<string, any>) => browser.storage.sync.set(items), set: (items: Record<string, unknown>) =>
browser.storage.sync.set(items),
}, },
onChanged: { onChanged: {
addListener: (listener: (changes: Record<string, browser.storage.StorageChange>, areaName: string) => void) => addListener: (
browser.storage.onChanged.addListener(listener), listener: (
changes: Record<string, browser.storage.StorageChange>,
areaName: string
) => void
) => browser.storage.onChanged.addListener(listener),
}, },
}; };
tabs = { tabs = {
reload: (tabId: number) => browser.tabs.reload(tabId), reload: (tabId: number) => browser.tabs.reload(tabId),
query: (query: browser.tabs._QueryQueryInfo) => browser.tabs.query(query), query: (query: browser.tabs._QueryQueryInfo) =>
sendMessage: (tabId: number, message: any, options: browser.tabs._SendMessageOptions) => browser.tabs.query(query),
browser.tabs.sendMessage(tabId, message, options), sendMessage: (
tabId: number,
message: unknown,
options: browser.tabs._SendMessageOptions
) => browser.tabs.sendMessage(tabId, message, options),
}; };
runtime = { runtime = {
onMessage: { onMessage: {
addListener: (listener: (message: any, sender: browser.runtime.MessageSender, sendResponse: (response?: any) => void) => boolean | Promise<any> | void) => addListener: (
browser.runtime.onMessage.addListener(listener), listener: (
message: unknown,
sender: browser.runtime.MessageSender,
sendResponse: (response?: unknown) => void
) => boolean | Promise<unknown> | void
) => browser.runtime.onMessage.addListener(listener),
}, },
getURL: (path: string) => browser.runtime.getURL(path), getURL: (path: string) => browser.runtime.getURL(path),
}; };
@ -111,7 +194,7 @@ if (typeof chrome !== "undefined") {
throw new Error("Extension API not found."); throw new Error("Extension API not found.");
} }
export async function getOptions(keys: string[]) { export async function getOptions(keys: string[]): Promise<Options> {
const options = await storage.sync.get(keys); const options = await storage.sync.get(keys);
// Copy over default options if they don't exist yet. // Copy over default options if they don't exist yet.