diff --git a/.github/workflows/lint-check.yml b/.github/workflows/lint-check.yml index 9340855..8faa142 100644 --- a/.github/workflows/lint-check.yml +++ b/.github/workflows/lint-check.yml @@ -44,8 +44,12 @@ jobs: - name: Build run: | - bun run build + bun run build:firefox - name: web-ext Lint run: | bun run webext:lint + + - name: Build + run: | + bun run build:chromium diff --git a/Makefile b/Makefile index ccb91ac..bc164e7 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,11 @@ format: tsc: bun tsc -build: - bun run build +build-firefox: + bun run build:firefox + +build-chromium: + bun run build:chromium spell: bun run spell diff --git a/bun.lockb b/bun.lockb index fa957af..83743b1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 82fa0d7..d15e8d2 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -7,7 +7,7 @@ This document provides instructions for installing the Web Extension on various 1. **Download the extension package**: Clone the project and run `bun run build`. The package will be created in `build/` directory. 2. **Open Mozilla Firefox and navigate to about:debugging**: Enter `about:debugging` in the address bar of Firefox and press Enter. 3. **Load the extension**: Click on the "This Firefox" button in the upper right corner of the screen, then select "Load Temporary Add-on..." from the dropdown menu. -4. **Select the extension package**: Navigate to the folder where you downloaded the extension package and select the `manifest.json` file. +4. **Select the extension package**: Navigate to the folder where you downloaded the extension package and select the generated `manifest.json` file. 5. **Confirm installation**: You should now see the extension added to the list of installed extensions. ## Google Chrome 🌐 diff --git a/docs/cspell.dict.txt b/docs/cspell.dict.txt index 835d011..faa8918 100644 --- a/docs/cspell.dict.txt +++ b/docs/cspell.dict.txt @@ -6,6 +6,7 @@ Barkha Brooooo Cacher camelcase +cfworker Classts compat cpolyline diff --git a/package.json b/package.json index 14bcc3a..85acee2 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,13 @@ }, "scripts": { "watch": "bun tsc && bun tools/watcher.ts", - "build": "bun tools/bundler.ts", - "package": "bun tools/bundler.ts && web-ext build --source-dir=build --overwrite-dest", - "dev:firefox": "bun tools/bundler.ts && concurrently --kill-others \"bun tools/watcher.ts\" \"web-ext run --source-dir=build\"", - "dev:chromium": "bun tools/bundler.ts && concurrently --kill-others \"bun tools/watcher.ts\" \"web-ext run -t chromium --source-dir=build\"", - "dev:firefox-android": "bun tools/bundler.ts && concurrently --kill-others \"bun tools/watcher.ts\" \"web-ext run -t firefox-android --source-dir=build\"", + "build:firefox": "BROWSER=firefox bun tools/bundler.ts", + "build:chromium": "BROWSER=chromium bun tools/bundler.ts", + "package:firefox": "BROWSER=firefox bun tools/bundler.ts && web-ext build --source-dir=build --overwrite-dest", + "package:chromium": "BROWSER=chromium bun tools/bundler.ts && web-ext build --source-dir=build --overwrite-dest", + "dev:firefox": "cross-env BROWSER=firefox bun tools/bundler.ts && concurrently --kill-others \"cross-env BROWSER=firefox bun tools/watcher.ts\" \"web-ext run --source-dir=build\"", + "dev:chromium": "cross-env BROWSER=chromium bun tools/bundler.ts && concurrently --kill-others \"cross-env BROWSER=chromium bun tools/watcher.ts\" \"web-ext run -t chromium --source-dir=build\"", + "dev:firefox-android": "cross-env BROWSER=firefox bun tools/bundler.ts && concurrently --kill-others \"cross-env BROWSER=firefox bun tools/watcher.ts\" \"web-ext run -t firefox-android --source-dir=build\"", "format": "prettier --write '**/*.{js,jsx,ts,tsx,json,md,html,css}' '!build/**' '!node_modules/**' '!web-ext-artifacts/**'", "format:check": "prettier --check '**/*.{js,jsx,ts,tsx,json,md,html,css}' '!build/**' '!node_modules/**' '!web-ext-artifacts/**'", "webext:lint": "web-ext lint --source-dir=build", @@ -37,11 +39,12 @@ "lint:fix": "eslint . --cache --cache-strategy content --fix", "tsc": "tsc --noEmit", "spell": "cspell \"**/*.{ts,js,md,json,txt,html,css}\" \"Makefile\" --gitignore", - "precommit": "bun run lint && bun run format:check && bun run tsc && bun run spell && bun run build && bun run webext:lint", + "precommit": "bun run lint && bun run format:check && bun run tsc && bun run spell && bun run build:firefox && web-ext lint --source-dir=build && bun run build:chromium", "prepare": "husky" }, "type": "module", "dependencies": { + "@cfworker/json-schema": "^4.0.3", "@langchain/anthropic": "^0.3.8", "@langchain/community": "^0.3.17", "@langchain/google-genai": "^0.1.5", @@ -65,9 +68,12 @@ "@eslint/js": "^9.16.0", "@types/chrome": "^0.0.287", "@types/firefox-webext-browser": "^120.0.4", + "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.1", + "@types/webextension-polyfill": "^0.12.1", "chokidar": "^4.0.1", "concurrently": "^9.1.0", + "cross-env": "^7.0.3", "cspell": "^8.16.1", "esbuild": "^0.24.0", "eslint": "^9.16.0", @@ -78,6 +84,7 @@ "husky": "^9.1.7", "prettier": "^3.4.2", "typescript-eslint": "^8.17.0", - "web-ext": "^8.3.0" + "web-ext": "^8.3.0", + "webextension-polyfill": "^0.12.0" } } diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 611e7d8..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "manifest_version": 3, - "name": "docFiller", - "version": "1.3.0", - - "description": "Automatically fills Google Forms entries, powered by GenAI.", - "homepage_url": "https://addons.mozilla.org/en-US/firefox/addon/docfiller/", - "icons": { - "64": "assets/icons/icon-form-64.png", - "96": "assets/icons/icon-form-96.png" - }, - "developer": { - "name": "rootCircle", - "url": "https://github.com/rootCircle" - }, - "permissions": ["activeTab", "scripting", "storage"], - "host_permissions": [ - "http://docs.google.com/forms/d/e/*/viewform", - "https://docs.google.com/forms/d/e/*/viewform" - ], - "background": { - "service_worker": "src/background/index.js", - "scripts": ["src/background/index.js"] - }, - "content_scripts": [ - { - "matches": [ - "http://docs.google.com/forms/*", - "https://docs.google.com/forms/*" - ], - "js": ["src/contentScript/index.js"] - } - ], - "action": { - "default_popup": "src/popup/index.html", - "default_title": "docFiller" - }, - "options_ui": { - "page": "src/options/index.html", - "open_in_tab": true - }, - "browser_specific_settings": { - "gecko": { - "id": "docFiller@rootcircle.github.io", - "strict_min_version": "109.0" - } - } -} diff --git a/public/manifest.ts b/public/manifest.ts new file mode 100644 index 0000000..e002776 --- /dev/null +++ b/public/manifest.ts @@ -0,0 +1,99 @@ +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +import fs from 'fs-extra'; +import type { Manifest } from 'webextension-polyfill'; + +import type PkgType from '../package.json'; + +const r = (...args: string[]) => { + return resolve(fileURLToPath(new URL('..', import.meta.url)), ...args); +}; + +const CHROMIUM_BASED = ['chrome', 'chromium', 'edge', 'brave', 'opera']; +const FIREFOX_BASED = ['firefox']; + +export async function getManifest() { + if (!(await fs.pathExists(r('package.json')))) { + throw new Error('package.json not found'); + } + + const pkg = (await fs.readJSON(r('package.json'))) as typeof PkgType; + const targetBrowser = process.env['BROWSER']; + if (!targetBrowser) { + throw new Error('BROWSER environment variable must be set'); + } + const isFirefoxBased = FIREFOX_BASED.includes(targetBrowser); + const isChromiumBased = CHROMIUM_BASED.includes(targetBrowser); + if (!isFirefoxBased && !isChromiumBased) { + throw new Error( + `Unsupported or unspecified browser: ${targetBrowser}. Supported browsers: ${[...CHROMIUM_BASED, ...FIREFOX_BASED].join(', ')}`, + ); + } + if (isFirefoxBased && isChromiumBased) { + throw new Error( + 'Both Firefox and Chromium based browsers are specified. Please specify only one.', + ); + } + + const baseManifest: Omit = { + manifest_version: 3, + name: 'docFiller', + version: pkg.version, + description: pkg.description, + homepage_url: 'https://addons.mozilla.org/en-US/firefox/addon/docfiller/', + icons: { + '64': 'assets/icons/icon-form-64.png', + '96': 'assets/icons/icon-form-96.png', + }, + developer: { + name: 'rootCircle', + url: 'https://github.com/rootCircle', + }, + permissions: ['activeTab', 'storage'], + host_permissions: [ + 'http://docs.google.com/forms/d/e/*/viewform', + 'https://docs.google.com/forms/d/e/*/viewform', + ], + action: { + default_popup: 'src/popup/index.html', + default_title: 'docFiller', + }, + options_ui: { + page: 'src/options/index.html', + open_in_tab: true, + }, + content_scripts: [ + { + matches: [ + 'http://docs.google.com/forms/*', + 'https://docs.google.com/forms/*', + ], + js: ['src/contentScript/index.js'], + }, + ], + }; + + const manifest: Manifest.WebExtensionManifest = { + ...baseManifest, + background: (() => { + if (isFirefoxBased) { + return { scripts: ['src/background/index.js'] }; + } + if (isChromiumBased) { + return { service_worker: 'src/background/index.js' }; + } + throw new Error('Unsupported browser type'); + })(), + browser_specific_settings: { + ...(isFirefoxBased && { + gecko: { + id: 'docFiller@rootcircle.github.io', + strict_min_version: '109.0', + }, + }), + }, + }; + + return manifest; +} diff --git a/src/options/metrics.ts b/src/options/metrics.ts index 7f3fcf5..6a012ee 100644 --- a/src/options/metrics.ts +++ b/src/options/metrics.ts @@ -23,10 +23,11 @@ export class MetricsUI { await this.updateMetricsDisplay(); this.setupEventListeners(); // Update metrics every 5 seconds - this.updateInterval = window.setInterval( - async () => void (await this.updateMetricsDisplay()), - 5000, - ); + this.updateInterval = window.setInterval(() => { + this.updateMetricsDisplay().catch((error) => + console.error('Error updating metrics display:', error), + ); + }, 5000); } catch (error) { console.error('Failed to initialize metrics:', error); } finally { diff --git a/tools/builder.ts b/tools/builder.ts index 22670f0..aee6ab2 100644 --- a/tools/builder.ts +++ b/tools/builder.ts @@ -3,10 +3,21 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable no-console */ import * as esbuild from 'esbuild'; - +import fs from 'fs-extra'; import copyContents from './copier'; import entryPoints from './entrypoints'; +import { writeManifest } from './manifestWriter'; +const cleanBuildFolder = async () => { + try { + await fs.remove('./build'); + await fs.ensureDir('./build'); + console.log('Build folder cleaned and recreated.'); + } catch (error) { + console.error('Error cleaning build folder:', error); + throw error; + } +}; const build = async (watch: boolean) => { const entrypoints = await entryPoints(); await copyContents('./public', './build'); @@ -45,6 +56,8 @@ const build = async (watch: boolean) => { const runBuild = async (watch: boolean) => { try { + await cleanBuildFolder(); + await writeManifest(); await build(watch); console.log('Build completed successfully.'); } catch (error) { diff --git a/tools/bundler.ts b/tools/bundler.ts index 016e252..1656156 100644 --- a/tools/bundler.ts +++ b/tools/bundler.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { runBuild } from './builder'; runBuild(false).catch(console.error); diff --git a/tools/entrypoints.ts b/tools/entrypoints.ts index de6e713..2b7c88a 100644 --- a/tools/entrypoints.ts +++ b/tools/entrypoints.ts @@ -43,7 +43,7 @@ async function getFiles( * @returns {Promise} The entrypoints */ export default async function entryPoints(): Promise { - const files = []; + const files: string[] = []; for (const { path, extensions } of sourceDir) { files.push(...(await getFiles(path, extensions))); } diff --git a/tools/manifestWriter.ts b/tools/manifestWriter.ts new file mode 100644 index 0000000..4904c41 --- /dev/null +++ b/tools/manifestWriter.ts @@ -0,0 +1,28 @@ +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { getManifest } from '../public/manifest'; +import fs from 'fs-extra'; + +const r = (...args: string[]) => { + return resolve(fileURLToPath(new URL('..', import.meta.url)), ...args); +}; + +export async function writeManifest() { + const browser = process.env.BROWSER; + if (!browser) { + throw new Error('BROWSER environment variable must be set'); + } + try { + await fs.ensureDir(r('build')); + const manifest = await getManifest(); + await fs.writeJSON(r('build/manifest.json'), manifest, { spaces: 2 }); + console.log(`✓ manifest.json generated for ${browser}`); + } catch (error) { + console.error('Error writing manifest:', error); + throw error; + } +} + +if (require.main === module) { + writeManifest().catch(console.error); +} diff --git a/tools/watcher.ts b/tools/watcher.ts index e00bac2..112394f 100644 --- a/tools/watcher.ts +++ b/tools/watcher.ts @@ -1,32 +1,23 @@ /* eslint-disable no-console */ import chokidar from 'chokidar'; - import { runBuild } from './builder'; import copyContents from './copier'; const buildWatch = () => { runBuild(true).catch(console.error); - // Watch directories - const watcher = chokidar.watch(['public/'], { + const watcher = chokidar.watch(['public/', 'public/manifest.ts'], { ignored: /node_modules/, persistent: true, }); - watcher.on('change', () => { - // console.log(`File ${path} has been changed`); + const handleChange = () => { copyContents('./public', './build').catch(console.error); - }); + }; - watcher.on('add', () => { - // console.log(`File ${path} has been added`); - copyContents('./public', './build').catch(console.error); - }); - - watcher.on('unlink', () => { - // console.log(`File ${path} has been removed`); - copyContents('./public', './build').catch(console.error); - }); + watcher.on('change', handleChange); + watcher.on('add', handleChange); + watcher.on('unlink', handleChange); }; buildWatch(); diff --git a/tsconfig.json b/tsconfig.json index d34f795..39297ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "types": [ "./types.d.ts", // add your own type definitions "@types/chrome", - "@types/firefox-webext-browser" + "@types/firefox-webext-browser", + "webextension-polyfill" ], "baseUrl": "src", "paths": {