Skip to content

Commit 9be2846

Browse files
committed
feat: bunch of small additions. Making it more compatible with SIH
1 parent 361ecd5 commit 9be2846

File tree

16 files changed

+229
-59
lines changed

16 files changed

+229
-59
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Want to know if your favorite chrome extension is compatible with Extendium? Che
2424

2525
Once installed it should just work out of the box.
2626

27+
## Known features that are currently not supported
28+
- context menus
29+
- alarms
30+
- notifications
31+
- permission control
32+
- global keyboard shortcuts
33+
- anything to do with tabs
34+
2735
## Contributing
2836

2937
Contributions are welcome! Please feel free to submit a Pull Request.

backend/main.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -150,34 +150,35 @@ def PrepareExtensionFiles():
150150
for ext_folder in os.listdir(extensions_dir):
151151
ext_path = os.path.join(extensions_dir, ext_folder)
152152
if os.path.isdir(ext_path):
153+
ext_folder = ext_folder.replace('\\', '/')
154+
full_dir_path = f"{extension_url}/{ext_folder}"
155+
# Define file processing rules
156+
processing_rules = {
157+
'.css': [
158+
{'pattern': r'url\((?!")chrome-extension:\/\/__MSG_@@extension_id__\/(.+?)\)', 'replacement': f"url(\"{full_dir_path}/\\g<1>\")", 'is_regex': True},
159+
{'pattern': "url('/", 'replacement': f"url('{full_dir_path}/"},
160+
{'pattern': r"url\((['\"])chrome-extension://__MSG_@@extension_id__/", 'replacement': f"url(\g<1>{full_dir_path}/", 'is_regex': True},
161+
],
162+
'.html': [
163+
{'pattern': "href=\"/", 'replacement': f"href=\"{full_dir_path}/"},
164+
{'pattern': "src=\"/", 'replacement': f"src=\"{full_dir_path}/"},
165+
],
166+
'.js': [
167+
{'pattern': "globalThis.chrome", 'replacement': "chrome"},
168+
]
169+
}
170+
171+
# Handle root folder references in JS files
172+
ext_root_folders = [d for d in os.listdir(ext_path) if os.path.isdir(os.path.join(ext_path, d))]
173+
for dir_name in ext_root_folders:
174+
processing_rules['.js'].append({'pattern': f"(['\"])\/{re.escape(dir_name)}", 'replacement': f"\\g<1>{full_dir_path}/{dir_name}", 'is_regex': True})
175+
processing_rules['.css'].append({'pattern': f"url\((['\"])\/{re.escape(dir_name)}", 'replacement': f"url(\\g<1>{full_dir_path}/{dir_name}", 'is_regex': True})
176+
processing_rules['.css'].append({'pattern': f"url\((/{re.escape(dir_name)}/.+?)\)", 'replacement': f"url(\"{full_dir_path}\\g<1>\")", 'is_regex': True})
177+
153178
for root, _, files in os.walk(ext_path):
154179
for file in files:
155180
file_extension = os.path.splitext(file)[1].lower()
156181
file_path = os.path.join(root, file)
157-
ext_folder = ext_folder.replace('\\', '/')
158-
159-
# Define file processing rules
160-
processing_rules = {
161-
'.css': [
162-
{'pattern': r'url\((?!")chrome-extension:\/\/__MSG_@@extension_id__\/(.+?)\)', 'replacement': f"url(\"{extension_url}/{ext_folder}/\\g<1>\")", 'is_regex': True},
163-
{'pattern': "url('/", 'replacement': f"url('{extension_url}/{ext_folder}/"},
164-
{'pattern': "url(\"chrome-extension://__MSG_@@extension_id__/", 'replacement': f"url(\"{extension_url}/{ext_folder}/"},
165-
],
166-
'.html': [
167-
{'pattern': "href=\"/", 'replacement': f"href=\"{extension_url}/{ext_folder}/"},
168-
{'pattern': "src=\"/", 'replacement': f"src=\"{extension_url}/{ext_folder}/"},
169-
],
170-
'.js': [
171-
{'pattern': "globalThis.chrome", 'replacement': "chrome"}
172-
]
173-
}
174-
175-
# Handle root folder references in JS files
176-
if file_extension == '.js':
177-
ext_root_folders = [d for d in os.listdir(ext_path) if os.path.isdir(os.path.join(ext_path, d))]
178-
for dir_name in ext_root_folders:
179-
processing_rules['.js'].append({'pattern': f"'/{dir_name}", 'replacement': f"'{extension_url}/{ext_folder}/{dir_name}"})
180-
181182

182183
# Process file if we have rules for its extension
183184
if file_extension in processing_rules:

eslint.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import tseslint from 'typescript-eslint';
88

99
export default tseslint.config(
1010
{ files: ['**/*.{js,mjs,cjs,ts}'] },
11-
globalIgnores(['.millennium/', '.venv/', '._extensions/']),
11+
globalIgnores(['.millennium/', '.venv/', '._extensions/', 'webkit/chromeInjectionContent.js']),
1212
{
1313
languageOptions: {
1414
globals: globals.browser,
@@ -42,7 +42,7 @@ export default tseslint.config(
4242
}),
4343
{
4444
plugins: {
45-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
45+
4646
perfectionist,
4747
},
4848
rules: {

frontend/browser/corsFetch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export function patchFetch(window: Window): void {
1818
const oldFetch = window.fetch;
1919

2020
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
21-
const corsCache = JSON.parse(localStorage.getItem(corsCacheKey) ?? '[]') as string[];
22-
2321
const baseUrl = input.toString()
2422
.replace('https://', '')
2523
.replace('http://', '')
@@ -29,6 +27,8 @@ export function patchFetch(window: Window): void {
2927
return credentialsFetch(input, init);
3028
}
3129

30+
const corsCache = JSON.parse(localStorage.getItem(corsCacheKey) ?? '[]') as string[];
31+
3232
if (corsCache.includes(baseUrl)) {
3333
return corsFetch(oldFetch, input, init);
3434
}

frontend/browser/createChrome.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
12
/* eslint-disable @typescript-eslint/no-empty-function */
23
import { ChromeEvent } from '../extension/ChromeEvent';
34
import { Extension } from '../extension/Extension';
@@ -27,6 +28,9 @@ export function createChrome(context: string, extension: Extension): typeof wind
2728
extension: createExtensionType(extension, logger),
2829
contextMenus: createContextMenusType(extension, logger),
2930
commands: createCommandsType(extension, logger),
31+
notifications: createNotificationsType(extension, logger),
32+
webRequest: createWebRequestType(extension, logger),
33+
declarativeNetRequest: createDeclarativeNetRequestType(extension, logger),
3034
};
3135

3236
return chromeObj;
@@ -90,6 +94,8 @@ function createRuntimeType(extension: Extension, logger: Logger): typeof chrome.
9094

9195
return port;
9296
},
97+
onConnect: new ChromeEvent<(port: chrome.runtime.Port) => void>(),
98+
onConnectExternal: new ChromeEvent<(port: chrome.runtime.Port) => void>(),
9399
};
94100
}
95101

@@ -185,6 +191,7 @@ function createWindowsType(extension: Extension, logger: Logger): typeof chrome.
185191

186192
return Promise.resolve([]);
187193
},
194+
onBoundsChanged: new ChromeEvent<(window: chrome.windows.Window) => void>(),
188195
};
189196
}
190197

@@ -273,3 +280,57 @@ function createCommandsType(extension: Extension, logger: Logger): typeof chrome
273280
},
274281
};
275282
}
283+
284+
/**
285+
* @see https://developer.chrome.com/docs/extensions/reference/api/notifications
286+
*/
287+
function createNotificationsType(extension: Extension, logger: Logger): typeof chrome.notifications {
288+
// TODO: implement
289+
return {
290+
create: (...args: unknown[]): string | number => {
291+
console.error('notifications.create not implemented', args);
292+
293+
return -1;
294+
},
295+
onClosed: new ChromeEvent<(notificationId: string) => void>(),
296+
onButtonClicked: new ChromeEvent<(notificationId: string) => void>(),
297+
onClicked: new ChromeEvent<(notificationId: string) => void>(),
298+
};
299+
}
300+
301+
/**
302+
* @see https://developer.chrome.com/docs/extensions/reference/api/webRequest
303+
*/
304+
function createWebRequestType(extension: Extension, logger: Logger): typeof chrome.webRequest {
305+
// TODO: implement
306+
return {
307+
onBeforeRequest: new ChromeEvent<(details: chrome.webRequest.WebRequestDetails) => void>(),
308+
onBeforeSendHeaders: new ChromeEvent<(details: chrome.webRequest.WebRequestDetails) => void>(),
309+
onHeadersReceived: new ChromeEvent<(details: chrome.webRequest.WebRequestDetails) => void>(),
310+
onCompleted: new ChromeEvent<(details: chrome.webRequest.WebRequestDetails) => void>(),
311+
onErrorOccurred: new ChromeEvent<(details: chrome.webRequest.WebRequestDetails) => void>(),
312+
};
313+
}
314+
315+
/**
316+
* @see https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest
317+
*/
318+
function createDeclarativeNetRequestType(extension: Extension, logger: Logger): typeof chrome.declarativeNetRequest {
319+
// TODO: implement
320+
return {
321+
getDynamicRules: async (callback?: (rules: chrome.declarativeNetRequest.Rule[]) => void): Promise<chrome.declarativeNetRequest.Rule[]> => {
322+
logger.log('declarativeNetRequest.getDynamicRules');
323+
324+
callback?.([]);
325+
326+
return Promise.resolve([]);
327+
},
328+
updateDynamicRules: async (_options: chrome.declarativeNetRequest.UpdateRuleOptions, callback?: Function): Promise<void> => {
329+
logger.log('declarativeNetRequest.updateDynamicRules');
330+
331+
callback?.();
332+
333+
return Promise.resolve();
334+
},
335+
};
336+
}

frontend/browser/injectBrowser.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable func-names */
22
import { ConfirmModal, showModal } from '@steambrew/client';
33
import React from 'react';
4+
import { loadScript } from 'shared';
45
import { Extension } from '../extension/Extension';
56
import { patchFetch } from './corsFetch';
67
import { createChrome } from './createChrome';
@@ -17,7 +18,7 @@ export function injectBrowser(context: string, window: Window, extension: Extens
1718
window.onerror = function (this: WindowEventHandlers, ...args: unknown[]): boolean {
1819
console.error(`Error in ${extension.manifest.name} - ${context}`, ...args);
1920

20-
extension.errors.push(`Error in "${extension.manifest.name} - ${context}": ${args.join(' ')}`);
21+
extension.logger.errors.push(`Error in "${extension.manifest.name} - ${context}": ${args.join(' ')}`);
2122

2223
// @ts-expect-error ignore
2324
return originalOnError?.call(this, ...args);
@@ -28,7 +29,7 @@ export function injectBrowser(context: string, window: Window, extension: Extens
2829
window.onunhandledrejection = function (this: WindowEventHandlers, event): unknown {
2930
console.error(`Error in ${extension.manifest.name} - ${context}`, event);
3031

31-
extension.errors.push(`Error in "${extension.manifest.name} - ${context}": ${event.reason}`);
32+
extension.logger.errors.push(`Error in "${extension.manifest.name} - ${context}": ${event.reason}`);
3233

3334
return originalOnUnhandledRejection?.call(this, event);
3435
};
@@ -40,23 +41,26 @@ export function injectBrowser(context: string, window: Window, extension: Extens
4041

4142
console.error(`Error in ${extension.manifest.name} - ${context}`, ...args);
4243

43-
extension.errors.push(`Error in "${extension.manifest.name} - ${context}": ${args.join(' ')}`);
44+
extension.logger.errors.push(`Error in "${extension.manifest.name} - ${context}": ${args.join(' ')}`);
4445
};
4546

4647
window.importScripts = (...urls: string[]): void => {
47-
for (const url of urls) {
48-
const script = window.document.createElement('script');
49-
script.src = extension.getFileUrl(url) ?? '';
50-
window.document.head.appendChild(script);
48+
async function asyncLoadScripts(): Promise<void> {
49+
for (const url of urls) {
50+
// eslint-disable-next-line no-await-in-loop
51+
await loadScript(extension.getFileUrl(url) ?? '', window.document);
52+
}
5153
}
54+
55+
asyncLoadScripts();
5256
};
5357

5458
patchFetch(window);
5559

5660
window.confirm = (message?: string): boolean => {
5761
const description = (
5862
<>
59-
<p>Oops looks like the extension {extension.manifest.name} tried to open a confirmation dialog. Sadly, this is not supported.</p>
63+
<p>Oops looks like the extension {extension.getName()} tried to open a confirmation dialog. Sadly, this is not supported.</p>
6064
<p>Original confirmation message was: <strong>{message}</strong></p>
6165
</>
6266
);

frontend/components/ExtensionBar/ExtensionButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function ExtensionButton({ extension }: { readonly extension: Extension;
1616
const popupContextMenuRef = useRef<ContextMenu | undefined>(undefined);
1717

1818
const { attributes, listeners, setNodeRef, transform, transition, isDragging }
19-
= useSortable({ id: extension.manifest.name });
19+
= useSortable({ id: extension.getName() });
2020

2121
const style: CSSProperties = {
2222
transform: CSS.Transform.toString(transform),
@@ -113,7 +113,7 @@ export function ExtensionButton({ extension }: { readonly extension: Extension;
113113
{...attributes}
114114
{...listeners}
115115
>
116-
<img src={iconUrl ?? ''} alt={extension.manifest.name} />
116+
<img src={iconUrl ?? ''} alt={extension.getName()} />
117117
</button>
118118
);
119119
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
3+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
4+
5+
// Because of some libraries like `chrome-promise`, we need to make all function on a class enumerable so it can loop through them.
6+
7+
export function enumerateMethods(instance: object): void {
8+
const proto = Object.getPrototypeOf(instance) as Record<string, unknown>;
9+
const methodNames = getAllMethodNames(instance);
10+
11+
for (const name of methodNames) {
12+
// Only define it if it's not already enumerable on the instance
13+
if (!Object.hasOwn(instance, name)) {
14+
Object.defineProperty(instance, name, {
15+
value: (proto[name] as Function).bind(instance),
16+
enumerable: true,
17+
writable: true,
18+
configurable: true,
19+
});
20+
}
21+
}
22+
}
23+
24+
function getAllMethodNames(obj: any): string[] {
25+
const methods = new Set<string>();
26+
27+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
28+
let current = obj;
29+
30+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
31+
while (current && current !== Object.prototype) {
32+
const props = Object.getOwnPropertyNames(current);
33+
34+
for (const prop of props) {
35+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
36+
if (typeof obj[prop] === 'function' && prop !== 'constructor') {
37+
methods.add(prop);
38+
}
39+
}
40+
41+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
42+
current = Object.getPrototypeOf(current);
43+
}
44+
45+
return Array.from(methods);
46+
}
47+
48+
export function EnumerableMethods<T extends new(...args: any[]) => object>(constructor: T): T {
49+
return class extends constructor {
50+
constructor(...args: any[]) {
51+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
52+
super(...args);
53+
enumerateMethods(this);
54+
}
55+
};
56+
}

frontend/extension/Locale.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
/* eslint-disable @typescript-eslint/class-methods-use-this */
2+
import { EnumerableMethods } from './EnumerableMethods';
23
import { Extension } from './Extension';
34

4-
export class Locale {
5+
type chromeLocale = typeof chrome.i18n;
6+
7+
@EnumerableMethods
8+
export class Locale implements chromeLocale {
59
private messages: Record<string, LanguageRecord> | undefined;
10+
readonly #extension: Extension;
611

7-
constructor(readonly extension: Extension) {
12+
constructor(extension: Extension) {
13+
this.#extension = extension;
814
}
915

1016
async initLocale(): Promise<void> {
11-
const defaultLocale = this.extension.manifest.default_locale;
17+
const defaultLocale = this.#extension.manifest.default_locale;
1218
if (defaultLocale === undefined) {
1319
// This extension does not have locales
1420
return;
@@ -17,11 +23,11 @@ export class Locale {
1723
const language = navigator.language.split('-')[0];
1824
let content;
1925
try {
20-
content = await fetch(this.extension.getFileUrl(`/_locales/${language}/messages.json`) ?? '').then(async r => r.text());
26+
content = await fetch(this.#extension.getFileUrl(`/_locales/${language}/messages.json`) ?? '').then(async r => r.text());
2127
} catch {
22-
console.debug(`[${this.extension.getName()}] Locale ${language} not found, falling back to default locale (${defaultLocale})`);
28+
console.debug(`[${this.#extension.getName()}] Locale ${language} not found, falling back to default locale (${defaultLocale})`);
2329
// Fallback to default locale (en) if the requested locale doesn't exist
24-
content = await fetch(this.extension.getFileUrl(`/_locales/${defaultLocale}/messages.json`) ?? '').then(async r => r.text());
30+
content = await fetch(this.#extension.getFileUrl(`/_locales/${defaultLocale}/messages.json`) ?? '').then(async r => r.text());
2531
}
2632
this.messages = JSON.parse(content) as Record<string, LanguageRecord>;
2733
}
@@ -33,7 +39,7 @@ export class Locale {
3339
}
3440

3541
if (this.messages === undefined) {
36-
throw new Error(`[${this.extension.getName()}] Locale not initialized, missing manifest.default_locale?`);
42+
throw new Error(`[${this.#extension.getName()}] Locale not initialized, missing manifest.default_locale?`);
3743
}
3844

3945
if (typeof substitutions === 'string') {

0 commit comments

Comments
 (0)