Internals — Build custom (packages/cmp-web/builds/)
Non è API pubblica. Come si costruisce e si pubblica il banner: i pacchetti in
packages/cmp-web/builds/, il loader v3 e la pipeline CDN. Un "build custom" (rai,cmp-saas-preview) è un bundle separato che consuma@avacy/cmp-webcome libreria e aggiunge un layer specifico per un cliente/scenario. Materiale per il team.
Ogni build è una cartella builds/<nome>/ con un build.mjs (descriptor scoperto da scripts/build.mjs) e un'entry esbuild propria. Tutte consumano l'output Stencil dist-custom-elements e risolvono @avacy/core a bundle time: l'output dist/cmpweb/cmpweb.esm.js tiene @avacy/core come bare specifier e funziona solo dietro un bundler. Per lo stesso motivo ogni entry reinietta global.scss (setupGlobalStyles: <style id="avacy-global-css"> in pagina più un patch di attachShadow limitato agli elementi avacy-*), che il target dist-custom-elements non include.
web — pacchetto generico
Nessuna specializzazione cliente, nessuna config bakata: la config arriva da banner.json (via loader) o dal blocco #avacy-config. Il build (yarn build web, builds/web/build.mjs) produce in dist/web/:
| File | Cos'è |
|---|---|
cmpweb-web.esm.js | La libreria (src/entry.ts): riesporta tutto @avacy/cmp-web, sostituisce init e defineCustomElements con versioni che prima applicano gli stili globali e definiscono i custom element. All'import non avvia nulla. È il file che carica il loader |
cmpweb-web.auto.esm.js | src/auto.ts: la stessa libreria più init() al caricamento, per l'inclusione manuale |
loader.js | Il loader v3 (src/loader.ts), IIFE classico con versioni e manifest bakati: vedi sotto |
tcf-stub.js | Lo stub IAB __tcfapi come IIFE (src/variants/tcf-stub.ts, costruito nello step core e copiato qui), per chi lo include a mano |
default-config.json | DEFAULT_CONFIG di @avacy/core serializzato dal build: i default di questa release. Il SaaS lo legge dal puntatore /v3/default-config.json |
assets/images/, assets/languages/ | SVG di shield e watermark; i 32 file lingua copiati da packages/core/public/assets/languages/ |
example/index.html | Pagina d'esempio (inclusione manuale) |
La versione del pacchetto è quella di packages/cmp-web/package.json (niente VERSION proprio): <v>+<sha> in dev, <v> o <v>-rc.N / <v>-pr<n>.<run> in release. Gli asset sono versionati con il bundle: su CDN stanno sotto /v3/web/<versione>/assets/.
Fonte di riferimento operativa: packages/cmp-web/builds/web/README.md.
Loader v3
builds/web/src/loader.ts → loader.js. È il file del tag di installazione, uno per release e uguale per tutti i webspace.
- Script classico, non modulo (format IIFE): GTM inietta il tag come
<script>classico. Legge il proprio URL dadocument.currentScript(fallback: il primoscript[src*="/v3/"][src*="webspace="], per un tag scritto contype="module"). - Versioni bakate a build time:
__AVACY_WEB_VERSION__(la versione del bundle) e__AVACY_ADDONS__, il manifest degli add-on (nome → { version, phase }). Il manifest lo costruiscescripts/addon-manifest.mjs: gli add-on sono ledependenciesdi@avacy/cmp-webche stanno inpackages/addons/<nome>/, con laversione l'avacy.phase(pre/post, obbligatoria) del loropackage.json. Oggi:preemptive-block(pre),consent-archive(post). - Tutto dallo stesso host: tenant = primo pezzo dell'hostname,
webspacedalla query. In parallelo:modulepreload+import()di/v3/web/<versione>/cmpweb-web.esm.jsefetchdi/config/<tenant>/<uuid>/banner.json. - Niente config, niente banner: HTTP non-ok (cancellare
banner.jsonè il modo in cui il backend spegne un webspace), errore di rete, JSON che non è un oggetto, tag senza?webspace=→console.error('[avacy-loader] …')e stop.init()non parte, nessun default. - Varianti (#625): lo stesso
src/loader.tsesce in quattro file.loader.js(base), poiloader-tcf.js,loader-gcm.jseloader-tcf-gcm.js, con in testa lo stub IAB (src/prelude-tcf.ts) e/o il default di Consent Mode (src/gcm-default.ts: tuttodenied,wait_for_update: 500, si fa da parte se c'è__AVACY_GTM_CONSENT_TEMPLATE__o unconsent defaultgià neldataLayer). La build le monta con esbuild (LOADER_VARIANTSinbuild.mjs), nessuna è scritta a mano. Sul CDN:/v3/loader/<ver>/<variante>.js,baseresta/v3/loader/<ver>.js. Le sceglierollout.jsdai flag del webspace. - Add-on: da
addons[]dibanner.jsontiene solo i nomi del manifest (gli altri: warning). Iprepartono subito da/v3/addons/<nome>/<versione>/index.js; un modulo il cuidefaultnon è{ name: <nome>, setup }conta come fallito e finisce infailed. Iprearrivati prima del bundle aspettano in coda e vengono registrati appena c'èregisterAddon. Ipostsi scaricano al primoavacy:ready({ once: true }). - Boot:
lib.init(undefined, { remote: { ...bannerJson, addons, assetPath }, addons: { expected: <pre>, failed } }).assetPathviene riscritto su/v3/web/<versione>/assets:banner.jsonnon può sapere quale release sceglie l'edge.
Pipeline CDN /v3/
Separata dalla pipeline v2 (deploy-cmp-web.yml, che sincronizza dist/ su avacy/cmp-web/).
scripts/publish-plan.mjsdecide (puro, niente AWS) cosa va dove.scripts/publish-v3.mjslo esegue su S3.- Path immutabili (
Cache-Control: max-age=31536000, immutable; collisione = errore, mai riscrivere):/v3/web/<ver>/—cmpweb-web.esm.js,cmpweb-web.auto.esm.js,tcf-stub.js,default-config.json,assets/**/v3/saas-preview/<ver>/—cmpweb-cmp-saas-preview.esm.js,tcf-stub.js,assets/**/v3/addons/<nome>/<versione>/index.js— versionati per conto loro; una collisione con contenuto identico è riusata (una release spedisce spesso un add-on invariato)
- Loader versionato
/v3/loader/<ver>.js(cache un'ora), caricato per ultimo: se c'è, la release è tutta online. - Puntatori (mutabili, cache un'ora), in due gruppi spostati in momenti diversi:
defaults—/v3/default-config.jsone/v3/saas-preview.json({"version":"<ver>"}, quale bundle carica l'anteprima dell'editor): a ogni release dichiarata;fallback—/v3/avacy-cmp-web.js, il loader all'URL fisso, servito solo quando la CloudFront Function o il suo KeyValueStore mancano: si sposta solo quando la release ha il 100% del traffico, o annullerebbe un rollout parziale.
.github/workflows/deploy-banner-v3.yml:- push su
development→ staging: ogni merge è una pre-release-rc.<100 + run_number>(web + saas-preview), pubblicata e con tutti i puntatori spostati subito, più l'invalidazione dei soli tre puntatori; - push su
master→ produzione: pubblica la versione dipackage.json, chiamabanner:rollout release, sposta idefaultssubito e ilfallbacksolo a traffico pieno. Il job gira solo se è impostataPROD_BANNER_BUCKET; - run manuale su
master:status/release/weight/promote/rollbackviabanner:rollout.
- push su
banner:rolloutè un comando artisan dell'API (lanciato da CI con.github/banner-v3/rollout.shin un task one-off): l'unico che scrive nel KeyValueStore del rollout, cioè quale versione (e con che peso) risponde all'URL fisso.- Ambienti PR:
pr-environment.ymlpubblica la stessa build come-pr<n>.<run>sottopr<n>/v3/dello stesso bucket (withPrefix); la CloudFront dell'ambiente mappa la sua radice sul prefisso, così i path del loader restano giusti.
cmp-saas-preview — anteprima nell'editor SaaS
Entry propria (builds/cmp-saas-preview/src/entry.ts) → cmpweb-cmp-saas-preview.esm.js: stesso schema generico di web, avvio automatico (defineCustomElements() + init()), config solo dal blocco #avacy-config della pagina di anteprima.
- Il primo import è
neutralize-iframe.ts, che impostawindow.__AVACY_FORCE_STANDARD_LAYOUT__prima ancora che@avacy/cmp-webvenga importato: l'anteprima (essa stessa un iframe) mostra il layout standard, non quello compatto. Dettaglio in Internals — cmp-web § Ambiente di esecuzione. - Si pubblica con la pipeline v3, nello stesso run e alla stessa versione di
web(/v3/saas-preview/<ver>/); l'editor sceglie la versione dal puntatore/v3/saas-preview.json. - Non passa dal loader, quindi non carica add-on: niente
consent-archive, e un click esplorativo nell'editor non entra nell'archivio consensi del cliente.
Altro in builds/ e dintorni
builds/shared/— codice dei build, non un build:release-assets.mjs(gli asset che ogni bundle pubblicato copia sotto<bundle>/<versione>/assets/),content-types.mjs,server.mjs(server locale che simula la topologia di deploy di un build cliente, solo per prove),override/(harness Playwright per provare un build sul sito vero),perf/,tests/.builds/web/bench/— banco di prova del loader con add-on finti (yarn bench:webdapackages/cmp-web): pagine conbanner.jsoneaddons/locali. Istruzioni inbench/README.md.src/variants/tcf-stub.ts— non un build cliente: l'entry esbuild didist/tcf-stub.js(lo stub IAB come IIFE autoeseguito). Lo stesso stub, da npm, sta in testa alle variantitcfetcf-gcmdel loader.
rai — il build più grande e complesso
Entry propria (builds/rai/src/entry.ts) → cmpweb-rai.esm.js, con config bakata (config.json). Non passa dalla pipeline /v3/: si pubblica ancora con deploy-cmp-web.yml (sync di dist/ su avacy/cmp-web/) e si consegna anche come zip. Tre layer aggiuntivi sopra il core, indipendenti fra loro: v2-compat (retrocompatibilità con l'API del vecchio banner), rai-native (bridge con l'app nativa iOS/Android), syndication (consenso ereditato da un CMP master upstream).
Boot e merge della config
entry.ts non usa l'auto-init standard di cmp-web: importa defineCustomElements esplicitamente e chiama init() da sé, dopo aver registrato i provider custom. Ordine deliberato:
v2-compatimportato per primo, prima ancora del modulo Stencil pesante — un modulo TypeScript leggero valuta più in fretta del componente Stencil, e il compat deve installarsi prima diDOMContentLoadedper vincere la race con l'evento di boot nativo (oil-dom-loaded) e non lasciare una finestra in cui script partner cercanowindow.AS_OILe non lo trovano ancora.- Registrazione provider (
registerStoreProvider('syndication', ...),registerStoreProvider('rai-native-webview', ...)), bridge nativo e deep-link legacy — devono essere pronti prima cheinit()risolvastore.type. defineCustomElements(), poi merge config, poiinit(merged).
Merge della config: RAI_CONFIG (bake-in, config.json) come base, l'inline #avacy-config sopra — ma non un semplice spread flat. core e store sono deep-merge a un livello (un inline { store: { type: 'rai-native-webview' } } non cancella il consentKey bakeato). frameworks fa merge per discriminatore (tcf/acm/custom/gcm): un inline può sovrascrivere solo una chiave annidata (es. tcf.gvlUrl per ambiente) senza dover riscrivere l'intero array e perdere i vendor custom bakeati. Tutte le altre chiavi restano last-write-wins.
consentFilter RAI-specifico: applica una regola di legittimo interesse sensibile allo stato per le azioni bulk (Accetta/Rifiuta tutto), diversa da quella generica del core — LI "standard" (TCF, e custom senza opt-out) preservato da entrambe le bulk action; LI "opt-out" (custom purposes con optOut: true) preservato da Deactivate ma forzato ON da Activate (ri-attiva un opt-out che l'utente aveva tolto). Un RejectAll coalizzato da un Personalized (obiezione esplicita dalla CPC) passa attraverso senza alterazioni — l'utente ha già spento tutto consapevolmente.
rai-native — bridge con l'app nativa
Il bridge notifica la shell nativa (Android via metodi diretti su window.Android, iOS via postMessage sui message handler webkit) di 3 eventi: consenso decodificato (sendDecodedConsent), mostra WebView (show/showView), distruggi WebView (destroy/hideView). Fault-tolerant: se né window.Android né il message handler webkit esistono (bundle caricato fuori da una WebView RAI), ogni chiamata è un no-op silenzioso — consuma solo la superficie pubblica di cmp-web (CoreEvents, getContextData, eventi DOM Tier 1), zero accesso allo store interno Stencil.
Lo store nativo (RaiNativeWebViewStoreProvider) persiste in un formato che rispecchia il contratto legacy RAI, non un formato Avacy nativo: blob principale sotto una chiave maiuscola + <CHIAVE>_EXPIRY, TCF proiettato nelle variabili IABTCF_* standard, ACM mergiato in IABTCF_AddtlConsent, custom proiettato in customVendors/customPurposes, GCM proiettato solo se esiste storage GCM standalone esplicito (in modalità mapping resta un no-op intenzionale).
Due deep-link legacy letti dalla querystring (?prefcenter=1 apre la CPC, ?avacy-rejectall=1 persiste un RejectAll) — cachati dopo la prima lettura, condivisi fra il bridge (seed di cpcOpen al boot) e l'handler post-avacy:ready.
syndication — consenso ereditato da un master upstream
Attivazione automatica: se window.AVACY_SYNDICATION_TCSTRING (canonico) o il legacy AS_OIL_SYNDICATION è presente PRIMA che il bundle carichi, entry.ts forza store.type: 'syndication' sopra qualunque config — vince sempre sullo store configurato, perché il master ha già deciso il consenso e uno store locale divergerebbe. Se sono definiti entrambi i globali, vince il canonico e viene loggato un warning.
Il provider è in-memory per design — writeConsent è un no-op, non persiste mai nulla: il consenso del child segue sempre quello del master, non ha una copia propria da salvare.
⚠️ Re-stamping "bit-surgical", non decode/encode completo. Per sovrascrivere i 3 campi che il child deve cambiare (IAB TCF v2.2 §B.1) il provider non passa dalla libreria
@iabtechlabtcf/core(costerebbe ~50KB in più nel bundle) — modifica direttamente i bit nel segmento core della TCString, un percorso da ~1KB senza dipendenze runtime. Chi tocca questo codice deve capire l'encoding binario della TCString, non solo l'API della libreria IAB.
v2-compat — retrocompatibilità con window.AS_OIL/window.AVACY
Reimplementa la superficie pubblica del banner legacy (v2.43.x, oil.js) sopra la v3: ogni metodo del vecchio AS_OIL/AVACY è mappato sulla nuova API Tier 1. Organizzato per gruppo (consent-actions, ui-control, consent-query, cookie-config-debug, event-collection, misc) — 6 moduli sotto methods/, caricato come side-effect import così il bundle cmpweb-rai.esm.js porta il compat insieme al core v3.
Non tutto è mappato di proposito: applyGDPR, clearVendorlistCaches, getLegalText, getLoginStatus, setLoginStatus, status, getFullConfiguration sono esplicitamente fuori scope — RAI non li chiama mai in produzione (verificato contro l'uso reale, non solo l'API storica). Un sito che dipendesse da uno di questi vedrebbe il metodo assente, non un adattamento silenzioso.