Assistente indipendente per Pegaso, Mercatorum e San Raffaele
Snapshot di riferimento del capitolo
Questo capitolo usa il candidato release 2.35.2, branchfeat/ui-ux-2.35.2, commit
f6fcb4c29c83ba9150df0be31da04fc91dffb634.
A differenza dei Capitoli 11–12, questo snapshot include anche la preparazione di manifest, Novità e materiale AMO 2.35.2, perché proprio il packaging cross-browser è parte dell’argomento.
Nei capitoli precedenti abbiamo spesso scritto:
il browser
come se Chrome, Edge e Firefox fossero tre finestre diverse sullo stesso runtime.
Per molte parti di PlumePilot è una semplificazione utile.
Il DOM è DOM.
fetch() è fetch().
Una Map è una Map.
Una Promise continua a rappresentare un’operazione asincrona.
Ma quando costruiamo, distribuiamo e manteniamo una browser extension reale, quella semplificazione smette presto di essere sufficiente.
Il progetto deve attraversare almeno cinque livelli di compatibilità:
sintassi JavaScript
↓
Web Platform API
↓
WebExtension API
↓
manifest / runtime model
↓
store policy / review pipeline
Ed è possibile essere compatibili a quattro livelli su cinque e non poter comunque pubblicare.
È esattamente ciò che è accaduto più volte durante l’evoluzione di PlumePilot.
Una funzione poteva:
funzionare correttamente in Firefox
ma il pacchetto poteva comunque contenere un pattern che AMO non considerava sufficientemente reviewable.
Oppure lo stesso codice applicativo poteva funzionare in Chrome ed Edge, mentre il pacchetto Edge aveva bisogno di una struttura di localizzazione diversa.
Oppure un’API poteva esistere in Chromium ma non nel runtime Firefox scelto dal progetto.
Il problema corretto, quindi, non è:
“Il nostro JavaScript funziona su tutti i browser?”
ma:
“Possiamo produrre per ogni browser un artefatto corretto, comprensibile, validabile, aggiornabile e conforme senza creare tre codebase indipendenti?”
Questo capitolo studia come PlumePilot prova a rispondere.
Immaginiamo questa funzione:
const value = new Map();
È supportata nei browser moderni che ci interessano.
Ora immaginiamo invece:
chrome.offscreen.createDocument(...);
Qui non stiamo più usando semplicemente JavaScript.
Stiamo usando una extension API specifica del runtime Chromium.
Poi prendiamo:
{
"background": {
"service_worker": "background.js"
}
}
Qui il problema non è neppure una funzione JavaScript.
È il modello di background dichiarato dal manifest.
Infine prendiamo del vendor code contenente:
new Function(...)
Potrebbe non essere mai raggiunto da PlumePilot.
Potrebbe persino funzionare tecnicamente.
Ma può diventare un problema di:
static analysis
reviewability
store policy
La parola “compatibilità” copre quindi problemi diversi.
Per il nostro caso possiamo usare questa matrice:
| Livello | Domanda |
|---|---|
| Linguaggio | Il motore JS comprende questa sintassi? |
| Web Platform | Il browser offre questa API DOM/Web? |
| WebExtension | L’extension runtime espone questa API? |
| Manifest | Questa dichiarazione è valida per quel browser? |
| Lifecycle | Il background vive e muore nello stesso modo? |
| Packaging | Lo ZIP richiesto ha la stessa struttura? |
| Policy | Il codice è accettabile per lo store? |
| Review | Il revisore può ricostruire e comprendere il pacchetto? |
| UX | La stessa UI si comporta allo stesso modo? |
Questo è già molto più vicino alla realtà.
Un errore concettuale comune è pensare:
repository
=
extension installata
In PlumePilot non è così.
Il repository contiene:
codice runtime
test
script di build
documentazione
fixture
asset sorgente
strumenti di validazione
metadata per revisori
mentre il pacchetto runtime dovrebbe contenere soltanto ciò che serve all’estensione installata.
Perciò la pipeline è:
repository
↓
selezione file runtime
↓
specializzazione per browser
↓
trasformazioni consentite
↓
manifest specifico
↓
ZIP deterministico
↓
validazione
↓
store
Questa separazione è già software architecture.
Il cuore della strategia attuale è in:
scripts/build-release.mjs
Il build conosce esplicitamente tre target:
const browsers = ["chrome", "firefox", "edge"];
Non abbiamo:
un manifest universale consegnato identico a tutti
Abbiamo invece:
source comune
↓
manifestFor(baseManifest, browser)
↓
Chrome artifact
Firefox artifact
Edge artifact
Questa tecnica può essere chiamata build-time specialization.
Le differenze note vengono risolte durante la build, non sparse in centinaia di condizioni runtime.
L’alternativa estrema sarebbe:
plumepilot-chrome
plumepilot-firefox
plumepilot-edge
Sembra semplice.
In realtà introduce rapidamente:
fix duplicati
release divergenti
bug risolti in 2 browser su 3
schema storage disallineato
feature drift
test duplicati
PlumePilot preferisce:
codice dominio comune
│
┌─────────┼─────────┐
↓ ↓ ↓
Chrome Firefox Edge
build build build
Le differenze sono concentrate ai boundary.
Questo è un principio molto più generale:
Mantieni comune il dominio; specializza l’adapter verso la piattaforma.
Il manifest del repository 2.35.2 contiene ancora una forma che descrive entrambe le possibilità:
"background": {
"scripts": [
"achievements.js",
"commission-state.js",
"sound-settings.js",
"whats-new.js",
"background.js"
],
"service_worker": "background.js"
}
Ma questa non è la forma che il release builder lascia necessariamente in ogni pacchetto.
Il build la specializza.
Per i target Chromium:
manifest.background = {
service_worker: "background.js"
};
e viene aggiunto:
"offscreen"
alle permissions.
Concettualmente:
Chromium MV3
↓
extension service worker
↓
background senza DOM
Questo ha conseguenze che abbiamo già incontrato:
chrome.storage;window/DOM nel service worker.Il Capitolo 6 aveva già mostrato perché l’operation state non può vivere soltanto in una variabile del background.
Qui ne vediamo la ragione cross-browser.
Il pacchetto Firefox viene invece costruito con:
manifest.background = {
scripts: [
"achievements.js",
"sound-settings.js",
"whats-new.js",
"background.js"
]
};
e senza:
background.service_worker
Il builder verifica persino l’invariante:
if (manifest.background?.service_worker)
throw new Error("Firefox: service_worker inatteso.");
Non stiamo quindi semplicemente dicendo:
Firefox è un Chromium con qualche API mancante
Stiamo accettando che il background execution environment sia diverso.
MDN documenta esplicitamente questa differenza: per Manifest V3 Chrome usa background.service_worker, mentre Firefox supporta background.scripts per questo scenario.
La documentazione WebExtensions mostra anche una possibile strategia universale:
"background": {
"scripts": ["background.js"],
"service_worker": "background.js"
}
lasciando al browser la scelta dell’ambiente disponibile.
PlumePilot, tuttavia, produce artefatti separati.
Perché?
Non necessariamente perché l’approccio universale sia sbagliato.
Ma perché noi abbiamo anche:
permission diverse
review constraints diverse
localizzazioni Edge
Firefox source submission
offscreen API Chromium-only
validatori diversi
Una volta che il packaging è già browser-specifico, un manifest specializzato diventa più facile da verificare.
A runtime, quando possibile, è meglio chiedere:
if (chrome.offscreen?.createDocument) {
...
}
piuttosto che:
if (browserName === "chrome") {
...
}
La prima domanda è:
Questa capability esiste?
La seconda è:
Che etichetta ha il browser?
La prima tende a essere più robusta.
Questo si chiama spesso feature detection o capability detection.
Ma non sempre basta.
Se una differenza riguarda il manifest, non possiamo scoprirla dopo che l’estensione è stata caricata.
Perciò:
runtime feature detection
+
build-time specialization
sono strumenti complementari.
I service worker non hanno DOM.
Ma la riproduzione audio è naturalmente associata ad API di documento.
Chromium offre:
chrome.offscreen
per creare un documento invisibile dell’estensione.
Nel background PlumePilot troviamo:
async function ensureOffscreenAudioDocument() {
if (!chrome.offscreen?.createDocument) return false;
if (await chrome.offscreen.hasDocument()) return true;
if (!offscreenCreation) {
offscreenCreation = chrome.offscreen.createDocument({
url: "offscreen-audio.html",
reasons: ["AUDIO_PLAYBACK"],
justification:
"Riproduce gli avvisi sonori locali richiesti dall’utente.",
}).finally(() => {
offscreenCreation = null;
});
}
await offscreenCreation;
return true;
}
C’è già un pattern che conosciamo:
Promise in-flight
offscreenCreation impedisce due creazioni concorrenti dello stesso documento.
La funzione chiamante non assume che offscreen esista.
if (await ensureOffscreenAudioDocument()) {
return sendRuntimeMessage(...);
}
const tabs = await chrome.tabs.query(...);
...
return sendTabMessage(target.id, ...);
Quindi l’architettura reale è:
riproduci suono
↓
offscreen disponibile?
┌───┴────┐
sì no
│ │
offscreen content tab
Questa è graceful degradation applicata a una WebExtension API.
L’obiettivo di dominio:
riprodurre una notifica sonora locale
rimane lo stesso.
Cambia l’adapter.
Il build Firefox sostituisce esattamente:
async function ensureOffscreenAudioDocument() {
...
}
con:
async function ensureOffscreenAudioDocument() {
return false;
}
Il resto della funzione non cambia.
Il risultato è:
Firefox
↓
ensureOffscreenAudioDocument()
↓ false
fallback tab-based
Questa non è una seconda implementazione della feature.
È la stessa feature che seleziona sempre il fallback già esistente.
Questo riduce molto il rischio di divergenza.
Per Chromium PlumePilot richiede:
["storage", "offscreen"]
Per Firefox:
["storage"]
Perché dichiarare "offscreen" in Firefox se non viene usato?
Non avrebbe valore.
Aggiungere permessi “per uniformità” è l’opposto del least privilege.
Il validator controlla quindi l’insieme esatto:
const expectedPermissions =
browser === "firefox"
? ["storage"]
: ["storage", "offscreen"];
Non verifica soltanto:
che i permessi necessari ci siano
ma anche:
che non ne compaiano di inattesi
È una differenza sottile ma importante.
Nel manifest Firefox viene aggiunto:
"browser_specific_settings": {
"gecko": {
"id": "plumepilot@fabiofloris"
}
}
L’ID non è un dettaglio cosmetico.
È parte dell’identità dell’add-on.
Se cambia, dal punto di vista del browser possiamo ottenere un’altra estensione invece di un aggiornamento della precedente.
Il changelog del progetto conserva proprio questa lezione storica.
Nel target Firefox troviamo inoltre:
"data_collection_permissions": {
"required": [
"authenticationInfo",
"websiteContent",
"websiteActivity"
]
}
Questo non significa automaticamente:
PlumePilot invia questi dati allo sviluppatore
Il significato della dichiarazione va letto secondo il modello Firefox e le interazioni dell’estensione con i dati dei siti.
Il punto architetturale che ci interessa è un altro:
Lo stesso comportamento applicativo può richiedere metadata di distribuzione differenti per browser.
Dal 2025 Mozilla richiede queste dichiarazioni per nuove estensioni in determinati flussi AMO; il progetto include quindi questa informazione nel manifest Firefox.
Il pacchetto Firefox dichiara:
"strict_min_version": "140.0"
e per Android:
"strict_min_version": "142.0"
La versione minima è una forma di contratto.
Stiamo dicendo:
sotto questa versione
non promettiamo che l'ambiente richiesto esista
Questo può essere migliore di riempire il codice di fallback per runtime che non intendiamo più supportare.
Dal punto di vista del runtime extension moderno, Chrome ed Edge condividono moltissimo.
Ma lo store non è il motore JavaScript.
Nel build Edge PlumePilot modifica:
manifest.name = "__MSG_extensionName__";
manifest.description = "__MSG_extensionDescription__";
manifest.default_locale = "it";
manifest.action.default_title = "__MSG_extensionName__";
e aggiunge:
_locales/en/messages.json
_locales/it/messages.json
Quindi:
stesso runtime family
≠
stesso artefatto store
Questa distinzione merita un nome.
Il codice funziona nel browser.
L’artefatto soddisfa:
manifest
metadata
locale
store validator
policy
Un’estensione può avere runtime compatibility ma non distribution compatibility.
Questa è stata una delle lezioni più pratiche del progetto.
PlumePilot usa librerie importanti:
PDF.js
pdf-lib
fontkit
JSZip
Nell’ecosistema web è normalissimo distribuire bundle minimizzati.
Per una browser extension pubblicata negli store, però, il contesto cambia.
Il reviewer deve poter capire:
che codice stiamo eseguendo
da dove proviene
come è stato trasformato
se il pacchetto corrisponde al source
La reviewability entra così nell’architettura.
Le policy del Chrome Web Store vietano l’offuscamento finalizzato a nascondere la funzionalità.
Permettono tecniche tipiche di minificazione.
Ma dal punto di vista pratico del progetto abbiamo comunque incontrato detector e review automatiche sensibili alla forma del codice.
Il changelog registra in 2.32.10:
- sostituzione dei bundle legacy minificati PDF.js
- distribuzione standard leggibile
- rimozione di compatibility code che attivava un detector di obfuscation
Il punto da portarsi dietro non è:
"minification = vietata"
che sarebbe falso.
È:
Il costo di un bundle poco leggibile non è soltanto umano: entra anche nelle pipeline automatiche di review.
Mozilla formalizza ulteriormente questo requisito.
Se il codice runtime deriva da:
minifier
bundler
transpiler
template generator
custom preprocessing
il reviewer può aver bisogno di:
source leggibile
+
istruzioni di build
+
toolchain riproducibile
Per PlumePilot questo ha portato a:
plumepilot-v2.35.2-source.zip
oltre ai tre ZIP runtime.
La release non è quindi:
3 file
ma:
Chrome runtime
Firefox runtime
Edge runtime
source submission
checksums
Il builder Firefox applica trasformazioni a una lista chiusa:
[
"background.js",
"vendor/jszip.js",
"vendor/fontkit.umd.js",
"vendor/pdf-lib.js",
"vendor/pdf.mjs",
"vendor/pdf.worker.mjs"
]
Tutto il resto:
return bytes;
Questo è importante.
Non stiamo facendo:
regex globale sull'intero repository
Stiamo applicando patch note soltanto dove necessario.
replaceExactly()La funzione fondamentale è:
function replaceExactly(source, search, replacement, label) {
const first = source.indexOf(search);
if (
first < 0 ||
source.indexOf(search, first + search.length) >= 0
) {
throw new Error(
`Firefox review-safe: pattern inatteso (${label}).`
);
}
return (
source.slice(0, first) +
replacement +
source.slice(first + search.length)
);
}
Questa funzione contiene un principio molto forte.
La patch viene applicata soltanto se il pattern:
esiste
e
esiste una volta sola
Altrimenti:
FAIL BUILD
Possiamo chiamarlo:
fail-closed transformation.
Una trasformazione fragile non deve “fare del suo meglio”.
Deve dire:
il vendor file è cambiato
non so più dimostrare che questa patch sia quella corretta
interrompo la release
È l’opposto di:
source.replace(regex, replacement);
e continuare comunque.
Immaginiamo un upgrade di PDF.js. La stringa attesa:
new Function(...)
potrebbe:
Se il build applicasse silenziosamente una sostituzione parziale potremmo generare un pacchetto:
sintatticamente valido
ma semanticamente inatteso
replaceExactly() trasforma una dipendenza implicita in un’invariante verificata.
La distribuzione JSZip leggibile contiene una compatibilità legacy che permette callback stringa e quindi usa:
new Function(...)
PlumePilot passa funzioni reali.
Quella branch non serve al nostro uso.
Ma static analysis non ragiona necessariamente così.
Può vedere:
Function constructor presente
e segnalarlo.
Il pacchetto Firefox sostituisce il fallback con:
if (typeof callback !== "function") {
throw new TypeError(
"setImmediate callback must be a function"
);
}
Questa trasformazione non tenta di:
camuffare new Function
Elimina una capability che PlumePilot non usa.
È una differenza essenziale.
fontkit include fallback storici che possono costruire o esporre executable code.
Il builder Firefox li sostituisce con versioni che:
mantengono il comportamento necessario
ma rimuovono il path eval-like non utilizzato
Di nuovo il principio è:
capability realmente necessaria?
│
no │
↓
rimuovila dal runtime target
Più capability significa:
più superficie
più review complexity
più possibili path da comprendere
PDF.js è particolarmente interessante perché contiene più strategie di fallback.
Per esempio può verificare se la compilazione dinamica è possibile:
function isEvalSupported() {
try {
new Function("");
return true;
} catch {
return false;
}
}
Nel Firefox artifact PlumePilot lo trasforma in:
function isEvalSupported() {
return false;
}
Non perché Firefox “non supporti JavaScript”.
Ma perché, nel nostro deployment target, vogliamo forzare il path interprete.
PDF.js può avere un percorso concettuale:
PostScript function
↓
compilazione dinamica possibile?
┌────┴────┐
sì no
│ │
Function interpreter
Nel target Firefox PlumePilot sceglie:
sempre interpreter
Questo è un esempio di capability reduction by build.
Alcuni fallback PDF.js tentano anche dynamic import di risorse calcolate.
Se quella risorsa non è inclusa nel nostro pacchetto e quel percorso non serve al nostro uso, mantenerlo significa conservare:
codice non utile
+
pattern difficile da revisionare
Il build Firefox lo disattiva esplicitamente.
È importante però distinguere:
dynamic import statico
da:
dynamic import calcolato
e soprattutto distinguere:
API usata dal nostro prodotto
da:
fallback generico incluso dalla libreria
Il progetto conserva i vendor file leggibili corrispondenti alle distribuzioni ufficiali.
Chrome ed Edge ricevono:
copie upstream
Il target Firefox riceve:
copie derivate deterministicamente
durante la build.
Questo mantiene chiaro il lineage:
official vendor source
↓
pinned local file
↓
Firefox deterministic patch
↓
runtime artifact
Se modificassimo direttamente il file vendor nel repository perderemmo questa distinzione.
Qui entra un concetto importante:
supply-chain provenance.
Per una dipendenza vogliamo poter rispondere:
qual è la versione?
da quale artefatto upstream viene?
qual è il checksum?
quali modifiche abbiamo applicato?
perché?
THIRD_PARTY_NOTICES.md e i checksum del validator trasformano queste domande in dati verificabili.
Un file chiamato:
vendor/jszip.js
non dimostra che sia JSZip 3.10.1.
Un nome è metadata umano.
Un SHA-256 è un’identità molto più forte del contenuto.
Il validator contiene mappe di checksum attesi per:
PDF.js
pdf-lib
fontkit
JSZip
e verifica i bytes inclusi.
Questa strategia crea una forma di:
checksum pinning.
Il progetto non dice soltanto:
usa PDF.js 5.x
ma:
mi aspetto esattamente questi bytes sorgente
Questo rende più evidente una modifica accidentale.
Un checksum non dice:
questo codice è sicuro
Dice:
questo codice è esattamente quello che avevamo deciso di usare
È un’invariante di integrità/provenance, non una proof of correctness.
Dopo il build:
scripts/validate-release.mjs
riapre gli artefatti e controlla ciò che è stato effettivamente prodotto.
Questo dettaglio è importantissimo.
Non valida soltanto:
il source tree
Valida:
il prodotto finale
Il validator controlla:
versione manifest
description length
permissions esatti
background model
Gecko ID
min Firefox version
data declarations
Edge locales
file referenziati dal manifest
assenza di script remoti
assenza di dynamic code first-party
licenze
vendor checksums
Novità corrette
source submission
SHA256SUMS
In altre parole:
release contract
↓
codice eseguibile
Questo è un concetto che spesso manca nei progetti piccoli.
Abbiamo:
unit/integration test dell'app
ma anche:
test dell'artefatto
Una feature può funzionare perfettamente nel checkout Git e fallire nello ZIP perché:
file escluso
manifest sbagliato
locale mancante
asset non copiato
transformation incompleta
Perciò:
L’artefatto di release è una cosa da testare.
Per Firefox il validator legge ogni file:
.js
.mjs
.html
e cerca pattern come:
eval(
Function(
dynamic import calcolato
chrome.offscreen
dynamic innerHTML template
Questo non equivale a una dimostrazione semantica.
È static analysis euristica.
Una regex può trovare:
pattern sospetto
anche se il path:
non è mai raggiunto
o se l’uso sarebbe innocuo.
Questo può sembrare frustrante.
Ma per un ecosistema di estensioni il reviewer non può eseguire mentalmente ogni possibile control flow di ogni bundle vendor.
Perciò parte del lavoro di engineering è rendere il prodotto:
facilmente comprensibile anche staticamente
Questo porta a una conclusione interessante:
La leggibilità del codice è anche una proprietà operativa di sicurezza e governance.
Non soltanto:
“il maintainer capisce meglio il file”
ma:
reviewer può verificare
scanner può classificare
provenance può essere confrontata
future diff sono comprensibili
Il validator contiene persino una piccola euristica:
function couldBeMinifiedCode(source) {
...
}
che guarda aspetti come:
indentazione
linee enormi
sourceMappingURL
Non è “l’algoritmo AMO”.
È un guardrail locale.
Il principio è:
se sappiamo quali classi di problema
ci hanno già bloccato,
automatizziamo un controllo prima della submission
Questo pattern tornerà fortissimo nel Capitolo 14.
rejection / bug
↓
root cause
↓
invariant
↓
automated check
È una delle trasformazioni più preziose nella maturazione di un progetto.
AMO_SOURCE_README.md documenta:
Ubuntu 24.04
Node 24.x
npm non richiesto
rete non richiesta
e il comando:
node scripts/build-release.mjs
Questo riduce il numero di dipendenze implicite.
Il fatto che il release build non richieda rete è molto utile.
Evita che la riproduzione dipenda da:
registry momentaneamente diverso
package cancellato
latest version cambiata
servizio esterno
Le dipendenze necessarie sono già incluse.
Possiamo descrivere questo come un build quasi hermetic.
Non è un sistema hermetic formalmente perfetto:
dipende ancora da Node e dal sistema operativo
ma riduce molto gli input esterni.
Due termini vicini ma distinti.
Con gli stessi input e ambiente controllato:
stesso output
Un’altra persona può ricostruire e verificare lo stesso artefatto a partire dal source dichiarato.
PlumePilot prova a favorire entrambe.
Nel builder:
const fixedZipDate =
new Date("2026-01-01T00:00:00.000Z");
Ogni file nello ZIP riceve quella data.
Perché?
Se usassimo:
timestamp corrente
due build logicamente identici produrrebbero bytes diversi.
collectFiles() termina con:
return files.sort();
Anche l’ordine delle entry ZIP diventa quindi deterministico.
Gli altri parametri includono:
DEFLATE level 9
platform UNIX
Ridurre variabili non funzionali rende più facile confrontare build differenti.
Il builder genera poi:
SHA256SUMS.txt
contenente il checksum di:
Chrome ZIP
Firefox ZIP
Edge ZIP
source ZIP
Il validator lo ricostruisce e confronta.
La chain diventa:
source
↓
build
↓
artifact
↓
SHA-256
↓
review notes / verification
Deve essere:
minimo
eseguibile
senza test
senza documentazione inutile
senza artefatti di sviluppo
Il builder esclude:
docs/
scripts/
tests/
release/
.git/
README
CHANGELOG
...
Deve invece contenere ciò che permette di:
comprendere
ricostruire
verificare
Perciò conserva:
script build
test
README AMO
vendor metadata
licenze
sorgenti
ed esclude per esempio:
.git
release artifact già costruiti
documentazione pubblica non necessaria
Uno stesso progetto produce due package con obiettivi quasi opposti:
runtime → minimo
source → spiegabile
Per il codice scritto da noi possiamo:
riscrivere
rifattorizzare
eliminare pattern
Per una libreria third-party è preferibile mantenere chiaro il rapporto con upstream.
Da qui la strategia:
source vendor leggibile e pinned
+
trasformazioni target-specific documentate
Una patch manuale non documentata in:
vendor/pdf.mjs
crea immediatamente domande:
è ancora PDF.js ufficiale?
cosa è cambiato?
perché?
posso aggiornarlo?
il reviewer può verificarlo?
La build transformation rende la derivazione esplicita.
innerHTML e review surfaceUna delle revisioni storiche ha rimosso pattern come:
element.innerHTML = `
...
${dynamicValue}
...
`;
in punti sensibili, sostituendoli con:
template statico
+
hydration tramite DOM nodes/attributes
Non perché ogni innerHTML dinamico sia automaticamente male.
Ma perché:
dynamic markup
+
extension context
+
review scanner
aumenta la superficie da analizzare.
Ridurre:
dynamic code
dynamic HTML
remote script
unknown vendor
aiuta sia:
security posture
sia:
reviewability
Questo è un caso in cui compliance e buona progettazione possono spingere nella stessa direzione.
Il validator controlla che i file first-party non contengano:
<script src="https://...">
Il principio per MV3 è importante:
codice eseguibile dell'estensione
deve essere comprensibile dal pacchetto
PlumePilot può fare fetch di:
dati
PDF
immagini autorizzate
API della piattaforma
ma non scarica JavaScript da eseguire per cambiare la propria logica.
Questo ci riporta a un concetto di sicurezza classico.
data remoto
≠
codice remoto
Un PDF recuperato da CloudFront è input.
Un file JS recuperato e eval()-uato sarebbe comportamento.
La distinzione è fondamentale.
Il changelog contiene casi apparentemente banali:
Chromium scrollbar gutter
Firefox popup shrink
Gaming frame clipping
right padding
Questi sono bug cross-browser reali.
Non dipendono da:
chrome.tabs
browser.storage
manifest
Dipendono da:
layout engine
popup viewport
scrollbar behavior
timing
rendering
Una UI perfetta in Firefox può risultare:
spostata
compressa
clippata
in Chromium perché lo spazio riservato alla scrollbar è diverso.
Il fix storico non è stato:
aumenta padding ovunque
ma:
compensazione solo dove il comportamento esiste
Ancora una volta:
shared default
+
targeted divergence
La storia di PlumePilot contiene anche una regressione in cui Chromium aveva bisogno di una settling window diversa durante il recovery del capitolo.
Il changelog descrive:
Chromium-only settling window
senza cambiare le normali transizioni Firefox.
Questo è interessante perché ci ricorda:
Il DOM finale può essere uguale, ma il timing con cui ci arriva può differire.
Non possiamo scrivere:
if ("domTimingBug" in window)
Alcune differenze emergono soltanto dal comportamento.
In questi casi abbiamo bisogno di:
osservazione
telemetria locale/log
reproduction test
guard specifico
Quindi il cross-browser engineering non è sempre elegante.
A volte nasce da empirica evidence.
chrome vs browserSpesso si dice:
Chrome usa chrome.*
Firefox usa browser.*
ma la realtà è più sfumata.
Firefox implementa molte WebExtension API anche attraverso compatibilità con il namespace chrome.
PlumePilot usa ampiamente:
chrome.storage
chrome.runtime
chrome.tabs
anche per Firefox.
Il problema non è quindi il nome del namespace in sé.
È:
quale API
quale firma
quale lifecycle
quale supporto
Alcuni ecosistemi WebExtension hanno storicamente differito anche nel supporto Promise/callback.
PlumePilot spesso costruisce wrapper espliciti:
new Promise((resolve) => {
chrome.runtime.sendMessage(message, resolve);
});
Questo crea un’interfaccia Promise interna controllata dal progetto.
È una piccola compatibility abstraction.
Una soluzione rapida sarebbe:
if (navigator.userAgent.includes("Firefox")) ...
Quasi sempre va evitata se possiamo usare:
capability
manifest specialization
feature test
Perché un brand string non descrive precisamente la capability che ci interessa.
Non dobbiamo però trasformare la regola in dogma.
Se abbiamo un bug riproducibile:
solo Chromium
e nessuna capability testabile lo identifica, una branch target-specific può essere il compromesso più corretto.
La domanda è:
stiamo modellando una capability
o
stiamo codificando un workaround empirico?
Se è il secondo, documentiamolo chiaramente.
Possiamo leggere build-release.mjs come un anti-corruption layer.
Il source tree dice:
questo è PlumePilot
Il builder traduce verso:
Chrome expectations
Firefox expectations
Edge expectations
senza far penetrare ovunque quelle differenze.
Da backend developer possiamo pensarlo così:
domain model
↓
adapter PostgreSQL
adapter Redis
adapter external REST API
Qui abbiamo:
extension core
↓
Chrome packaging adapter
Firefox packaging adapter
Edge packaging adapter
Non sono identici, ma il principio è molto simile.
Per una release reale la matrice è più simile a:
| Area | Chrome | Edge | Firefox |
|---|---|---|---|
| Runtime feature | ✓ | ✓ | ✓ |
| Manifest | ✓ | ✓ | ✓ |
| Background | SW | SW | scripts |
| Audio path | offscreen | offscreen | tab fallback |
| Locale package | base | _locales |
base |
| Review-safe transforms | no | no | sì |
| Source archive | no | no | sì |
| Store validation | CWS | Edge | AMO |
“Done” significa attraversare la matrice necessaria.
Consideriamo:
menu-ux.js
Può passare tutti i test Node.
Ma se manca da:
web_accessible_resources
il floating menu potrebbe non caricare lo stylesheet condiviso.
Oppure il source può essere corretto ma il manifest Firefox può contenere offscreen.
Questi sono bug di packaging.
Una pipeline matura distingue:
1. source tests
2. browser/runtime tests
3. artifact tests
funzioni
algoritmi
normalizzazione
browser fixture
DOM
messaging
UI
ZIP
manifest
file presence
checksum
store-specific rules
PlumePilot possiede esempi di tutti e tre.
web-ext lint può produrre:
errors
warnings
Un error può bloccare.
Un warning richiede valutazione.
Il file AMO del progetto dice esplicitamente:
a zero-warning result is not required for AMO approval
Questo evita un errore di processo:
warning presente
↓
panic
↓
patch non necessaria
↓
nuovo rischio
Anche qui serve una tassonomia.
syntax/load error
security/policy error
unsupported API
review warning
informational warning
Non dobbiamo trattarli come equivalenti.
Il Capitolo 14 riprenderà questo modello nel debugging.
Noi sappiamo:
perché c'è una patch
quale account usare
quale corso aprire
quale path vendor è inutilizzato
Il reviewer no.
Perciò dobbiamo trasferire contesto attraverso:
Notes for Reviewers
demo credentials
AMO_SOURCE_README
third-party notices
reproduction commands
Questo è quasi un problema di protocol design.
Una buona procedura dice:
input
passi
risultato atteso
Esattamente come una API specification.
Una procedura vaga:
“provate l'estensione”
ha alta ambiguità.
Una procedura:
apri DEMO
vai al corso X
clicca PlumePilot
esegui Y
verifica Z
riduce il numero di interpretazioni possibili.
if browser è debito potenzialeImmaginiamo:
if (firefox) pathA();
else if (edge) pathB();
else pathC();
ripetuto in 50 file.
Ogni nuova feature moltiplicherebbe:
casi da ragionare
casi da testare
casi da mantenere
Per questo conviene concentrare la divergenza.
Possiamo introdurre un concetto utile:
divergence budget.
Ogni differenza browser-specifica ha un costo futuro.
La aggiungiamo soltanto se risolve una differenza reale.
Il design ideale è:
core comune molto grande
+
adapter specifici molto piccoli
non:
tre implementazioni quasi uguali
Un wrapper è utile quando nasconde una differenza stabile.
Per esempio:
playNotificationSound
nasconde:
offscreen
vs
content-tab fallback
Il caller non deve sapere quale adapter verrà usato.
Un abstraction layer può diventare peggiore del problema se prova a uniformare:
API profondamente diverse
semantiche diverse
lifecycle diversi
e finisce con un’API interna piena di eccezioni.
La regola:
Astrarre la capacità comune, non fingere che gli ambienti siano identici.
Implementiamo un’API mancante con semantica compatibile.
Se una capability manca, usiamo un’altra strategia per raggiungere lo stesso obiettivo.
Generiamo un artefatto diverso prima del runtime.
PlumePilot usa soprattutto:
fallback
+
build specialization
offscreen
non viene polyfilled in Firefox.
Facciamo:
fallback al content tab
Non polyfilliamo:
service worker Firefox
Produciamo:
background.scripts
nel manifest target.
Questa è specialization.
Una delle scelte più preziose è mantenere:
storage keys
schema
operation model
course state
comuni fra browser.
Il changelog sottolinea più volte che cambi di packaging non alterano:
runtime storage keys
Questo preserva il modello mentale e riduce branch applicative.
Ma lo storage di un’estensione è collegato anche all’identità dell’add-on.
Cambiare Firefox ID prima della pubblicazione definitiva ha avuto conseguenze sui dati locali delle build precedenti.
Quindi:
schema compatibile
non basta se cambia:
identity container
È analogo a un database schema corretto ma collegato a un database diverso.
Una versione precedente tentava di leggere:
chrome.runtime.getManifest()
da un contesto in cui l’extension API non era disponibile.
Il fix ha spostato la lettura nel bridge, dove l’API esiste, e ha passato la versione attraverso il protocollo già presente.
Questo unisce:
context isolation
cross-browser
source of truth
La versione dovrebbe venire da:
manifest
e poi propagarsi.
Non da:
const VERSION = "2.35.2";
copiato in cinque file.
Ogni duplicazione crea rischio di drift.
Il manifest può contenere:
gecko_android.strict_min_version
mentre la release AMO può essere intenzionalmente desktop-only.
Dobbiamo distinguere:
metadata di compatibilità
da:
canale distributivo scelto
Sono due decisioni diverse.
Dire:
supportiamo Firefox
significa assumersi:
build
test
review
bug report
update path
documentazione
store metadata
regression matrix
Supporto non è una checkbox tecnica.
È una promessa di manutenzione.
Ogni browser aggiunto aumenta:
compatibility surface
release surface
support surface
Per un progetto piccolo, supportare tre browser bene può essere meglio che supportarne sei superficialmente.
Se aggiungiamo una feature:
Clipboard?
Notifications?
Downloads?
Side panel?
Offscreen?
prima chiediamo:
è una normale Web API
o una WebExtension API?
Le extension API hanno maggiore probabilità di divergere.
Cerchiamo documentazione primaria e costruiamo una matrice:
Chrome
Edge
Firefox
minimum versions
permission
manifest key
Non implementiamo basandoci sul ricordo.
Forse Firefox raggiunge lo stesso obiettivo con un adapter diverso.
Se sì:
domain capability
↓
adapter A / adapter B
Se la differenza è:
manifest
permission
locale
vendor transform
preferiamo build-time.
Se è:
feature opzionale presente/non presente
può essere runtime feature detection.
Ogni nuova differenza dovrebbe produrre almeno un’invariante.
Esempio:
Firefox non deve contenere offscreen permission
diventa:
if (manifest.permissions?.includes("offscreen")) {
throw ...
}
È possibile.
Ed è una pessima release practice.
Perché il file finale non corrisponde più a:
source
build script
checksum atteso
e il prossimo rilascio dimenticherà la correzione.
La regola corretta è:
problema nel pacchetto
↓
correggi builder/source
↓
rigenera
↓
valida
L’artefatto deve essere output, non editor.
Non serve soltanto al reviewer.
Se:
stesso commit
stesso builder
produce bytes diversi ogni volta, diventa difficile capire:
il cambiamento è funzionale?
è metadata ZIP?
è ordine file?
è timestamp?
Riducendo il rumore, il diff diventa più informativo.
Abbiamo due identità diverse.
Identifica:
stato del repository Git
Identifica:
artefatto binario distribuito
Un commit può produrre più artefatti:
chrome.zip
firefox.zip
edge.zip
source.zip
quindi:
commit identity
≠
artifact identity
Entrambe sono utili.
flowchart TD
A[Git commit] --> B[build-release.mjs]
B --> C[Chrome manifest]
B --> D[Firefox manifest]
B --> E[Edge manifest]
B --> F[Firefox review-safe transforms]
C --> G[Chrome ZIP]
D --> F
F --> H[Firefox ZIP]
E --> I[Edge ZIP]
A --> J[Source submission ZIP]
G --> K[SHA256SUMS]
H --> K
I --> K
J --> K
G --> L[validate-release.mjs]
H --> L
I --> L
J --> L
Questa è già una software supply chain in miniatura.
Le differenze non devono cancellare le invarianti comuni.
Per esempio:
stesse feature core
stessi host supportati
stesso storage schema
stessa privacy model
stessi export semantics
stessi message contracts
background environment
offscreen audio path
manifest metadata
locale packaging
vendor review transforms
store notes
Una buona architettura rende chiaro quale categoria contiene ogni differenza.
Chrome e Firefox ZIP non devono essere uguali. Devono avere:
semantic parity.
Cioè:
dato lo stesso scenario utente
otteniamo lo stesso comportamento osservabile
entro le differenze deliberate del browser.
Implementazione:
Chrome → offscreen document
Firefox → content tab
Semantic parity:
utente sente la notifica richiesta
Questo è il livello giusto su cui confrontare i target.
Per una feature condivisa:
happy path
fallback path
browser-specific adapter
storage result
cleanup
error result
Non basta:
“cliccato in Chrome, funziona”
Immaginiamo un aggiornamento JSZip che reintroduce:
un pattern Function(...)
in un punto nuovo.
I test funzionali possono essere tutti verdi.
Ma la submission Firefox può fallire.
Perciò il validator deve vivere nella normale release pipeline, non come script usato soltanto una volta dopo una rejection.
Questo è simile a integrare una API esterna.
Noi non controlliamo:
Chrome Web Store policy
AMO review policy
manifest requirements
data declaration requirements
Quindi dobbiamo trattarli come un external contract.
Non hardcodiamo per sempre nella nostra testa:
“Firefox richiede X”
Prima di una release importante verifichiamo la documentazione corrente.
Le policy sono più dinamiche del codice vendor pinned.
Se inseguiamo ogni warning con una patch locale possiamo deformare il progetto attorno allo store.
La soluzione non è ignorare le policy.
È cercare convergenza con proprietà sane:
no remote executable code
readable source
reproducible build
least privilege
documented dependencies
bounded dynamic behavior
Sono utili anche fuori dal processo di review.
Evitiamo:
background-firefox.js
background-chrome.js
background-edge.js
se differiscono per cinque righe.
Meglio:
shared implementation
+
small adapter
Questo:
try {
chrome.someApi.doThing();
} catch {
...
}
può essere appropriato in alcuni casi.
Ma non sostituisce:
feature test
manifest awareness
documented support matrix
soprattutto se l’errore avviene asincronamente.
Edge può condividere il motore ma avere:
metadati
dashboard
market
listing requirements
localization expectations
review process
differenti.
Un warning può essere:
innocuo
futuro breaking change
security smell
false positive
Va classificato.
Ignorarlo automaticamente è sbagliato quanto correggerlo automaticamente.
Ogni modifica dovrebbe rispondere:
file upstream?
versione?
checksum?
patch?
motivazione?
test?
Se non sappiamo rispondere, stiamo aumentando il rischio supply-chain.
Possiamo leggere PlumePilot così:
DOM / API / domain logic
│
┌──────────┴──────────┐
│ │
extension runtime adapters UI adapters
│
↓
source tree
│
↓
release "compiler"
│
┌──────┼──────┐
↓ ↓ ↓
Chrome Firefox Edge
Uso volutamente la parola compiler in senso architetturale.
build-release.mjs prende:
source representation
e produce:
target representations
con regole specifiche del target.
Il source tree.
La lista dei file runtime + manifest base + metadata.
manifestFor(browser)
Firefox vendor transforms
Edge locale injection
ZIP.
validate-release.mjs.
Questa analogia ci aiuta a separare:
semantica del programma
da:
forma richiesta dal target
Un type system impedisce alcune classi di programma invalido prima dell’esecuzione.
Il release validator prova a impedire classi di artefatto invalido prima della submission.
Esempio:
Firefox + offscreen permission
viene reso un errore di build.
Possiamo quasi leggerlo come:
type FirefoxManifest = {
background: BackgroundScripts;
permissions: ["storage"];
...
}
mentre:
type ChromiumManifest = {
background: ServiceWorker;
permissions: ["storage", "offscreen"];
}
“Ricordati che per Edge servono le due locale.”
È tribal knowledge.
if (
JSON.stringify(packagedLocales) !==
JSON.stringify(["en", "it"])
) {
throw ...
}
È un’invariante eseguibile.
Il secondo approccio scala meglio.
Una feature può degradare:
stesso risultato
path diverso
oppure, se impossibile:
capability non disponibile
UI lo comunica chiaramente
Il problema è il failure silenzioso.
Per un progetto in crescita avrebbe senso avere una matrice esplicita del tipo:
docs/compatibility.md
con:
| Capability | Chrome | Edge | Firefox | Adapter |
|---|---|---|---|---|
| background | service worker | service worker | scripts | build |
| sound | offscreen | offscreen | content tab | runtime |
| storage | local | local | local | shared |
| popup | yes | yes | yes | shared |
| MAIN world | yes | yes | yes* | validated target |
| source review | CWS | Edge | AMO source ZIP | build |
Non perché la tabella sostituisca i test.
Perché rende visibili le promesse.
Molte differenze sono ancora ben localizzate.
Quindi un enorme:
compat.js
potrebbe essere prematuro.
Se aumentassero:
Safari
Firefox Android reale
browser enterprise
API opzionali
potrebbe diventare utile un oggetto capability:
const capabilities = {
offscreenAudio: ...,
backgroundWorker: ...,
...
};
Ma soltanto se riduce realmente branching e duplicazione.
Quando un bug esiste solo in Firefox dobbiamo prima verificare:
stesso account?
stesso corso?
stessa storage state?
stesso manifest?
stessa versione?
Altrimenti possiamo attribuire al browser una differenza causata dai dati.
Non confrontiamo:
Chrome unpacked source
con:
Firefox store ZIP trasformato
senza ricordare che non sono lo stesso artifact.
Per riprodurre un problema store-specifico dobbiamo provare il build target reale.
Se il browser rifiuta di caricare:
manifest problem
Se carica ma la feature fallisce:
runtime problem
Se funziona localmente ma store rifiuta:
policy/review problem
Questa classificazione restringe molto il search space.
Syntax incompatibility
API unavailable
Manifest invalid
Lifecycle mismatch
Behavioral timing divergence
Layout divergence
Packaging omission
Policy violation
Reviewer ambiguity
Vendor provenance failure
Dare un nome al problema aiuta a non applicare il fix sbagliato.
riproduci suono locale
playNotificationSound(sound, volume, sourceTabId)
offscreen permission
service worker
offscreen document
runtime message
background scripts
no offscreen permission
build patch → ensureOffscreenAudioDocument() = false
content-tab fallback
sound
volume
duplicate suppression
achievement
Firefox ZIP non deve contenere:
chrome.offscreen
Questo è un ottimo esempio end-to-end:
requirement
→ adapter
→ build target
→ validator
store/runtime metadata localizzati
manifest strings → __MSG_...__
default_locale → it
_locales/en
_locales/it
Controlla:
set esatto delle locale
chiavi richieste
nessuna locale Edge negli altri target
Ancora:
requirement
→ build
→ artifact invariant
reviewable dependency code
Readable official distributions.
Deterministic review-safe patches.
Original readable files + build instructions.
Checksums + pattern scans.
Abbiamo una vera catena di evidenza.
È sapere:
quali parti appartengono al dominio
quali al browser
quali allo store
quali al build
e tenere i boundary chiari.
Le differenze inevitabili devono essere locali; le semantiche condivise devono restare globali.
È simile a distribuire lo stesso servizio su:
AWS
GCP
on-prem
Il dominio non dovrebbe cambiare.
Cambiano:
IAM
runtime
secrets integration
load balancer
deployment manifest
Chrome Web Store, Edge Add-ons e AMO assomigliano in parte a control plane diversi.
Non eseguono il nostro dominio.
Controllano:
come viene impacchettato
come viene dichiarato
come viene revisionato
come viene distribuito
build-release.mjs è quindi paragonabile a:
Terraform module
Docker build target
Kubernetes overlay
nel senso che trasforma lo stesso prodotto logico per un environment differente.
L’analogia non è perfetta, ma è utile.
Potremmo tentare:
un unico ZIP
per più browser.
meno artefatti.
offscreen permission
background differences
Edge locales
Firefox transforms
review source
rendono il vantaggio piccolo.
Potremmo adottare una libreria che uniformi:
browser.*
chrome.*
Promise/callback
Potrebbe essere utile se il numero di API divergenti aumentasse.
Oggi però non risolverebbe:
manifest
offscreen
AMO review
Edge packaging
quindi sarebbe solo una parte della soluzione.
Potremmo usare:
Vite/Rollup/Webpack + target build
Avremmo più possibilità di compile-time flags.
Ma introdurremmo:
bundling
source maps
source submission complexity
toolchain dependency
Per PlumePilot, che oggi ha source first-party leggibile senza bundle, la semplicità della build Node custom ha vantaggi reali.
Una tecnica possibile:
if (__FIREFOX__) ...
sostituita dal bundler.
Può funzionare.
Ma tende a spargere conoscenza del target nel codice dominio.
Meglio usarla solo se la differenza non può essere confinata nel packaging/adapter.
Tre target significano:
3 manifest
3 ZIP
3 checks
3 store drafts
più source AMO.
Ogni artifact può essere:
minimo
esplicito
validabile
least privilege
La complessità viene spostata nel build, dove può essere automatizzata.
Se build-release.mjs ha un bug, possiamo distribuire un’estensione sbagliata pur avendo:
100% test applicativi verdi
Quindi il release tooling merita:
review
test
versioning
invariants
come il runtime.
Un dependency artifact potrebbe:
cambiare accidentalmente
essere sostituito
essere patchato localmente
provenire da fonte sbagliata
Checksum e provenance riducono questi rischi.
Il builder usa la copia locale leggibile di JSZip.
Quindi persino lo strumento che crea gli ZIP è:
pinned nel repository
anziché scaricato all’ultimo momento.
È una scelta piccola ma coerente con la riproducibilità.
Una policy può vietare qualcosa che tecnicamente sarebbe sicuro nel nostro caso.
Una pratica può essere consentita dalla policy ma comunque rischiosa.
Quindi:
policy compliant
≠
security proof
e:
technically safe
≠
store acceptable
Entrambe le dimensioni contano.
Gli store presentano moduli e categorie differenti.
Il progetto deve mappare:
comportamento reale
verso:
Chrome disclosure
Edge disclosure
Firefox data_collection_permissions
privacy policy
review notes
La fonte non dovrebbe essere il form di uno store. Dovrebbe essere:
data-flow reale dell'estensione
da cui deriviamo le dichiarazioni.
Altrimenti i dashboard possono divergere dal codice.
Dobbiamo pensare:
fresh install
update da versione precedente
storage esistente
permissions
ID
Novità
Non basta che il nuovo ZIP si installi da zero.
Il progetto ha persino dovuto gestire in passato una situazione in cui Firefox saltava una release intermedia.
Per questo:
whats-new-firefox.js
può differire dalla sequenza Chrome/Edge.
Questo è un caso interessante di:
release history as input.
La UI di aggiornamento dipende dal percorso distributivo del target.
Nello snapshot del capitolo:
Chrome
Edge
Firefox
presentano tutti le sei card 2.35.2.
Il builder continua però a mantenere:
whats-new.js
scripts/whats-new-firefox.js
come sorgenti separate.
Perché la necessità potrebbe tornare.
Questa è una forma di flessibilità controllata.
Potremmo dire:
ora i file sono uguali, eliminiamone uno
Ma se Firefox ha una cronologia release differente, il costo della piccola duplicazione può essere inferiore al costo di reintrodurre branching complesso nel builder.
Questo è un trade-off.
Ogni workaround dovrebbe avere una domanda associata:
questa differenza esiste ancora nelle versioni minime supportate?
Se la risposta diventa no:
rimuoviamo il workaround
Altrimenti il compatibility layer cresce per accumulo geologico.
Se una API diventa universalmente disponibile sopra una certa versione:
supportare browser più vecchi
ha un costo.
La decisione è prodotto + engineering:
quanti utenti perdiamo?
quanto codice eliminiamo?
quali bug evitiamo?
Quando dobbiamo sapere se una API è supportata:
preferiamo:
MDN
Chrome for Developers
Firefox Extension Workshop
specifica ufficiale
e verifichiamo la data.
Non affidiamoci a un vecchio post Stack Overflow come contratto.
Quando una futura feature introduce una nuova API browser:
Abbiamo questa esigenza:
copiatura automatica negli appunti
e un’API disponibile solo in alcuni contesti.
Domande:
Supponi che Edge richieda una terza locale:
fr
Dove faresti la modifica?
Soluzione desiderabile:
edgeLocales
+
messages file
+
validator
non:
modifica manuale allo ZIP dopo il build
Un upgrade PDF.js modifica il pattern usato da:
replaceExactly()
e la build Firefox fallisce.
È un bug del builder?
Non necessariamente.
Il fallimento è intenzionale.
Prima dobbiamo:
ispezionare upstream
capire la nuova semantica
decidere se la patch serve ancora
aggiornare transform + validator
Il pacchetto Firefox funziona localmente ma AMO segnala:
unknown minified code
Costruisci una diagnosi per livelli:
runtime?
manifest?
artifact?
vendor provenance?
policy?
source submission?
Chrome e Firefox mostrano lo stesso popup, ma solo Chrome taglia il bordo destro.
Perché sarebbe sbagliato iniziare da:
if (isChrome) width -= 8;
senza prima misurare:
scrollbar
viewport
box sizing
overflow
?
Perché questa condizione:
if (chrome.offscreen?.createDocument)
è migliore di:
if (!isFirefox)
ma non può sostituire la specializzazione del manifest?
language
API
manifest
lifecycle
package
policy
review
UX
Non duplicare il dominio per adattarti al browser.
Chiedi alla capability, non al brand.
Manifest e packaging sono ottimi candidati per differenze compile/build-time.
Testa ciò che distribuirai, non soltanto ciò che hai nel repository.
Se una patch target-specific non può più essere applicata con certezza:
interrompi la build
Readable source, provenance e riproducibilità non sono burocrazia separata dal software.
Browser diversi possono usare adapter differenti e mantenere lo stesso comportamento utente.
Timestamp, ordering e checksum controllati rendono più facile verificare gli artifact.
Va riletta periodicamente e non memorizzata una volta per sempre.
| Problema | Chrome | Edge | Firefox | Strategia PlumePilot |
|---|---|---|---|---|
| background | service worker | service worker | scripts | build specialization |
| audio senza DOM | offscreen | offscreen | tab fallback | capability + fallback |
| permission offscreen | sì | sì | no | target manifest |
| add-on ID Gecko | no | no | sì | target manifest |
| data declarations Gecko | no | no | sì | target manifest |
| locale store runtime | base | _locales |
base | package injection |
| readable vendor | sì | sì | sì | pinned readable upstream |
| review-safe vendor transforms | no | no | sì | deterministic build patch |
| source submission | no | no | sì | dedicated source ZIP |
| checksum artifact | sì | sì | sì | SHA256SUMS |
| UI timing/layout workaround | quando serve | quando serve | quando serve | localized behavioral fix |
MDN:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background
Chrome Manifest V3 migration:
https://developer.chrome.com/docs/extensions/develop/migrate/checklist
Chrome for Developers:
https://developer.chrome.com/docs/extensions/reference/api/offscreen
MDN:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings
Firefox Extension Workshop:
https://extensionworkshop.com/documentation/publish/add-on-policies/
https://extensionworkshop.com/documentation/publish/source-code-submission/
https://extensionworkshop.com/documentation/publish/third-party-library-usage/
Chrome for Developers:
https://developer.chrome.com/docs/webstore/program-policies/code-readability
PlumePilot parte da un’unica codebase.
Ma il prodotto distribuito non è un unico artefatto.
La pipeline produce:
Chrome ZIP
Firefox ZIP
Edge ZIP
AMO source ZIP
SHA256SUMS
perché i target condividono molto ma non tutto.
Le differenze principali vengono confinate in:
manifest generation
permissions
background model
offscreen fallback
Edge locales
Firefox review-safe transforms
release validation
mentre il dominio rimane il più possibile comune.
La lezione più importante non è:
come supportare Firefox
o:
come pubblicare su Edge
È una lezione di architettura più generale:
Quando una piattaforma esterna impone differenze inevitabili, concentriamole in adapter e build boundary verificabili invece di lasciarle contaminare il dominio.
E soprattutto:
Il software che consegniamo non è il repository. È l’artefatto prodotto dalla nostra supply chain.
Adesso abbiamo tutti gli ingredienti per studiare uno degli aspetti più istruttivi di PlumePilot.
Non:
come scrivere test in astratto
ma:
come un bug reale diventa conoscenza permanente
Vedremo casi come:
accordion chiusi
autoplay regressions
chapter routing
first-incomplete
Firefox review
EPUB fidelity
UI synchronization
hidden tabs
duplicate operations
e costruiremo il percorso:
symptom
↓
reproduction
↓
hypothesis
↓
instrumentation
↓
root cause
↓
regression test
↓
invariant
Il vero obiettivo sarà capire quando un bug smette di essere semplicemente “corretto” e diventa invece una nuova proprietà formalizzata del sistema.
← 12 — Performance e memoria · Indice · 14 — Debug, test e regressioni →