## 🎯 Cel Dodanie pełnego wsparcia dla Chrome Manifest V3 przy zachowaniu kompatybilności z Firefox. Główne wyzwanie: Chrome MV3 używa service workerów zamiast persistent background pages, co wymaga nowej architektury zarządzania pamięcią. --- ## 🏗️ Architektura ### Nowa warstwa abstrakcji: lib/browser-api/ **lib/browser-api/index.ts** - Główny punkt wejścia do zunifikowanego API - Wybiera właściwą implementację na podstawie zmiennej TARGET (build-time) - Eksportuje jeden interfejs dla całej aplikacji **lib/browser-api/types.ts** - Wspólne definicje typów dla obu przeglądarek - Interfejs BrowserAPI definiujący wszystkie potrzebne metody - Typy dla tabs, badge, webRequest, cookies, extension, windows **lib/browser-api/firefox.ts** - Adapter dla Firefox browser.* API - Lazy access do globalnego obiektu browser (bezpieczne dla środowisk bez Firefox API) - Wszystkie metody zwracają Promise lub są no-op jeśli API niedostępne **lib/browser-api/chrome.ts** - Adapter dla Chrome chrome.* API - Mapowanie chrome.action → badge (różnica nazewnictwa) - Ochrona przed błędami gdy karta zostaje zamknięta (try-catch w operacjach badge) --- ## 🔧 Build System ### esbuild.config.js - **Dodano**: Rozpoznawanie zmiennej środowiskowej TARGET (firefox/chrome) - **Dodano**: Różne katalogi wyjściowe (dist-firefox, dist-chrome) - **Dodano**: Kopiowanie odpowiedniego manifestu na podstawie TARGET - **Dodano**: Plugin do konwersji ikon SVG → PNG dla Chrome (wymaga PNG w MV3) - **Zmieniono**: define zawiera teraz process.env.TARGET dostępny w runtime ### package.json - **Dodano**: Skrypty build:firefox, build:chrome, watch:firefox, watch:chrome - **Dodano**: Skrypty build-addon:firefox, build-addon:chrome do tworzenia paczek - **Dodano**: Skrypt convert-icons do generowania PNG z SVG - **Dodano**: Zależność sharp do konwersji obrazów ### scripts/convert-icons.js (NOWY PLIK) - Konwertuje assets/icon-addon.svg → PNG w rozmiarach 16, 32, 48, 128px - Wymagane dla Chrome (MV3 nie akceptuje SVG w manifestach) ### manifest-chrome.json (NOWY PLIK) - Manifest V3 dla Chrome - background.service_worker zamiast background.scripts - action zamiast browser_action - Ikony PNG zamiast SVG - host_permissions zamiast embedowanych w permissions --- ## 🧠 Pamięć i Stan (NAJWIĘKSZA ZMIANA) ### memory.ts - Kompletna refaktoryzacja **Problem Chrome MV3:** - Service worker może być wyładowany w dowolnym momencie - Brak dostępu do window.memory z popup/sidebar - chrome.extension.getBackgroundPage() zwraca null w MV3 **Rozwiązanie:** 1. **Service Worker**: Trzyma pełne dane, synchronizuje do chrome.storage.session 2. **Popup/Sidebar**: Tworzy własną instancję Memory czytając z storage 3. **Throttled sync**: Maksymalnie co 500ms zapisy do storage (wydajność) **Nowa klasa: CachedRequestCluster** - Dziedziczy po RequestCluster ale NIE ma rzeczywistych requestów - Przechowuje tylko metadane: hasCookies, exposesOrigin, hasMarks - Implementuje wszystkie wymagane metody zwracając cached state - Używana TYLKO w popup/report window w Chrome **Zmiany w Memory klasie:** - **Dodano**: isReady flag i readyPromise dla async inicjalizacji (Chrome) - **Dodano**: waitUntilReady() - popup musi poczekać na załadowanie danych - **Dodano**: tabUrls: Map<number, string> - tracking pełnych URL dla Chrome (service worker nie dostaje documentUrl) - **Dodano**: scheduleSyncToStorage() - throttled sync do storage - **Dodano**: syncToStorage() - serializacja clustrów do JSON - **Dodano**: loadFromStorage() - deserializacja przy starcie popup - **Zmieniono**: register() śledzi main_frame URL i synuje po każdej zmianie - **Zmieniono**: Badge operacje w try-catch (karta może być zamknięta) **Funkcja getMemory():** - **Firefox**: browserAPI.extension.getBackgroundPage().memory (tradycyjnie) - **Chrome Service Worker**: self.memory (jesteśmy W service workerze) - **Chrome Popup**: Tworzy NOWĄ instancję czytając z storage (cachowana jako popupMemoryInstance) --- ## 🔒 Bezpieczeństwo i Obsługa Błędów ### util.ts - getshorthost() - **Dodano**: Walidacja wejścia (null, undefined, pusty string) - **Dodano**: Czyszczenie URL (protokół, ścieżka, port) - **Dodano**: Obsługa edge cases (localhost, single word domains) - **Dodano**: Bezpieczne fallbacki zamiast crashowania - **Dodano**: Console.warn zamiast milczących błędów ### extended-request.ts - **MASYWNE POPRAWKI** parsowania URL w konstruktorze - **Dodano**: isValidHttpUrl() helper - sprawdza czy URL zaczyna się od http(s) - **Dodano**: safeParseUrl() helper - try-catch wokół new URL() - **Dodano**: Próba parsowania wielu URL w kolejności priorytetów - **Dodano**: Obsługa Chrome MV3 initiator property - **Dodano**: Bezpieczne defaulty gdy parsowanie się nie uda (unknown://unknown) - **Zmieniono**: isThirdParty() pomija requesty z unparseable URLs - **Dodano**: uint8ArrayToString() - chunked konwersja dużych arrayów (zapobiega stack overflow) - **Zmieniono**: Request body processing używa chunked konwersji --- ## 🎨 UI Components ### toolbar.tsx (popup) - **Dodano**: getCurrentTab() z retry mechanism (Chrome czasem nie zwraca karty od razu) - **Dodano**: Sprawdzanie memoryReady przed renderowaniem danych - **Dodano**: Wywołanie waitUntilReady() w useEffect - **Dodano**: Opóźnienie 200ms dla Chrome przy inicjalizacji (service worker + storage delay) - **Dodano**: Graceful handling gdy popup otwarty bez active tab ### sidebar.tsx - **Dodano**: Stan memoryReady i loading screen dla Chrome - **Dodano**: Wywołanie waitUntilReady() przed dostępem do danych - **Dodano**: Conditional rendering - pokazuje Ładowanie... gdy pamięć nie gotowa ### report-window.tsx - **Dodano**: Stan memoryReady i loading message - **Dodano**: Wywołanie waitUntilReady() przed generowaniem raportu - **Dodano**: Fallback konstruowania URL z origin gdy brak visited_url - **Zmieniono**: Filtr clustrów używa hasMarks() zamiast getMarkedRequests().length ### stolen-data-cluster.tsx - **Bez znaczących zmian** - działa z abstrakcją RequestCluster --- ## 🐛 Poprawki Bugów ### background.ts - **Dodano**: Diagnostic logging do debugowania inicjalizacji - **Dodano**: Try-catch wokół init() z error logging - **Dodano**: Różne logi dla Firefox vs Chrome ### memory.ts - badge operations - **Dodano**: Try-catch wokół wszystkich operacji badge - Zapobiega crashowaniu gdy użytkownik zamknie kartę podczas operacji ### chrome.ts - badge adapter - **Dodano**: Try-catch w setBadgeText, setTitle, setBadgeBackgroundColor - Chrome rzuca błędy gdy operujemy na zamkniętych kartach --- ## 📝 Workflow Użytkownika (Chrome) 1. **Użytkownik odwiedza stronę** → Service worker rejestruje requesty → Auto-mark podejrzanych → Sync do storage 2. **Użytkownik otwiera popup** → Tworzy Memory → Czyta ze storage → Pokazuje dane z flagami 3. **Użytkownik (od)zaznacza domeny** → Zmienia flagi → Sync do storage 4. **Użytkownik generuje raport** → Otwiera report-window → Czyta ze storage → Filtruje według hasMarks() --- ## ✅ Rezultat - ✅ **Firefox**: Działa jak wcześniej (background page + window.memory) - ✅ **Chrome**: Pełne wsparcie MV3 (service worker + storage.session) - ✅ **Wspólny kod**: 95% kodu jest shared, tylko warstwa dostępu do API różni się - ✅ **Bezpieczeństwo**: Obsługa wszystkich edge cases w parsowaniu URL - ✅ **Wydajność**: Throttled sync do storage (max co 500ms) - ✅ **UX**: Loading states w popup/sidebar dla Chrome - ✅ **Build**: npm run build-addon:firefox lub npm build-addon dla firefox / npm run build:chrome dla chrome
179 lines
6.8 KiB
JavaScript
179 lines
6.8 KiB
JavaScript
import esbuild from 'esbuild';
|
|
import scss from 'esbuild-plugin-sass';
|
|
import { copyFileSync, mkdirSync, readdirSync, existsSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
|
|
// Określenie platformy docelowej: firefox (domyślnie) lub chrome
|
|
const TARGET = process.env.TARGET || 'firefox';
|
|
const IS_FIREFOX = TARGET === 'firefox';
|
|
const IS_CHROME = TARGET === 'chrome';
|
|
|
|
// Katalogi wyjściowe
|
|
const DIST_DIR = IS_FIREFOX ? './dist-firefox' : './dist-chrome';
|
|
const LIB_DIR = join(DIST_DIR, 'lib');
|
|
|
|
console.log(`🎯 Budowanie dla: ${TARGET.toUpperCase()}`);
|
|
console.log(`📁 Katalog wyjściowy: ${DIST_DIR}`);
|
|
|
|
const watch = process.argv.includes('--watch') && {
|
|
onRebuild(error) {
|
|
if (error) console.error('[watch] budowanie nie powiodło się', error);
|
|
else console.log('[watch] budowanie zakończone');
|
|
},
|
|
};
|
|
|
|
// Funkcja pomocnicza: rekurencyjne kopiowanie katalogów
|
|
function copyDir(src, dest) {
|
|
if (!existsSync(dest)) {
|
|
mkdirSync(dest, { recursive: true });
|
|
}
|
|
|
|
const entries = readdirSync(src, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const srcPath = join(src, entry.name);
|
|
const destPath = join(dest, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
copyDir(srcPath, destPath);
|
|
} else {
|
|
copyFileSync(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Plugin: kopiowanie plików statycznych po zakończeniu budowania
|
|
const copyStaticFiles = {
|
|
name: 'copy-static-files',
|
|
setup(build) {
|
|
build.onEnd(() => {
|
|
console.log('📋 Kopiowanie plików statycznych...');
|
|
|
|
// Kopiowanie manifestu (wybór na podstawie platformy docelowej)
|
|
const manifestSrc = IS_FIREFOX ? './manifest.json' : './manifest-chrome.json';
|
|
const manifestDest = join(DIST_DIR, 'manifest.json');
|
|
mkdirSync(dirname(manifestDest), { recursive: true });
|
|
copyFileSync(manifestSrc, manifestDest);
|
|
console.log(` ✓ Skopiowano ${manifestSrc} → ${manifestDest}`);
|
|
|
|
// Kopiowanie katalogu components
|
|
if (existsSync('./components')) {
|
|
copyDir('./components', join(DIST_DIR, 'components'));
|
|
console.log(' ✓ Skopiowano components/');
|
|
}
|
|
|
|
// Kopiowanie katalogu assets
|
|
if (existsSync('./assets')) {
|
|
copyDir('./assets', join(DIST_DIR, 'assets'));
|
|
console.log(' ✓ Skopiowano assets/');
|
|
}
|
|
|
|
// Kopiowanie wymaganych bibliotek z node_modules (potrzebne dla plików HTML z UMD React)
|
|
const nodeModulesDest = join(DIST_DIR, 'node_modules');
|
|
|
|
// React
|
|
const reactUmdSrc = './node_modules/react/umd';
|
|
const reactUmdDest = join(nodeModulesDest, 'react/umd');
|
|
if (existsSync(reactUmdSrc)) {
|
|
copyDir(reactUmdSrc, reactUmdDest);
|
|
console.log(' ✓ Skopiowano node_modules/react/umd/');
|
|
}
|
|
|
|
// React-DOM
|
|
const reactDomUmdSrc = './node_modules/react-dom/umd';
|
|
const reactDomUmdDest = join(nodeModulesDest, 'react-dom/umd');
|
|
if (existsSync(reactDomUmdSrc)) {
|
|
copyDir(reactDomUmdSrc, reactDomUmdDest);
|
|
console.log(' ✓ Skopiowano node_modules/react-dom/umd/');
|
|
}
|
|
|
|
// Survey-React
|
|
const surveyReactSrc = './node_modules/survey-react';
|
|
const surveyReactDest = join(nodeModulesDest, 'survey-react');
|
|
if (existsSync(surveyReactSrc)) {
|
|
// Kopiowanie tylko niezbędnych plików
|
|
mkdirSync(surveyReactDest, { recursive: true });
|
|
const surveyFiles = ['survey.react.min.js', 'survey.react.min.css'];
|
|
surveyFiles.forEach(file => {
|
|
const src = join(surveyReactSrc, file);
|
|
if (existsSync(src)) {
|
|
copyFileSync(src, join(surveyReactDest, file));
|
|
}
|
|
});
|
|
console.log(' ✓ Skopiowano node_modules/survey-react/');
|
|
}
|
|
|
|
console.log(`✅ Budowanie dla ${TARGET.toUpperCase()} zakończone!`);
|
|
});
|
|
},
|
|
};
|
|
|
|
// Zobacz: https://github.com/evanw/esbuild/issues/806#issuecomment-779138268
|
|
// Plugin pomijający importy React (używamy globalnych obiektów z UMD)
|
|
let skipReactImports = {
|
|
name: 'skipReactImports',
|
|
setup(build) {
|
|
build.onResolve({ filter: /^(react(-dom)?|survey-react)$/ }, (args) => {
|
|
return {
|
|
path: args.path,
|
|
namespace: `globalExternal_${args.path}`,
|
|
};
|
|
});
|
|
|
|
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react' }, () => {
|
|
return {
|
|
contents: `module.exports = globalThis.React`,
|
|
loader: 'js',
|
|
};
|
|
});
|
|
|
|
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react-dom' }, () => {
|
|
return {
|
|
contents: `module.exports = globalThis.ReactDOM`,
|
|
loader: 'js',
|
|
};
|
|
});
|
|
|
|
build.onLoad({ filter: /.*/, namespace: 'globalExternal_survey-react' }, () => {
|
|
return {
|
|
contents: `module.exports = globalThis.Survey`,
|
|
loader: 'js',
|
|
};
|
|
});
|
|
},
|
|
};
|
|
|
|
esbuild
|
|
.build({
|
|
entryPoints: [
|
|
// JavaScript/TypeScript
|
|
'components/toolbar/toolbar.tsx',
|
|
'components/sidebar/sidebar.tsx',
|
|
'components/report-window/report-window.tsx',
|
|
'background.ts',
|
|
'diag.tsx',
|
|
|
|
// Globalne style
|
|
'styles/global.scss',
|
|
'styles/fonts.scss',
|
|
|
|
// Style komponentów (kompilowane osobno)
|
|
'components/toolbar/toolbar.scss',
|
|
// 'components/sidebar/sidebar.scss',
|
|
// 'components/report-window/report-window.scss',
|
|
],
|
|
bundle: true,
|
|
// minify: true,
|
|
outdir: LIB_DIR,
|
|
loader: { '.woff': 'file', '.woff2': 'file' },
|
|
plugins: [scss(), skipReactImports, copyStaticFiles],
|
|
define: {
|
|
PLUGIN_NAME: '"Rentgen"',
|
|
PLUGIN_URL: '"https://addons.mozilla.org/pl/firefox/addon/rentgen/"',
|
|
'process.env.TARGET': JSON.stringify(TARGET),
|
|
},
|
|
external: ['react', 'react-dom', 'survey-react'],
|
|
watch,
|
|
})
|
|
.then(() => console.log(`\n🎉 Dodatek dla ${TARGET.toUpperCase()} zbudowany pomyślnie!\n`))
|
|
.catch(() => process.exit(1)); |