rentgen/esbuild.config.js
am0 ffb37f9d61 feat/refactor: Chrome MV3 Support + Refaktoryzacja architektury dla kompatybilności obu przeglądarek
## 🎯 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
2025-10-18 13:08:14 +02:00

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));