universal-ad-loader 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ # UAL come libreria: componenti per React, Next.js, Vue e Astro
2
+
3
+ Oltre al tag `<script>` (CDN), UAL si può usare come pacchetto: import sicuro anche sul server, componenti per i framework, tipi TypeScript. Stessa configurazione e stesso comportamento del tag.
4
+
5
+ > **Stato:** il pacchetto `universal-ad-loader` è pronto ma **non ancora pubblicato su npm** (come pubblicarlo: `pubblicazione-npm.md`). Per provarlo ora: `npm pack` nella cartella del loader e `npm i ./universal-ad-loader-0.1.0.tgz` nel progetto, oppure `npm i /percorso/universal-ad-loader`.
6
+
7
+ ## Quando conviene cosa
8
+
9
+ | | Tag `<script>` dalla CDN | Pacchetto (componenti) |
10
+ |---|---|---|
11
+ | Siti senza build (HTML, WordPress) | ✅ | — |
12
+ | Siti React / Next / Vue / Astro | funziona, con attenzioni (sotto) | ✅ consigliato |
13
+ | Idratazione e cambio pagina | a carico tuo | gestiti dai componenti |
14
+ | Tipi e completamento della config | — | ✅ `defineConfig` |
15
+ | Aggiornare loader/config senza rideploy | ✅ | ✅ con `src` (loader) e `config` come URL |
16
+
17
+ I componenti supportano entrambi i modelli: il **loader** si prende dalla CDN (`src="…/ual.min.js"`) oppure è incluso nel bundle del sito; la **configurazione** è un URL (modifiche senza rideploy) oppure un oggetto nel codice.
18
+
19
+ ## React
20
+
21
+ ```tsx
22
+ import { UalProvider, AdSlot } from 'universal-ad-loader/react';
23
+ import { defineConfig } from 'universal-ad-loader';
24
+
25
+ const config = defineConfig({
26
+ site: 'mio-sito',
27
+ providers: { main: { adapter: 'json', endpoint: 'https://ads.tuodominio.net/v1/decide' } },
28
+ slots: { top: { sizes: [[728, 90]], providers: ['main'] } },
29
+ });
30
+
31
+ export function App() {
32
+ const { pathname } = useLocation(); // react-router, o il tuo router
33
+ return (
34
+ <UalProvider config={config} pageKey={pathname}>
35
+ <AdSlot name="top" minHeight={90} />
36
+ </UalProvider>
37
+ );
38
+ }
39
+ ```
40
+
41
+ - `config`: oggetto o URL del JSON (letto solo all'avvio).
42
+ - `src`: URL del loader su CDN; senza, UAL è incluso nel bundle del sito.
43
+ - `pageKey`: quando cambia (percorso), UAL ricalcola regole per pagina e slot.
44
+ - `targeting`: coppie chiave/valore, anche dinamiche.
45
+ - `onEvent(evento, dati)`: tutti gli eventi (`ready`, `consent`, `slotRendered`, `slotEmpty`, `error`…), compresi quelli iniziali.
46
+ - `useUal()`: `{ ual, ready }` (`ual` è `null` sul server e prima dell'avvio).
47
+ - `AdSlot`: `name`, `params`, `minHeight`, `className`, `style`, `id`. È un `div` vuoto: UAL lo riempie.
48
+
49
+ ## Next.js
50
+
51
+ Stessi componenti da `universal-ad-loader/next`: il cambio pagina è automatico (`usePathname`). Funziona con App Router e importato da un Server Component (i file hanno `'use client'`).
52
+
53
+ ```jsx
54
+ // app/layout.jsx (Server Component)
55
+ import { defineConfig } from 'universal-ad-loader';
56
+ import { UalProvider } from 'universal-ad-loader/next';
57
+
58
+ const config = defineConfig({ /* … */ });
59
+
60
+ export default function RootLayout({ children }) {
61
+ return (
62
+ <html lang="it"><body>
63
+ <UalProvider config={config}>{children}</UalProvider>
64
+ </body></html>
65
+ );
66
+ }
67
+
68
+ // app/page.jsx
69
+ import { AdSlot } from 'universal-ad-loader/next';
70
+ export default function Page() { return <AdSlot name="top" minHeight={90} />; }
71
+ ```
72
+
73
+ ## Vue 3 e Nuxt
74
+
75
+ ```vue
76
+ <script setup>
77
+ import { UalProvider, AdSlot } from 'universal-ad-loader/vue';
78
+ import { useRoute } from 'vue-router';
79
+ const route = useRoute();
80
+ const config = { /* … */ };
81
+ </script>
82
+
83
+ <template>
84
+ <UalProvider :config="config" :page-key="route.fullPath" @event="(name, data) => console.log(name)">
85
+ <AdSlot name="top" :min-height="90" />
86
+ </UalProvider>
87
+ </template>
88
+ ```
89
+
90
+ Stesse prop (kebab-case) e stesso `useUal()` (con ref). UAL parte in `onMounted`: solo nel browser e dopo l'idratazione. Il rendering sul server di Vue è verificato dai test; **Nuxt non è stato provato** (il provider va messo in `app.vue`, dove vive il layout).
91
+
92
+ ## Astro
93
+
94
+ Gli slot nelle pagine `.astro` sono HTML statico: nessuna idratazione.
95
+
96
+ ```astro
97
+ ---
98
+ import UalBundled from 'universal-ad-loader/astro/UalBundled.astro'; // UAL nel bundle del sito
99
+ // oppure: import UalLoader from 'universal-ad-loader/astro/UalLoader.astro'; // loader dalla CDN
100
+ import AdSlot from 'universal-ad-loader/astro/AdSlot.astro';
101
+ import { config } from '../config';
102
+ ---
103
+ <head><UalBundled config={config} /></head>
104
+ <body><AdSlot name="top" minHeight={90} /></body>
105
+ ```
106
+
107
+ - `UalBundled`: la configurazione sta in un blocco `application/json` (non eseguito: compatibile con una CSP rigida) e UAL è incluso nel bundle.
108
+ - `UalLoader` (`src`, `config`): carica `ual.min.js` dalla CDN, con la config come URL o come oggetto.
109
+ - Entrambi, con le View Transitions (`ClientRouter`), ricalcolano le regole per pagina a ogni navigazione.
110
+
111
+ ## Nucleo e Node
112
+
113
+ ```js
114
+ import { createUal, getUal, defineConfig, version } from 'universal-ad-loader';
115
+ ```
116
+
117
+ - **Nessun effetto collaterale all'import:** si può importare in Node, in un Server Component, in un file di configurazione. Il DOM si tocca solo quando si chiama `init()` nel browser.
118
+ - `getUal()`: l'istanza condivisa della pagina. Sul server lancia un errore chiaro: chiamalo dopo il montaggio.
119
+ - `createUal()`: una nuova istanza (utile nei test).
120
+ - `defineConfig(config)`: identità con i tipi. **Segnala un provider inesistente nel waterfall** di uno slot.
121
+
122
+ Il rendering sul server produce solo i `div` degli slot (con `min-height` se indicato: nessun salto di layout). Gli annunci arrivano nel browser.
123
+
124
+ ## Cosa gestiscono i componenti (e il tag no)
125
+
126
+ - **Idratazione.** UAL parte in un effect, dopo l'idratazione. Caricare invece il loader con un tag nell'HTML prima dell'idratazione fa segnalare a React *Hydration failed* (anche con `suppressHydrationWarning`) e fa richiedere lo slot **due volte**: il test del repository lo riproduce (`test/wrappers.mjs`, "Senza componenti").
127
+ - **Cambio pagina.** `pageKey` / `usePathname` / `astro:page-load` chiamano `pageView()`: le regole per pagina si ricalcolano e le richieste sono quelle giuste, senza duplicati.
128
+ - **StrictMode e doppio montaggio.** Una sola istanza e un solo `init` per pagina.
129
+ - **Eventi e targeting dall'inizio.** Si agganciano prima dell'`init`: `onEvent` riceve anche `ready` e `consent`, e il targeting vale per la prima richiesta.
130
+
131
+ ## Cosa resta uguale
132
+
133
+ - **CSP rigida.** Le creatività girano in un iframe che eredita la CSP della pagina: senza `'unsafe-inline'` per script e stili il banner compare ma `UALExt` e il click tracciato non funzionano. Vedi il README.
134
+ - **Un solo modo per pagina.** Non usare insieme il tag e i componenti sulla stessa pagina (il secondo `init` viene ignorato con un avviso).
135
+ - **Versioni.** Con `src` (CDN) il loader può essere più vecchio o più nuovo del pacchetto: i componenti usano solo l'API comune e non richiedono `isInitialized`/`whenReady` (se mancano, chiamano `init`).
136
+
137
+ ## Come è verificato
138
+
139
+ `npm test` esegue, oltre ai test del loader:
140
+
141
+ - `test/package.mjs`: import in Node (ESM e CJS) senza `window`, errore chiaro di `getUal()` sul server, rendering sul server di React e Vue, mappa degli export, contenuto del pacchetto (`npm pack`), tipi (usi corretti compilano, quelli errati danno errore).
142
+ - `test/wrappers.mjs`: in un browser vero, con rendering sul server e idratazione ritardata (il caso peggiore): React (UAL nel bundle, loader da CDN, versione Next), Vue, **Next.js 16** (App Router, build di produzione, Server Component, `<Link>`) e **Astro 7** (View Transitions). Per ognuno: nessun errore di idratazione, console pulita, una richiesta per slot, regole per pagina ricalcolate alla navigazione senza richieste doppie.
@@ -0,0 +1,114 @@
1
+ # Pubblicare `universal-ad-loader` su npm
2
+
3
+ Si pubblica come `pm2-dash`: con la **CI di GitLab**, spingendo un branch dedicato (`deploy-npm`). Il pacchetto è già pronto (`universal-ad-loader`, MIT, autore ArdensLab, `publishConfig.access = public`, `prepack` che costruisce, `.gitlab-ci.yml`, `CHANGELOG.md`). Mancano solo i passaggi una tantum qui sotto, che richiedono le tue credenziali.
4
+
5
+ La pubblicazione è **irreversibile**: una versione pubblicata si può solo deprecare (ritirarla è possibile solo entro 72 ore e se nessuno dipende da lei).
6
+
7
+ ## Come funziona
8
+
9
+ - **GitLab è l'unica CI.** Il repository di riferimento sta su GitLab (privato); GitHub (`ArdensLab/ual`) è solo un *push mirror* di `master`.
10
+ - **Il branch `deploy-npm` è il comando di pubblicazione:** un push lì lancia la pipeline. Non `master`, non i tag.
11
+ - **I tag** (`v0.1.0`…) sono marcatori di versione, come in `pm2-dash`: annotati, sul commit "Release X" in cui `package.json` ha quella versione. Non pubblicano nulla, ma la loro pipeline rilancia i test e il `release-check`.
12
+
13
+ La pipeline (la stessa di `pm2-dash`, con in più i job che servono a un pacchetto con test in browser):
14
+
15
+ | Stadio | Job | Cosa fa |
16
+ |---|---|---|
17
+ | test | `code_quality` | `oxlint` sui sorgenti, report nella merge request |
18
+ | test | `test-node` (matrice Node **22 e 24**) | `npm run test:node`: import in Node (ESM e CJS), rendering sul server, mappa degli export, contenuto del tarball, tipi |
19
+ | test | `test-browser` | `npm run test:browser`: loader e regressioni in Chromium (immagine Playwright) |
20
+ | test | `test-frameworks` | `npm run test:frameworks`: React, Vue, **Next.js 16** (build di produzione) e **Astro 7**; l'unico che installa `test/heavy` |
21
+ | test | `release-check` (solo tag e `deploy-*`) | il primo titolo del CHANGELOG è la versione di `package.json`; un tag è esattamente `v<versione>`; su `deploy-*` quella versione **non è già su npm** |
22
+ | deploy | `npm-publish` | componente condiviso `matteo/ci-templates/npm-publish@1.5.0`: `npm ci` e `npm publish --access public` in `node:22` |
23
+ | deploy | `gitlab-npm-publish` | stesso pacchetto sul registro npm di GitLab (`CI_JOB_TOKEN`, nessun segreto); parte **solo dopo** `npm-publish`, così un rifiuto di npm non lascia copie orfane |
24
+
25
+ Tutti i job `test` pubblicano un report JUnit (scheda Tests della pipeline). Gli stadi sono in sequenza: **se un job di `test` fallisce, la pubblicazione non parte.**
26
+
27
+ Perché è fatta così:
28
+ - **Pesa poco.** I job leggeri installano solo la radice (~120 MB); Next.js e Astro (~620 MB) stanno in `test/heavy`, un progetto a parte con il proprio lockfile, che installa solo `test-frameworks`. I job girano in parallelo.
29
+ - **La build sta in `prepack`:** il componente gira senza browser, quindi i test non possono stare in `prepublishOnly` (romperebbero il job). I test li esegue lo stadio `test`.
30
+ - `release-check` fa fallire presto ciò che `npm publish` scoprirebbe tardi (versione già pubblicata, CHANGELOG dimenticato).
31
+
32
+ ## Una tantum
33
+
34
+ ### 1. Il nome: `universal-ad-loader`
35
+ Storia dei nomi (2026-11-04), perché il nome si decide **solo pubblicando**:
36
+
37
+ - `ual`: **non disponibile**, su npm è di un altro utente (`hedin_hiervard`, dal 2018, versioni 0.1.0, 0.1.1 e 1.0.0).
38
+ - `@ardenslab/ual`: è stato pubblicato e poi ritirato (`Unpublished`): npm non permette più di riusare quella coppia nome e versione (`0.1.0`) e blocca il nome per 24 ore.
39
+ - `ual-loader`: **rifiutato da npm** con `E403 Package name too similar to existing packages url-loader, raw-loader, null-loader, yaml-loader`. `npm view` lo dava libero: il filtro anti-typosquatting si applica ai nomi senza scope e si vede solo al momento della pubblicazione.
40
+ - **`universal-ad-loader`**: libero (`npm view` dà E404). Se anche questo fosse rifiutato dal filtro, l'alternativa che npm suggerisce di solito è lo scope: `@ardenslab/universal-ad-loader` (lo scope `@ardenslab` è l'utente npm `ardenslab` e non è soggetto al filtro; basta aggiornare nome, import e documenti con la stessa sostituzione).
41
+
42
+ Nessuna organizzazione da creare: si pubblica dall'utente npm **`ardenslab`**, lo stesso di `pm2-dash`.
43
+
44
+ ### 2. Repository
45
+ Segui la convenzione di `pm2-dash`, dentro `~/workspace/ArdensLab/` (là è configurata l'identità SSH di ArdensLab):
46
+
47
+ 1. **Fatto (2026-11-04):** il progetto GitLab **`ardenslab/ual`** esiste (privato, id 95, creato con `glab api`) e il remoto `origin` è già collegato in questo repository (`ssh://git@gitlab.cloudnetwork.one:2222/ardenslab/ual.git`). `pm2-dash` risulta `internal` su GitLab: per cambiare visibilità, Settings ▸ General ▸ Visibility.
48
+ 2. Fai il primo commit (il repository è già inizializzato, senza commit) e il push di `master`: `git push -u origin master`. **Poi imposta `master` come ramo predefinito**: GitLab ha creato il progetto con `main` e non permette di cambiarlo finché `master` non esiste (`glab api projects/95 -X PUT -f default_branch=master`, o Settings ▸ Repository ▸ Default branch).
49
+ 3. Crea su GitHub il repository vuoto **`ArdensLab/ual`** (pubblico) e configura in GitLab il *push mirror* (Settings ▸ Repository ▸ Mirroring), solo `master`. Il campo `repository` di `package.json` punta già a `github.com/ArdensLab/ual`: se scegli un altro nome, cambialo prima di pubblicare, perché dopo si corregge solo con una nuova versione.
50
+ 4. **Proteggi i branch di deploy** con la regola jolly **`deploy-*`** (Settings ▸ Repository ▸ Protected branches; push e merge solo Maintainer, niente force push), come in `pm2-dash`: altrimenti le variabili *Protected* arrivano vuote. **Fatto** su `ardenslab/ual` il 2026-11-04. Il branch `deploy-npm` non esiste finché non lo crea il primo push.
51
+
52
+ ### 3. Il token
53
+ Su npmjs.com ▸ Access Tokens ▸ *Granular Access Token*:
54
+
55
+ - **Permessi:** lettura e scrittura. Per un nome **che non esiste ancora** npm non permette di limitare il token a quel pacchetto: per la **prima** pubblicazione serve un token con scrittura su *All packages*. **Subito dopo**, sostituiscilo con un token limitato al solo `universal-ad-loader` (come quello di `pm2-dash`) e aggiorna `NPM_TOKEN`: un token che può scrivere su tutti i pacchetti è più pericoloso se trapela.
56
+ - **"Bypass 2FA" attivo:** la CI non può rispondere a una richiesta del codice. Se la casella non è davvero attiva il job fallisce con `EOTP` (è successo con `pm2-dash`): si rigenera il token con la casella spuntata, senza toccare il codice.
57
+ - Annota la scadenza del token e rinnovalo.
58
+
59
+ In GitLab (Settings ▸ CI/CD ▸ Variables) aggiungi **`NPM_TOKEN`** con **Masked** e **Protected**.
60
+
61
+ ## Ogni rilascio
62
+
63
+ ```bash
64
+ npm version 0.1.1 --no-git-tag-version # la prima volta la versione è già 0.1.0: salta questo passo
65
+ # aggiorna CHANGELOG.md (un blocco "# 0.1.1" in testa: lo richiede release-check)
66
+ git commit -am "Release 0.1.1: <cosa cambia>"
67
+ git push origin master # il mirror GitHub si aggiorna
68
+ git push origin HEAD:deploy-npm # questo lancia la pubblicazione
69
+ ```
70
+
71
+ Quando la pipeline è verde e il pacchetto è su npm, **il tag** (annotato, sul commit di rilascio, come in `pm2-dash`) e la release GitLab:
72
+
73
+ ```bash
74
+ git tag -a v0.1.1 -m "Release 0.1.1"
75
+ git push origin v0.1.1
76
+ glab release create v0.1.1 --notes-file <(sed -n '/^# 0.1.1/,/^# /p' CHANGELOG.md | sed '$d') # la pagina Release, con le note del CHANGELOG
77
+ ```
78
+
79
+ Il push del tag lancia la pipeline di tag (test e `release-check`, nessuna pubblicazione): se il tag non è `v<versione>` fallisce.
80
+
81
+ Poi controlla:
82
+
83
+ ```bash
84
+ npm view universal-ad-loader version # la versione appena pubblicata
85
+ ```
86
+
87
+ e prova `npm i universal-ad-loader` in un progetto vuoto.
88
+
89
+ Sotto la `1.0.0` l'API può ancora cambiare tra una versione minor e l'altra: è il significato di `0.x`. Dopo ogni rilascio ricostruisci `dist/ual.min.js` e caricalo anche sulla CDN (meglio in un percorso con la versione, vedi `docs/cdn.md`). Una versione sbagliata si segnala con `npm deprecate universal-ad-loader@0.1.0 "usa 0.1.1"`.
90
+
91
+ ## Subito dopo la prima pubblicazione
92
+
93
+ - Nel configuratore (`ual-configurator/src/lib/output.ts`) imposta `PACKAGE_PUBLISHED = true`: l'avviso «non ancora pubblicato» sparisce dalla scheda Snippet. Aggiorna le frasi «non ancora pubblicato» in `docs/libreria.md`, nel README del loader e nella guida (`docs.astro`).
94
+
95
+ ## Pubblicare a mano (emergenza)
96
+
97
+ Possibile, ma senza la rete di sicurezza della pipeline:
98
+
99
+ ```bash
100
+ npm login # come utente ardenslab, con 2FA
101
+ npm test # la build la fa prepack, i test NON partono da soli
102
+ npm publish # chiede il codice della 2FA
103
+ ```
104
+
105
+ ## Provenance
106
+
107
+ `npm publish --provenance` allega un'attestazione firmata di quale commit e quale pipeline hanno prodotto il pacchetto, ma funziona solo da una CI con **repository pubblico**. `pm2-dash` non la usa (GitLab privato, GitHub è solo uno specchio): lo stesso vale qui.
108
+
109
+ ## Cosa significa rendere pubblico il pacchetto
110
+
111
+ - Il codice (compresi i controlli di sicurezza del render) è leggibile da chiunque: i difetti vanno corretti prima, non dopo.
112
+ - La licenza MIT permette a chiunque di usarlo e modificarlo, anche commercialmente, e non si può ritirare per le versioni già pubblicate.
113
+ - Gli adapter `adsense`, `revive-async` e `gpt` sono scritti sulle API pubbliche ma non provati su account reali (`gpt` solo con uno stub): il README lo dichiara, e conviene tenerlo visibile.
114
+ - Possono arrivare segnalazioni e richieste: decidi se e come rispondere.
package/package.json CHANGED
@@ -1,6 +1,121 @@
1
1
  {
2
2
  "name": "universal-ad-loader",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Loader pubblicitario universale: un tag per sito, configurazione centralizzata, waterfall tra provider e consenso TCF. Componenti per React, Next.js, Vue e Astro.",
5
+ "author": {
6
+ "name": "ArdensLab",
7
+ "url": "https://www.npmjs.com/~ardenslab"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ArdensLab/ual.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/ArdensLab/ual/issues"
16
+ },
17
+ "homepage": "https://github.com/ArdensLab/ual#readme",
18
+ "keywords": [
19
+ "ads",
20
+ "advertising",
21
+ "ad-loader",
22
+ "adserver",
23
+ "banner",
24
+ "waterfall",
25
+ "tcf",
26
+ "gdpr",
27
+ "consent",
28
+ "react",
29
+ "nextjs",
30
+ "vue",
31
+ "astro"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "main": "./dist/index.cjs",
37
+ "module": "./dist/index.mjs",
38
+ "types": "./dist/index.d.ts",
39
+ "sideEffects": false,
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "import": "./dist/index.mjs",
44
+ "require": "./dist/index.cjs"
45
+ },
46
+ "./react": {
47
+ "types": "./dist/react.d.ts",
48
+ "import": "./dist/react.mjs",
49
+ "require": "./dist/react.cjs"
50
+ },
51
+ "./next": {
52
+ "types": "./dist/next.d.ts",
53
+ "import": "./dist/next.mjs",
54
+ "require": "./dist/next.cjs"
55
+ },
56
+ "./vue": {
57
+ "types": "./dist/vue.d.ts",
58
+ "import": "./dist/vue.mjs",
59
+ "require": "./dist/vue.cjs"
60
+ },
61
+ "./global": {
62
+ "types": "./dist/global.d.ts"
63
+ },
64
+ "./astro/*": "./astro/*",
65
+ "./loader": "./dist/ual.min.js",
66
+ "./schema": "./schema/ual-config.schema.json",
67
+ "./package.json": "./package.json"
68
+ },
69
+ "files": [
70
+ "dist",
71
+ "astro",
72
+ "schema",
73
+ "docs",
74
+ "CHANGELOG.md"
75
+ ],
76
+ "directories": {
77
+ "example": "examples",
78
+ "test": "test"
79
+ },
80
+ "scripts": {
81
+ "build": "node scripts/build.mjs",
82
+ "prepack": "npm run build",
83
+ "lint": "oxlint src",
84
+ "pretest": "npm run build",
85
+ "test": "node test/run.mjs . && node test/regression.mjs . && node test/package.mjs . && node test/wrappers.mjs .",
86
+ "test:node": "npm run build && node test/package.mjs .",
87
+ "test:browser": "npm run build && node test/run.mjs . && node test/regression.mjs .",
88
+ "test:frameworks": "npm run build && node test/wrappers.mjs ."
89
+ },
90
+ "peerDependencies": {
91
+ "astro": ">=4",
92
+ "next": ">=13",
93
+ "react": ">=18",
94
+ "vue": ">=3.3"
95
+ },
96
+ "peerDependenciesMeta": {
97
+ "astro": {
98
+ "optional": true
99
+ },
100
+ "next": {
101
+ "optional": true
102
+ },
103
+ "react": {
104
+ "optional": true
105
+ },
106
+ "vue": {
107
+ "optional": true
108
+ }
109
+ },
110
+ "devDependencies": {
111
+ "@types/react": "^19.3.0",
112
+ "@types/react-dom": "^19.3.0",
113
+ "esbuild": "^0.28.2",
114
+ "oxlint": "^1.88.0",
115
+ "playwright": "^1.64.1",
116
+ "react": "^19.3.1",
117
+ "react-dom": "^19.3.1",
118
+ "typescript": "^7.0.2",
119
+ "vue": "^3.5.44"
120
+ }
121
+ }
@@ -0,0 +1,191 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
3
+ "title": "UAL — configurazione sito",
4
+ "description": "Configurazione JSON letta dal loader UAL (data-config). Fonte di verità per il configuratore web.",
5
+ "type": "object",
6
+ "required": ["site", "providers", "slots"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "site": { "type": "string", "minLength": 1, "description": "ID del sito, inviato agli adapter" },
10
+ "debug": { "type": "boolean" },
11
+ "consent": { "$ref": "#/definitions/consent" },
12
+ "lazy": { "$ref": "#/definitions/lazy" },
13
+ "viewability": { "$ref": "#/definitions/viewability" },
14
+ "collapseEmpty": { "type": "boolean", "description": "Nasconde lo slot se nessun provider lo riempie" },
15
+ "providerTimeoutMs": { "type": "integer", "minimum": 100, "maximum": 30000 },
16
+ "targeting": { "$ref": "#/definitions/targeting" },
17
+ "providers": {
18
+ "type": "object",
19
+ "propertyNames": { "pattern": "^[A-Za-z0-9_-]+$" },
20
+ "additionalProperties": { "$ref": "#/definitions/provider" }
21
+ },
22
+ "slots": {
23
+ "type": "object",
24
+ "propertyNames": { "pattern": "^[A-Za-z0-9_-]+$" },
25
+ "additionalProperties": { "$ref": "#/definitions/slot" }
26
+ },
27
+ "pageRules": { "type": "array", "items": { "$ref": "#/definitions/pageRule" } }
28
+ },
29
+ "definitions": {
30
+ "consent": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "mode": { "enum": ["tcf", "none"] },
35
+ "timeoutMs": { "type": "integer", "minimum": 100, "maximum": 30000 },
36
+ "cmpWaitMs": { "type": "integer", "minimum": 0, "maximum": 10000 },
37
+ "lateWatchMs": { "type": "integer", "minimum": 0, "maximum": 300000 },
38
+ "requiredPurposes": {
39
+ "type": "array", "uniqueItems": true,
40
+ "items": { "type": "integer", "minimum": 1, "maximum": 10 }
41
+ },
42
+ "vendorId": { "type": "integer", "minimum": 1 }
43
+ }
44
+ },
45
+ "lazy": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "properties": {
49
+ "enabled": { "type": "boolean" },
50
+ "fetchMargin": { "type": "string", "pattern": "^\\d+(px|%)$" }
51
+ }
52
+ },
53
+ "viewability": {
54
+ "type": "object",
55
+ "additionalProperties": false,
56
+ "properties": {
57
+ "threshold": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
58
+ "durationMs": { "type": "integer", "minimum": 0, "maximum": 10000 }
59
+ }
60
+ },
61
+ "targeting": {
62
+ "type": "object",
63
+ "additionalProperties": {
64
+ "anyOf": [
65
+ { "type": ["string", "number", "boolean"] },
66
+ { "type": "array", "items": { "type": ["string", "number"] } }
67
+ ]
68
+ }
69
+ },
70
+ "sizePair": {
71
+ "type": "array", "minItems": 2, "maxItems": 2,
72
+ "items": { "type": "integer", "minimum": 1, "maximum": 2000 }
73
+ },
74
+ "sizes": { "type": "array", "items": { "$ref": "#/definitions/sizePair" } },
75
+ "providerRef": {
76
+ "oneOf": [
77
+ { "type": "string", "minLength": 1 },
78
+ {
79
+ "type": "object", "required": ["id"], "additionalProperties": false,
80
+ "properties": {
81
+ "id": { "type": "string", "minLength": 1 },
82
+ "params": { "type": "object" }
83
+ }
84
+ }
85
+ ]
86
+ },
87
+ "creative": {
88
+ "type": "object",
89
+ "required": ["width", "height"],
90
+ "additionalProperties": false,
91
+ "anyOf": [{ "required": ["src"] }, { "required": ["html"] }],
92
+ "properties": {
93
+ "id": { "type": "string" },
94
+ "type": { "enum": ["image", "html"] },
95
+ "src": { "type": "string", "minLength": 1 },
96
+ "html": { "type": "string", "minLength": 1 },
97
+ "alt": { "type": "string" },
98
+ "width": { "type": "integer", "minimum": 1, "maximum": 2000 },
99
+ "height": { "type": "integer", "minimum": 1, "maximum": 2000 },
100
+ "clickUrl": { "type": "string" },
101
+ "weight": { "type": "number", "exclusiveMinimum": 0 },
102
+ "slots": { "type": "array", "items": { "type": "string" } },
103
+ "devices": { "type": "array", "uniqueItems": true, "items": { "enum": ["desktop", "tablet", "mobile"] } },
104
+ "from": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
105
+ "to": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
106
+ }
107
+ },
108
+ "provider": {
109
+ "type": "object",
110
+ "required": ["adapter"],
111
+ "unevaluatedProperties": false,
112
+ "properties": {
113
+ "adapter": { "enum": ["json", "iframe", "revive", "revive-async", "gpt", "adsense", "house"] },
114
+ "consent": { "enum": ["optional", "required", "contextual"] },
115
+ "timeoutMs": { "type": "integer", "minimum": 100, "maximum": 30000 },
116
+ "endpoint": { "type": "string", "minLength": 1 },
117
+ "headers": { "type": "object", "additionalProperties": { "type": "string" } },
118
+ "withCredentials": { "type": "boolean" },
119
+ "map": { "type": "string" },
120
+ "url": { "type": "string", "minLength": 1 },
121
+ "sandbox": { "type": "string" },
122
+ "server": { "type": "string", "minLength": 1 },
123
+ "deliveryPath": { "type": "string" },
124
+ "reviveId": { "type": "string", "minLength": 1 },
125
+ "fillTimeoutMs": { "type": "integer", "minimum": 100, "maximum": 30000 },
126
+ "network": { "type": "string" },
127
+ "adUnitPath": { "type": "string", "minLength": 1 },
128
+ "singleRequest": { "type": "boolean" },
129
+ "nonPersonalizedWithoutConsent": { "type": "boolean" },
130
+ "src": { "type": "string" },
131
+ "client": { "type": "string", "pattern": "^ca-pub-\\d+$" },
132
+ "test": { "type": "boolean" },
133
+ "creatives": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/creative" } }
134
+ },
135
+ "allOf": [
136
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "json" } } }, "then": { "required": ["endpoint"] } },
137
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "iframe" } } }, "then": { "required": ["url"] } },
138
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "revive" } } }, "then": { "required": ["server"] } },
139
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "revive-async" } } }, "then": { "required": ["server", "reviveId"] } },
140
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "gpt" } } }, "then": { "required": ["adUnitPath"] } },
141
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "adsense" } } }, "then": { "required": ["client"] } },
142
+ { "if": { "required": ["adapter"], "properties": { "adapter": { "const": "house" } } }, "then": { "required": ["creatives"] } }
143
+ ]
144
+ },
145
+ "slotFields": {
146
+ "type": "object",
147
+ "properties": {
148
+ "sizes": { "$ref": "#/definitions/sizes" },
149
+ "sizeMapping": {
150
+ "type": "array",
151
+ "items": {
152
+ "type": "object", "required": ["minWidth", "sizes"], "additionalProperties": false,
153
+ "properties": {
154
+ "minWidth": { "type": "integer", "minimum": 0, "maximum": 10000 },
155
+ "sizes": { "$ref": "#/definitions/sizes" }
156
+ }
157
+ }
158
+ },
159
+ "providers": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/providerRef" } },
160
+ "label": { "type": "string" },
161
+ "minHeight": { "type": "integer", "minimum": 0, "maximum": 2000 },
162
+ "lazy": { "type": "boolean" },
163
+ "collapseEmpty": { "type": "boolean" },
164
+ "refreshSec": { "type": "integer", "minimum": 5 },
165
+ "maxRefresh": { "type": "integer", "minimum": 1 },
166
+ "enabled": { "type": "boolean" },
167
+ "viewability": { "$ref": "#/definitions/viewability" }
168
+ }
169
+ },
170
+ "slot": {
171
+ "allOf": [{ "$ref": "#/definitions/slotFields" }, { "required": ["providers"] }],
172
+ "unevaluatedProperties": false
173
+ },
174
+ "slotOverride": {
175
+ "allOf": [{ "$ref": "#/definitions/slotFields" }],
176
+ "unevaluatedProperties": false
177
+ },
178
+ "pageRule": {
179
+ "type": "object",
180
+ "required": ["match"],
181
+ "additionalProperties": false,
182
+ "properties": {
183
+ "match": { "type": "string", "minLength": 1, "description": "Regex sul path della pagina" },
184
+ "disable": { "type": "array", "items": { "type": "string" } },
185
+ "enable": { "type": "array", "items": { "type": "string" } },
186
+ "targeting": { "$ref": "#/definitions/targeting" },
187
+ "slots": { "type": "object", "additionalProperties": { "$ref": "#/definitions/slotOverride" } }
188
+ }
189
+ }
190
+ }
191
+ }