@uniweb/core 0.10.0 → 0.10.2
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.
- package/package.json +4 -3
- package/src/block.js +100 -4
- package/src/icon-corpus.js +80 -0
- package/src/locale-config.js +1 -1
- package/src/page.js +19 -0
- package/src/tracker.js +70 -2
- package/src/uniweb.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./data-paths": "./src/data-paths.js",
|
|
10
10
|
"./detail-url": "./src/detail-url.js",
|
|
11
11
|
"./fetch-config": "./src/fetch-config.js",
|
|
12
|
+
"./icon-corpus": "./src/icon-corpus.js",
|
|
12
13
|
"./locale-config": "./src/locale-config.js",
|
|
13
14
|
"./route-match": "./src/route-match.js",
|
|
14
15
|
"./section-id": "./src/section-id.js",
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
"vitest": "^4.1.7"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"@uniweb/
|
|
43
|
-
"@uniweb/
|
|
43
|
+
"@uniweb/theming": "^0.1.15",
|
|
44
|
+
"@uniweb/semantic-parser": "^1.2.3"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
47
|
"test": "vitest run"
|
package/src/block.js
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* child blocks, and state management. Connects to foundation components.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
parseContent as parseSemanticContent,
|
|
10
|
+
resolveAssetUrl
|
|
11
|
+
} from '@uniweb/semantic-parser'
|
|
9
12
|
import { normalizeTokenValue } from '@uniweb/theming'
|
|
13
|
+
import { sectionDomId } from './section-id.js'
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Lift container fences out of a content document.
|
|
@@ -156,7 +160,7 @@ export default class Block {
|
|
|
156
160
|
if (rawBg && !this.standardOptions.background) {
|
|
157
161
|
this.standardOptions = {
|
|
158
162
|
...this.standardOptions,
|
|
159
|
-
background: Block.normalizeBackground(rawBg)
|
|
163
|
+
background: Block.normalizeBackground(rawBg, this.parseOptions())
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
|
|
@@ -285,6 +289,28 @@ export default class Block {
|
|
|
285
289
|
* block already knows both — a foundation should not have to thread context
|
|
286
290
|
* it was handed. Same arrangement as `useFormSubmit({ block })`.
|
|
287
291
|
*
|
|
292
|
+
* ## ⭐ `section` and `section_id` answer DIFFERENT questions — both ride
|
|
293
|
+
*
|
|
294
|
+
* `section` is the component **type** (`Hero`); `section_id` is this
|
|
295
|
+
* **instance** (`section-hero`), the same string the renderers write as the
|
|
296
|
+
* DOM id, so it joins to the anchor a search result already links to.
|
|
297
|
+
*
|
|
298
|
+
* | | cardinality to a collector | survives a foundation swap | survives a content rename |
|
|
299
|
+
* |---|---|---|---|
|
|
300
|
+
* | `section` (type) | the foundation's vocabulary — bounded, small | ⛔ no | ✅ yes |
|
|
301
|
+
* | `section_id` (instance) | pages × sections — needs scoping to be storable | ✅ yes | ⛔ **no — a rename silently splits the series** |
|
|
302
|
+
*
|
|
303
|
+
* ⛔ **Both are sent, deliberately, and dropping either later is a wire
|
|
304
|
+
* break.** A consumer storing only one is free to ignore the other — the cost
|
|
305
|
+
* of carrying it is one field — whereas **collecting under an identity that is
|
|
306
|
+
* later changed throws the data away rather than merely delaying it.**
|
|
307
|
+
* *(Agreed across the producing and serving sides, 2026-08-17; the cardinality
|
|
308
|
+
* numbers that are the reason are recorded internally.)*
|
|
309
|
+
*
|
|
310
|
+
* ⚠️ Instance identity on the wire is **`(path, section_id)`** — `path` is
|
|
311
|
+
* already here, so no `path#section` composite is ever sent and neither side
|
|
312
|
+
* keeps one in sync.
|
|
313
|
+
*
|
|
288
314
|
* ⛔ **No guard is needed at the call site.** A site with no tracking
|
|
289
315
|
* destination is the default: the call returns having done nothing, opened no
|
|
290
316
|
* connection and thrown nothing. Absent is the normal state, not an error.
|
|
@@ -302,6 +328,7 @@ export default class Block {
|
|
|
302
328
|
globalThis.uniweb?.tracking?.track(event, {
|
|
303
329
|
path: this.path,
|
|
304
330
|
section: this.type,
|
|
331
|
+
section_id: sectionDomId(this),
|
|
305
332
|
...data
|
|
306
333
|
})
|
|
307
334
|
}
|
|
@@ -377,10 +404,43 @@ export default class Block {
|
|
|
377
404
|
* Uses @uniweb/semantic-parser for intelligent content extraction
|
|
378
405
|
* Returns flat content structure
|
|
379
406
|
*/
|
|
407
|
+
/**
|
|
408
|
+
* Options handed to the semantic parser for this block.
|
|
409
|
+
*
|
|
410
|
+
* `assets` carries the host's asset-URL pattern (`config.assets.url`) so a
|
|
411
|
+
* node's `assetId`/`assetExt` resolve to a real URL. It is read from the
|
|
412
|
+
* published payload and passed as an explicit input rather than reached for
|
|
413
|
+
* as module state — the parser must never know a host, and a pattern is the
|
|
414
|
+
* only thing that tells it one.
|
|
415
|
+
*
|
|
416
|
+
* ⚠️ This depends on `website.config` being populated before a Block is
|
|
417
|
+
* constructed, and it is — but only because `Page.bodyBlocks` is a LAZY
|
|
418
|
+
* getter, so blocks are built at render, long after the Website constructor
|
|
419
|
+
* returns. The constructor itself assigns `this.pages` (line ~149) BEFORE
|
|
420
|
+
* `this.config` (line ~159), so an eager Block would parse against an empty
|
|
421
|
+
* config and every asset would silently fall through to `src` — working
|
|
422
|
+
* output, no error, no resolution.
|
|
423
|
+
*
|
|
424
|
+
* ⚠️ **That getter now has a SECOND consumer, in another lane.** The editor
|
|
425
|
+
* relies on it re-running: `updateParams` → `website.rebuild` → `_applyContent`
|
|
426
|
+
* → `page.bodyBlocks` constructs fresh Blocks, which is what re-normalises a
|
|
427
|
+
* section background on every live edit (traced by the frontend lane,
|
|
428
|
+
* 2026-08-17, before deleting their own duplicate normaliser). So making block
|
|
429
|
+
* construction eager breaks background editing as well as asset resolution —
|
|
430
|
+
* two failures, two lanes, one cause, and neither visible from the other side.
|
|
431
|
+
*
|
|
432
|
+
* Do not make block construction eager
|
|
433
|
+
* without moving the config assignment first.
|
|
434
|
+
*/
|
|
435
|
+
parseOptions() {
|
|
436
|
+
const assets = this.website?.config?.assets
|
|
437
|
+
return assets ? { assets } : {}
|
|
438
|
+
}
|
|
439
|
+
|
|
380
440
|
extractFromProseMirror(doc) {
|
|
381
441
|
try {
|
|
382
442
|
// Parse with semantic-parser - returns flat structure
|
|
383
|
-
const parsed = parseSemanticContent(doc)
|
|
443
|
+
const parsed = parseSemanticContent(doc, this.parseOptions())
|
|
384
444
|
|
|
385
445
|
// Parsed content is now flat: { title, pretitle, paragraphs, links, items, sequence, ... }
|
|
386
446
|
return parsed
|
|
@@ -678,9 +738,45 @@ export default class Block {
|
|
|
678
738
|
* - Object without mode: mode inferred from which fields are present
|
|
679
739
|
*
|
|
680
740
|
* @param {string|Object} raw - Raw background value from frontmatter
|
|
741
|
+
* @param {Object} [options] - Parse options; `options.assets.url` is the host's
|
|
742
|
+
* asset-URL pattern, used to resolve a store-held background.
|
|
681
743
|
* @returns {Object} Normalized background config with mode
|
|
682
744
|
*/
|
|
683
|
-
static normalizeBackground(raw) {
|
|
745
|
+
static normalizeBackground(raw, options) {
|
|
746
|
+
return Block.resolveBackgroundMedia(Block.normalizeBackgroundShape(raw), options)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Resolve a store-held background asset (`assetId` + `assetExt`) to a `src`.
|
|
751
|
+
*
|
|
752
|
+
* ⭐ This runs HERE, at normalize time, and deliberately not at render. A
|
|
753
|
+
* background is drawn by two twinned implementations — `Background.jsx` (SPA)
|
|
754
|
+
* and `ssr-renderer.js` (SSG + edge) — which both read `background.image?.src`.
|
|
755
|
+
* Resolving at render would mean the identical change in both, and the twins
|
|
756
|
+
* drifting is this repo's standing hazard: the lane you tested keeps working
|
|
757
|
+
* while the other is wrong in production. One resolution here, and both lanes
|
|
758
|
+
* get it for free.
|
|
759
|
+
*
|
|
760
|
+
* Same precedence as the node path: a store-held asset wins WHEN IT RESOLVES,
|
|
761
|
+
* so a producer may write `assetId` beside a `src` and the `src` carries the
|
|
762
|
+
* render until a host declares a pattern.
|
|
763
|
+
*/
|
|
764
|
+
static resolveBackgroundMedia(bg, options) {
|
|
765
|
+
const pattern = options?.assets?.url
|
|
766
|
+
if (!pattern || !bg || typeof bg !== 'object') return bg
|
|
767
|
+
|
|
768
|
+
let out = bg
|
|
769
|
+
for (const key of ['image', 'video']) {
|
|
770
|
+
const media = bg[key]
|
|
771
|
+
if (!media || typeof media !== 'object') continue
|
|
772
|
+
const url = resolveAssetUrl(media.assetId, media.assetExt, pattern)
|
|
773
|
+
if (url) out = { ...out, [key]: { ...media, src: url } }
|
|
774
|
+
}
|
|
775
|
+
return out
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Shape normalization only — no resolution. See `normalizeBackground`. */
|
|
779
|
+
static normalizeBackgroundShape(raw) {
|
|
684
780
|
// String shorthand — classify by content
|
|
685
781
|
if (typeof raw === 'string') {
|
|
686
782
|
// URL or path → image/video
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icon corpus — its default origin and its filename rule.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is one module and not five constants
|
|
5
|
+
*
|
|
6
|
+
* An icon referenced by `library` + `name` is **our own asset**, not the site's.
|
|
7
|
+
* We publish the families, we document them, and `@uniweb/icons`'
|
|
8
|
+
* `scripts/build-cdn.js` writes the files. So unlike a site asset — whose URL
|
|
9
|
+
* pattern the HOST declares because the bytes are in the host's store — the
|
|
10
|
+
* layout here is ours to name, and a default origin is the correct answer
|
|
11
|
+
* rather than a guessed one.
|
|
12
|
+
*
|
|
13
|
+
* That makes this a **writer/reader pair**, which is the part that needs a
|
|
14
|
+
* single definition:
|
|
15
|
+
*
|
|
16
|
+
* writer @uniweb/icons scripts/build-cdn.js emits cdn/{family}/{family}-{name}.svg
|
|
17
|
+
* readers @uniweb/runtime setup.js browser resolution
|
|
18
|
+
* @uniweb/runtime ssr-renderer.js prerender + Worker isolate prefetch
|
|
19
|
+
* @uniweb/icons src/resolver.js local-then-CDN resolution
|
|
20
|
+
*
|
|
21
|
+
* Before 2026-08-17 the origin was spelled out in three of those and the
|
|
22
|
+
* filename rule in all four. A writer and its readers drifting is the exact
|
|
23
|
+
* defect `@uniweb/core/route-match` exists to prevent, and the one the runtime
|
|
24
|
+
* channel's bridge-filename helper prevents by construction. Same treatment
|
|
25
|
+
* here: one helper, no second spelling.
|
|
26
|
+
*
|
|
27
|
+
* ## ⛔ Keep this a LEAF — zero imports
|
|
28
|
+
*
|
|
29
|
+
* `ssr-renderer.js` is bundled into the SSR isolate that runs in a Cloudflare
|
|
30
|
+
* Worker, so anything it reaches must import nothing: no `node:*`, no DOM, no
|
|
31
|
+
* `@uniweb/core` root (which pulls semantic-parser and theming). That is the
|
|
32
|
+
* same constraint `route-match` and `locale-config` carry, and the reason this
|
|
33
|
+
* lives in core rather than in `@uniweb/icons` — a Worker cannot take a package
|
|
34
|
+
* whose value is ~3,200 icon modules behind a dynamic import, and `@uniweb/runtime`
|
|
35
|
+
* depends on core already.
|
|
36
|
+
*
|
|
37
|
+
* A host may override the ORIGIN — a mirror of this corpus is a legitimate
|
|
38
|
+
* deployment choice, and on a hosted site the base comes from the payload the
|
|
39
|
+
* host serves. It may not override the LAYOUT: a mirror mirrors. Re-deriving
|
|
40
|
+
* filenames instead of copying them is what produced two incompatible spellings
|
|
41
|
+
* of the same corpus once already.
|
|
42
|
+
*
|
|
43
|
+
* @module @uniweb/core/icon-corpus
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where the framework publishes its own icon corpus.
|
|
48
|
+
*
|
|
49
|
+
* Not a fallback for a missing host address — it is the address of OUR artifact,
|
|
50
|
+
* and it is what makes `` work in a project with no backend at all.
|
|
51
|
+
* A host that mirrors the corpus supplies its own origin on the payload.
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_ICON_BASE = 'https://uniweb.github.io/icons'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The corpus path for one icon, relative to any origin serving it.
|
|
57
|
+
*
|
|
58
|
+
* `{family}/{family}-{name}.svg` — the family repeats deliberately: the
|
|
59
|
+
* directory groups, and the filename prefix keeps ids unique across families so
|
|
60
|
+
* a name alone is never ambiguous.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} family - short family code (`lu`, `hi2`, `fa6`)
|
|
63
|
+
* @param {string} name - icon id within that family (`house`, `a-arrow-down`)
|
|
64
|
+
* @returns {string} e.g. `lu/lu-house.svg`
|
|
65
|
+
*/
|
|
66
|
+
export function iconPath(family, name) {
|
|
67
|
+
return `${family}/${family}-${name}.svg`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The full URL for one icon against a serving origin.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} family - short family code
|
|
74
|
+
* @param {string} name - icon id within that family
|
|
75
|
+
* @param {string} [base] - serving origin; defaults to the framework's own
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function iconUrl(family, name, base = DEFAULT_ICON_BASE) {
|
|
79
|
+
return `${String(base).replace(/\/+$/, '')}/${iconPath(family, name)}`
|
|
80
|
+
}
|
package/src/locale-config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared locale-config helpers — the ONE home for the language rules that
|
|
3
3
|
* build, sync, runtime, and the CLI all apply to a site's config.
|
|
4
4
|
*
|
|
5
|
-
* Contract (
|
|
5
|
+
* Contract ("Per-locale publish readiness"):
|
|
6
6
|
* - `languages` (site.yml) / `info.languages` (wire) — the DECLARED working
|
|
7
7
|
* set. A plain, strongly-validated string list.
|
|
8
8
|
* - `publishLanguages` (site.yml) / `info.publish_languages` (wire) — publish
|
package/src/page.js
CHANGED
|
@@ -48,6 +48,25 @@ export default class Page {
|
|
|
48
48
|
// Rewrite target (if set, this route is served by an external site)
|
|
49
49
|
this.rewrite = pageData.rewrite || null
|
|
50
50
|
|
|
51
|
+
// This page's answer on section-level instrumentation (`trackSections` in
|
|
52
|
+
// page.yml) — an OVERRIDE, not a switch.
|
|
53
|
+
//
|
|
54
|
+
// ⭐ **Three states, and the third is the point.** `undefined` means the page
|
|
55
|
+
// said nothing and the site's own `tracking.emit` decides; `true` and
|
|
56
|
+
// `false` override it in either direction, so an owner can instrument one
|
|
57
|
+
// page of a quiet site or exempt a noisy page of a loud one.
|
|
58
|
+
//
|
|
59
|
+
// ⛔ **`|| false` was wrong here and collapsed the tri-state**, which made
|
|
60
|
+
// page-level the ONLY spelling and forced every owner to tick pages
|
|
61
|
+
// individually. The wire already carried all three — `content-collector`
|
|
62
|
+
// emits the key only when it is `!= null` — so nothing but this line had to
|
|
63
|
+
// change. ⇒ Read `?? undefined`, never a boolean coercion.
|
|
64
|
+
//
|
|
65
|
+
// The runtime passes it to `Tracker.arms('section_view', …)`, which is where
|
|
66
|
+
// the precedence against the host's own list is resolved. It means nothing
|
|
67
|
+
// without a tracking destination.
|
|
68
|
+
this.trackSections = pageData.trackSections ?? undefined
|
|
69
|
+
|
|
51
70
|
// Two orthogonal visibility axes:
|
|
52
71
|
// • `hidden` — REACHABILITY. When true the page is excluded from the published
|
|
53
72
|
// site entirely (the build prunes it and its subtree); it survives only in
|
package/src/tracker.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ⭐ **A page visit is a trackable event.** That sentence is the design. There is
|
|
5
5
|
* one destination, one envelope, and one queue; the runtime emits `page_view`
|
|
6
6
|
* automatically and a foundation emits whatever else it likes, through the same
|
|
7
|
-
* path.
|
|
7
|
+
* path.
|
|
8
8
|
*
|
|
9
9
|
* ```
|
|
10
10
|
* { event: 'page_view', path: '/about', referrer?, utm_* }
|
|
@@ -71,6 +71,30 @@ const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefine
|
|
|
71
71
|
* loudly.
|
|
72
72
|
*
|
|
73
73
|
* A cross-origin parent throws on `window.top` access; that also means framed.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ **The boundary condition: this checks `framed`, not `preview`.** A preview
|
|
76
|
+
* rendered in a popup or a top-level tab would be same-origin and NOT framed,
|
|
77
|
+
* `isLiveDocument()` would return true, and — like every failure in this area —
|
|
78
|
+
* it would have no symptom: the payload stays internally consistent while the
|
|
79
|
+
* owner's own edits quietly inflate their numbers.
|
|
80
|
+
*
|
|
81
|
+
* ✅ **Covered on the other side too, by a test rather than a promise**
|
|
82
|
+
* **[frontend, measured; relayed 2026-08-17]**: `preview-runtime` imports the
|
|
83
|
+
* real `setup` / `provider` / `foundation-loader` (not a fork), all five of its
|
|
84
|
+
* mounts are `<iframe>`, and the popup case is already instantiated —
|
|
85
|
+
* `DetachedPreview.jsx` is a popup *root* with the runtime in an iframe inside
|
|
86
|
+
* it, so the guard holds. A non-framed preview is not reachable by re-parenting
|
|
87
|
+
* at all: content and foundation arrive *only* over the frame-bridge, so going
|
|
88
|
+
* non-framed means replacing the transport. Their
|
|
89
|
+
* `previewFraming.smoke.test.js` pins both the iframe property and the exact set
|
|
90
|
+
* of mounting files, so a new mount fails there.
|
|
91
|
+
*
|
|
92
|
+
* ⛔ **Do not "fix" this by adding an explicit preview flag from the harness.**
|
|
93
|
+
* A suppression that depends on another lane remembering something has no
|
|
94
|
+
* symptom when forgotten, which is the whole reason this one reads the DOM
|
|
95
|
+
* instead; two suppressions where one is authoritative is how you get a stale
|
|
96
|
+
* one. The two guards now fail independently, in different repos, for the same
|
|
97
|
+
* violation.
|
|
74
98
|
*/
|
|
75
99
|
function detectFramed() {
|
|
76
100
|
if (!isBrowser) return false
|
|
@@ -183,6 +207,17 @@ export default class Tracker {
|
|
|
183
207
|
// reports three times.
|
|
184
208
|
this.currentPath = null
|
|
185
209
|
|
|
210
|
+
// What the runtime may ARM, as two independent narrowings. Both are `null`
|
|
211
|
+
// when nothing narrows, which is the common case and the cheap one.
|
|
212
|
+
//
|
|
213
|
+
// ⛔ **Neither ever filters `track()`.** The registry is open by design, and
|
|
214
|
+
// a client-side allowlist over a foundation's own events would export one
|
|
215
|
+
// host's policy onto every host — including hosts that sent no list, whose
|
|
216
|
+
// sites would silently drop everything their foundation emits. These gate
|
|
217
|
+
// the runtime's OWN emissions and nothing else. See `arms()`.
|
|
218
|
+
this.hostEvents = options.hostEvents ? new Set(options.hostEvents) : null
|
|
219
|
+
this.siteEmit = options.siteEmit ? new Set(options.siteEmit) : null
|
|
220
|
+
|
|
186
221
|
this.framed = detectFramed()
|
|
187
222
|
|
|
188
223
|
// Called once when consent moves to granted, and never otherwise. The
|
|
@@ -235,6 +270,39 @@ export default class Tracker {
|
|
|
235
270
|
return !!this.endpoint && this.isLiveDocument()
|
|
236
271
|
}
|
|
237
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Whether the runtime should ARM one of its own automatic emitters.
|
|
275
|
+
*
|
|
276
|
+
* ⭐ **Three questions, in the order that makes each one's absence safe:**
|
|
277
|
+
*
|
|
278
|
+
* 1. **Is there anywhere to send, in a live document?** — `isEnabled()`.
|
|
279
|
+
* 2. **Will the host consume this?** `hostEvents` is the host's cost switch:
|
|
280
|
+
* no point arming an observer for a row nobody stores. ⛔ **Absent means NO
|
|
281
|
+
* NARROWING, never an empty set** — a host that sends no list is an older
|
|
282
|
+
* or simpler one, and reading absence as "consume nothing" would take
|
|
283
|
+
* every site on that host dark with every gate reading yes.
|
|
284
|
+
* 3. **Did the site ask for it?** `siteEmit` is the operator's own selection.
|
|
285
|
+
* Absent means everything.
|
|
286
|
+
*
|
|
287
|
+
* `override` is the per-page answer where one exists (`page.trackSections`).
|
|
288
|
+
* It replaces the SITE's answer and cannot escape the host's: a page may
|
|
289
|
+
* widen what its own site configured, and may not conjure a row the host
|
|
290
|
+
* declined to store.
|
|
291
|
+
*
|
|
292
|
+
* ⛔ **Not consulted by `track()`.** A foundation's events are never gated —
|
|
293
|
+
* see the constructor.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} event
|
|
296
|
+
* @param {boolean} [override] - the per-page decision, when the caller has one
|
|
297
|
+
* @returns {boolean}
|
|
298
|
+
*/
|
|
299
|
+
arms(event, override) {
|
|
300
|
+
if (!this.isEnabled()) return false
|
|
301
|
+
if (this.hostEvents && !this.hostEvents.has(event)) return false
|
|
302
|
+
if (override != null) return !!override
|
|
303
|
+
return !this.siteEmit || this.siteEmit.has(event)
|
|
304
|
+
}
|
|
305
|
+
|
|
238
306
|
/** @returns {'granted'|'denied'|'pending'} */
|
|
239
307
|
consentStatus() {
|
|
240
308
|
return this.consent
|
|
@@ -309,7 +377,7 @@ export default class Tracker {
|
|
|
309
377
|
* @param {string} path
|
|
310
378
|
*/
|
|
311
379
|
trackPageView(path) {
|
|
312
|
-
if (!this.
|
|
380
|
+
if (!this.arms('page_view') || !path) return
|
|
313
381
|
if (path === this.currentPath) return
|
|
314
382
|
this.currentPath = path
|
|
315
383
|
// Promptly, rather than waiting out the batch window: a page view is the
|
package/src/uniweb.js
CHANGED
|
@@ -72,7 +72,7 @@ export default class Uniweb {
|
|
|
72
72
|
// Populated by prerender before rendering, read synchronously by Icon.
|
|
73
73
|
this.iconCache = new Map()
|
|
74
74
|
|
|
75
|
-
// Site tracking — one event stream
|
|
75
|
+
// Site tracking — one event stream.
|
|
76
76
|
//
|
|
77
77
|
// ⛔ Deliberately constructed DISABLED, and the runtime replaces it in L2
|
|
78
78
|
// (`wire-foundation.js` → `wireTracker`). It cannot be configured here even
|