universal-ad-loader 0.0.0-stage → 0.1.1

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 as a library: components for React, Next.js, Vue and Astro
2
+
3
+ Besides the `<script>` tag (CDN), UAL can be used as a package: server-safe import, framework components, TypeScript types. Same configuration and same behavior as the tag.
4
+
5
+ > **Status:** the package is **published on npm**: `npm i universal-ad-loader` (version 0.1.0, MIT).
6
+
7
+ ## When to use what
8
+
9
+ | | `<script>` tag from the CDN | Package (components) |
10
+ |---|---|---|
11
+ | Sites without a build step (HTML, WordPress) | ✅ | — |
12
+ | React / Next / Vue / Astro sites | works, with caveats (below) | ✅ recommended |
13
+ | Hydration and page changes | up to you | handled by the components |
14
+ | Config types and autocompletion | — | ✅ `defineConfig` |
15
+ | Updating loader/config without a redeploy | ✅ | ✅ with `src` (loader) and `config` as a URL |
16
+
17
+ The components support both models: the **loader** comes from the CDN (`src="…/ual.min.js"`) or is included in the site bundle; the **configuration** is a URL (changes without a redeploy) or an object in the code.
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: 'my-site',
27
+ providers: { main: { adapter: 'json', endpoint: 'https://ads.yourdomain.net/v1/decide' } },
28
+ slots: { top: { sizes: [[728, 90]], providers: ['main'] } },
29
+ });
30
+
31
+ export function App() {
32
+ const { pathname } = useLocation(); // react-router, or your own router
33
+ return (
34
+ <UalProvider config={config} pageKey={pathname}>
35
+ <AdSlot name="top" minHeight={90} />
36
+ </UalProvider>
37
+ );
38
+ }
39
+ ```
40
+
41
+ - `config`: object or JSON URL (read only at startup).
42
+ - `src`: URL of the loader on the CDN; without it, UAL is included in the site bundle.
43
+ - `pageKey`: when it changes (path), UAL recomputes the per-page rules and slots.
44
+ - `targeting`: key/value pairs, also dynamic ones.
45
+ - `onEvent(event, data)`: all events (`ready`, `consent`, `slotRendered`, `slotEmpty`, `error`…), including the initial ones.
46
+ - `useUal()`: `{ ual, ready }` (`ual` is `null` on the server and before startup).
47
+ - `AdSlot`: `name`, `params`, `minHeight`, `className`, `style`, `id`. It is an empty `div`: UAL fills it.
48
+
49
+ ## Next.js
50
+
51
+ Same components from `universal-ad-loader/next`: page changes are automatic (`usePathname`). Works with the App Router and when imported from a Server Component (the files have `'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="en"><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 and 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
+ Same props (kebab-case) and same `useUal()` (with refs). UAL starts in `onMounted`: browser only and after hydration. Vue server rendering is verified by the tests; **Nuxt has not been tried** (the provider goes in `app.vue`, where the layout lives).
91
+
92
+ ## Astro
93
+
94
+ Slots in `.astro` pages are static HTML: no hydration.
95
+
96
+ ```astro
97
+ ---
98
+ import UalBundled from 'universal-ad-loader/astro/UalBundled.astro'; // UAL in the site bundle
99
+ // or: import UalLoader from 'universal-ad-loader/astro/UalLoader.astro'; // loader from the 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`: the configuration lives in an `application/json` block (not executed: compatible with a strict CSP) and UAL is included in the bundle.
108
+ - `UalLoader` (`src`, `config`): loads `ual.min.js` from the CDN, with the config as a URL or an object.
109
+ - Both, with View Transitions (`ClientRouter`), recompute the per-page rules on every navigation.
110
+
111
+ ## Core and Node
112
+
113
+ ```js
114
+ import { createUal, getUal, defineConfig, version } from 'universal-ad-loader';
115
+ ```
116
+
117
+ - **No side effects on import:** it can be imported in Node, in a Server Component, in a config file. The DOM is only touched when `init()` is called in the browser.
118
+ - `getUal()`: the page's shared instance. On the server it throws a clear error: call it after mounting.
119
+ - `createUal()`: a new instance (useful in tests).
120
+ - `defineConfig(config)`: identity function with types. **It flags a nonexistent provider in a slot's waterfall**.
121
+
122
+ Server rendering produces only the slot `div`s (with `min-height` if specified: no layout shift). Ads arrive in the browser.
123
+
124
+ ## What the components handle (and the tag does not)
125
+
126
+ - **Hydration.** UAL starts in an effect, after hydration. Loading the loader with a tag in the HTML before hydration makes React report *Hydration failed* (even with `suppressHydrationWarning`) and makes the slot be requested **twice**: the repository test reproduces it (`test/wrappers.mjs`, "Senza componenti").
127
+ - **Page change.** `pageKey` / `usePathname` / `astro:page-load` call `pageView()`: per-page rules are recomputed and the requests are the right ones, with no duplicates.
128
+ - **StrictMode and double mounting.** One instance and one `init` per page.
129
+ - **Events and targeting from the start.** They are attached before `init`: `onEvent` also receives `ready` and `consent`, and the targeting applies to the first request.
130
+
131
+ ## What stays the same
132
+
133
+ - **Strict CSP.** Creatives run in an iframe that inherits the page's CSP: without `'unsafe-inline'` for scripts and styles the banner shows up but `UALExt` and the tracked click do not work. See the README.
134
+ - **One mode per page.** Do not use the tag and the components together on the same page (the second `init` is ignored with a warning).
135
+ - **Versions.** With `src` (CDN) the loader may be older or newer than the package: the components use only the common API and do not require `isInitialized`/`whenReady` (if missing, they call `init`).
136
+
137
+ ## How it is verified
138
+
139
+ `npm test` runs, besides the loader tests:
140
+
141
+ - `test/package.mjs`: import in Node (ESM and CJS) without `window`, clear `getUal()` error on the server, server rendering of React and Vue, exports map, package contents (`npm pack`), types (correct usages compile, wrong ones fail).
142
+ - `test/wrappers.mjs`: in a real browser, with server rendering and delayed hydration (the worst case): React (UAL in the bundle, loader from CDN, Next version), Vue, **Next.js 16** (App Router, production build, Server Component, `<Link>`) and **Astro 7** (View Transitions). For each: no hydration errors, clean console, one request per slot, per-page rules recomputed on navigation with no duplicate requests.
package/package.json CHANGED
@@ -1,6 +1,128 @@
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.1",
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
+ "browser": "./dist/ual.min.js",
40
+ "jsdelivr": "./dist/ual.min.js",
41
+ "unpkg": "./dist/ual.min.js",
42
+ "sideEffects": false,
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "exports": {
47
+ ".": {
48
+ "types": "./dist/index.d.ts",
49
+ "import": "./dist/index.mjs",
50
+ "require": "./dist/index.cjs"
51
+ },
52
+ "./react": {
53
+ "types": "./dist/react.d.ts",
54
+ "import": "./dist/react.mjs",
55
+ "require": "./dist/react.cjs"
56
+ },
57
+ "./next": {
58
+ "types": "./dist/next.d.ts",
59
+ "import": "./dist/next.mjs",
60
+ "require": "./dist/next.cjs"
61
+ },
62
+ "./vue": {
63
+ "types": "./dist/vue.d.ts",
64
+ "import": "./dist/vue.mjs",
65
+ "require": "./dist/vue.cjs"
66
+ },
67
+ "./global": {
68
+ "types": "./dist/global.d.ts"
69
+ },
70
+ "./astro/*": "./astro/*",
71
+ "./loader": "./dist/ual.min.js",
72
+ "./schema": "./schema/ual-config.schema.json",
73
+ "./package.json": "./package.json"
74
+ },
75
+ "files": [
76
+ "dist",
77
+ "astro",
78
+ "schema",
79
+ "docs",
80
+ "!docs/pubblicazione-npm.md",
81
+ "CHANGELOG.md"
82
+ ],
83
+ "directories": {
84
+ "example": "examples",
85
+ "test": "test"
86
+ },
87
+ "scripts": {
88
+ "build": "node scripts/build.mjs",
89
+ "prepack": "npm run build",
90
+ "lint": "oxlint src",
91
+ "pretest": "npm run build",
92
+ "test": "node test/run.mjs . && node test/regression.mjs . && node test/package.mjs . && node test/wrappers.mjs .",
93
+ "test:node": "npm run build && node test/package.mjs .",
94
+ "test:browser": "npm run build && node test/run.mjs . && node test/regression.mjs .",
95
+ "test:frameworks": "npm run build && node test/wrappers.mjs ."
96
+ },
97
+ "peerDependencies": {
98
+ "astro": ">=4",
99
+ "next": ">=13",
100
+ "react": ">=18",
101
+ "vue": ">=3.3"
102
+ },
103
+ "peerDependenciesMeta": {
104
+ "astro": {
105
+ "optional": true
106
+ },
107
+ "next": {
108
+ "optional": true
109
+ },
110
+ "react": {
111
+ "optional": true
112
+ },
113
+ "vue": {
114
+ "optional": true
115
+ }
116
+ },
117
+ "devDependencies": {
118
+ "@types/react": "^19.3.0",
119
+ "@types/react-dom": "^19.3.0",
120
+ "esbuild": "^0.28.2",
121
+ "oxlint": "^1.88.0",
122
+ "playwright": "^1.64.1",
123
+ "react": "^19.3.1",
124
+ "react-dom": "^19.3.1",
125
+ "typescript": "^7.0.2",
126
+ "vue": "^3.5.44"
127
+ }
128
+ }
@@ -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
+ }