Skip to content
20 changes: 15 additions & 5 deletions src/execute.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
function hasResult<T>(
injectionResult: chrome.scripting.InjectionResult<T>,
): injectionResult is chrome.scripting.InjectionResult<T> & { result: T } {
return Object.hasOwn(injectionResult, 'result');
}

export default async function execute<T, U>(
func: (argument: T) => U,
argument: T,
): Promise<U | null> {
): Promise<chrome.scripting.Awaited<U> | null> {
let tab;

try {
Expand All @@ -12,14 +18,14 @@ export default async function execute<T, U>(

const tabId = tab.id;

if (tabId === undefined) {
if (typeof tabId !== 'number') {
return null;
}

let result;
let injectionResult: chrome.scripting.InjectionResult<chrome.scripting.Awaited<U>>;

try {
[{ result }] = await chrome.scripting.executeScript({
[injectionResult] = await chrome.scripting.executeScript<[T], U>({
target: { tabId },
func,
args: [argument],
Expand All @@ -28,5 +34,9 @@ export default async function execute<T, U>(
return null;
}

return result as U | null;
if (!hasResult(injectionResult)) {
return null;
}

return injectionResult.result;
}
6 changes: 3 additions & 3 deletions src/fill-in-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export default async function fillInPassword(generatedPassword: string): Promise
// If the active element is an iframe, descend into it to find the innermost active element.
while (element instanceof iframeElementType) {
const contentDocument = element.contentDocument;
const contentWindow = element.contentWindow as (Window & typeof globalThis) | null;
const contentWindow = element.contentWindow;

if (contentDocument !== null && contentWindow !== null) {
element = contentDocument.activeElement;
iframeElementType = contentWindow.HTMLIFrameElement;
inputElementType = contentWindow.HTMLInputElement;
iframeElementType = contentWindow.window.HTMLIFrameElement;
inputElementType = contentWindow.window.HTMLInputElement;
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/get-is-password-field-active.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export default async function getIsPasswordFieldActive(): Promise<boolean | null
// If the active element is an iframe, descend into it to find the innermost active element.
while (element instanceof iframeElementType) {
const contentDocument = element.contentDocument;
const contentWindow = element.contentWindow as (Window & typeof globalThis) | null;
const contentWindow = element.contentWindow;

if (contentDocument !== null && contentWindow !== null) {
element = contentDocument.activeElement;
iframeElementType = contentWindow.HTMLIFrameElement;
inputElementType = contentWindow.HTMLInputElement;
iframeElementType = contentWindow.window.HTMLIFrameElement;
inputElementType = contentWindow.window.HTMLInputElement;
}
}

Expand Down
21 changes: 14 additions & 7 deletions src/worker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,31 @@ const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'modu
let nextMessageId = 0;

// Keep track of all in-flight requests so we know what to do with the corresponding responses.
const requests: Record<number, (generatedPassword: string) => void> = {};
const requests = new Map<number, (generatedPassword: string) => void>();

// This is the handler for incoming responses.
worker.onmessage = (event: MessageEvent<Response>) => {
requests[event.data.messageId](event.data.generatedPassword);
delete requests[event.data.messageId];
};
worker.addEventListener('message', (event: MessageEvent<Response>): void => {
const resolve = requests.get(event.data.messageId);

export default function hashpass(domain: string, universalPassword: string): Promise<string> {
if (resolve === undefined) {
return;
}

resolve(event.data.generatedPassword);
requests.delete(event.data.messageId);
});

export default async function hashpass(domain: string, universalPassword: string): Promise<string> {
return new Promise((resolve) => {
const request: Request = {
messageId: nextMessageId,
domain,
universalPassword,
};

requests[nextMessageId] = resolve;
requests.set(nextMessageId, resolve);

// oxlint-disable-next-line unicorn/require-post-message-target-origin -- Worker.postMessage does not take targetOrigin.
worker.postMessage(request);

nextMessageId = (nextMessageId + 1) % Number.MAX_SAFE_INTEGER;
Expand Down
5 changes: 3 additions & 2 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import hashpass from './hashpass.ts';
import type { Request, Response } from './worker-protocol.ts';

self.onmessage = (event: MessageEvent<Request>) => {
self.addEventListener('message', (event: MessageEvent<Request>): void => {
const request = event.data;

const response: Response = {
messageId: request.messageId,
generatedPassword: hashpass(request.domain, request.universalPassword),
};

// oxlint-disable-next-line unicorn/require-post-message-target-origin -- WorkerGlobalScope.postMessage does not take targetOrigin.
self.postMessage(response);
};
});
27 changes: 0 additions & 27 deletions tsconfig.app.json

This file was deleted.

21 changes: 19 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"moduleResolution": "bundler",
"types": ["chrome", "vite/client"],
"allowArbitraryExtensions": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"jsx": "react-jsx",
"moduleDetection": "force",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"strict": true,
"verbatimModuleSyntax": true
},
"include": ["src", "vite.config.ts"]
}
24 changes: 0 additions & 24 deletions tsconfig.node.json

This file was deleted.

37 changes: 21 additions & 16 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import { defineConfig, lazyPlugins } from 'vite-plus';
import react from '@vitejs/plugin-react';

// https://vite.dev/config/
export default defineConfig({
// Use relative asset URLs so the build works both as an extension popup and as a static website
// served from any path.
base: './',
fmt: { singleQuote: true, ignorePatterns: ['**/*.md'] },
fmt: {
ignorePatterns: ['**/*.md'],
singleQuote: true,
},
lint: {
plugins: ['react', 'typescript', 'oxc'],
rules: {
'react/rules-of-hooks': 'error',
'react/only-export-components': [
'warn',
{
allowConstantExport: true,
},
],
'vite-plus/prefer-vite-plus-imports': 'error',
},
options: {
typeAware: true,
typeCheck: true,
categories: {
correctness: 'deny',
perf: 'deny',
restriction: 'deny',
suspicious: 'deny',
},
jsPlugins: [
{
name: 'vite-plus',
specifier: 'vite-plus/oxlint-plugin',
},
],
options: {
typeAware: true,
typeCheck: true,
},
plugins: ['oxc', 'react', 'typescript', 'unicorn'],
rules: {
'no-undefined': 'allow',
'oxc/no-async-await': 'allow',
'react/jsx-filename-extension': 'allow',
'react/jsx-no-literals': 'allow',
'react/react-in-jsx-scope': 'allow',
},
},
plugins: lazyPlugins(() => [react()]),
});