@gradial/aci 0.1.52-rc.8 → 0.1.52
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/CHANGELOG.md +126 -0
- package/README.md +86 -5
- package/dist/astro/error-page.js +1 -0
- package/dist/astro/index.js +4 -0
- package/dist/astro/page.d.ts +5 -2
- package/dist/astro/page.js +35 -23
- package/dist/astro/preview.js +2 -17
- package/dist/audience-data-models.d.ts +308 -197
- package/dist/audience-data-models.js +313 -51
- package/dist/cli/compile-registry.mjs +6 -34
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +6 -7
- package/dist/collect/api.js +21 -8
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +9 -7
- package/dist/collect/batcher.js +6 -5
- package/dist/collect/consent/envelope.d.ts +7 -1
- package/dist/collect/consent/envelope.js +3 -1
- package/dist/collect/consent/mapping.d.ts +2 -0
- package/dist/collect/consent/mapping.js +1 -0
- package/dist/collect/consenthold.d.ts +38 -0
- package/dist/collect/consenthold.js +82 -0
- package/dist/collect/declarative.js +7 -5
- package/dist/collect/gapbuffer.d.ts +13 -4
- package/dist/collect/gapbuffer.js +21 -10
- package/dist/collect/index.js +78 -21
- package/dist/collect/pageviews.d.ts +11 -11
- package/dist/collect/pageviews.js +27 -22
- package/dist/consent-banner/index.d.ts +17 -0
- package/dist/consent-banner/index.js +23 -0
- package/dist/content/index.d.ts +2 -1
- package/dist/content/index.js +4 -1
- package/dist/content/provider.d.ts +16 -2
- package/dist/content/provider.js +5 -1
- package/dist/content/resolve-slots.d.ts +24 -2
- package/dist/content/resolve-slots.js +127 -21
- package/dist/content/types.d.ts +5 -1
- package/dist/edge/expression-capabilities.v1.d.ts +106 -0
- package/dist/edge/expression-capabilities.v1.js +165 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/internal/domain/edge/identity.d.ts +47 -4
- package/dist/internal/domain/edge/identity.js +130 -26
- package/dist/internal/domain/edge/index.d.ts +1 -1
- package/dist/internal/domain/edge/index.js +1 -1
- package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
- package/dist/internal/domain/edge/mimic_token.js +125 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
- package/dist/internal/domain/edge/personalization-core.js +790 -200
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +55 -0
- package/dist/internal/domain/edge/resolve.js +76 -39
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
- package/dist/internal/domain/edge/types.d.ts +82 -78
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/internal/domain/edge/variant_key.d.ts +4 -0
- package/dist/internal/domain/edge/variant_key.js +56 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
- package/dist/internal/platform/collectionstore/errors.js +48 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +201 -0
- package/dist/mintlify/config.d.ts +3 -0
- package/dist/mintlify/config.js +77 -0
- package/dist/mintlify/index.d.ts +3 -0
- package/dist/mintlify/index.js +2 -0
- package/dist/mintlify/json.d.ts +8 -0
- package/dist/mintlify/json.js +37 -0
- package/dist/mintlify/materialize.d.ts +14 -0
- package/dist/mintlify/materialize.js +254 -0
- package/dist/mintlify/types.d.ts +74 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +256 -0
- package/dist/next/assignments-navigation.d.ts +4 -0
- package/dist/next/assignments-navigation.js +18 -0
- package/dist/next/assignments-script.d.ts +3 -2
- package/dist/next/assignments-script.js +5 -5
- package/dist/next/error-pages.d.ts +4 -0
- package/dist/next/error-pages.js +4 -0
- package/dist/next/error-seat-payload.d.ts +2 -1
- package/dist/next/error-seat-payload.js +14 -24
- package/dist/next/error-seat.js +7 -2
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/page.d.ts +2 -1
- package/dist/next/page.js +15 -4
- package/dist/next/release-context.d.ts +30 -0
- package/dist/next/release-context.js +56 -0
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +15 -5
- package/dist/next/server.d.ts +9 -3
- package/dist/next/server.js +17 -32
- package/dist/preview/core.d.ts +1 -0
- package/dist/preview/core.js +27 -0
- package/dist/react/VideoPlayer.d.ts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/registry.d.ts +4 -2
- package/dist/testing/conformance/checks.d.ts +2 -2
- package/dist/testing/conformance/checks.js +2 -0
- package/dist/testing/conformance/index.d.ts +1 -0
- package/dist/testing/conformance/index.js +1 -0
- package/dist/testing/conformance/next-stubs.d.ts +6 -0
- package/dist/testing/conformance/next-stubs.js +9 -0
- package/dist/testing/conformance/preset.d.ts +3 -2
- package/dist/testing/conformance/preset.js +15 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +361 -0
- package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-image.js +5 -0
- package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-link.js +4 -0
- package/dist/testing/conformance/types.d.ts +5 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -3
- package/dist/types/block-ref.js +2 -2
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/page.d.ts +0 -1
- package/migrations/conformance-registry.md +22 -0
- package/migrations/experiment-goal-ids.md +43 -0
- package/package.json +21 -7
- package/src/cli/compile-registry.mjs +6 -30
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/types/block-ref.test.ts +16 -1
- package/src/types/block-ref.ts +2 -2
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +0 -1
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -1
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
- package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
- package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
- package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
- package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
- package/templates/astro/template/.aci.yaml.tmpl +0 -1
- package/templates/astro/template/package.json.tmpl +5 -5
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- package/templates/common/.agents/skills/aci/SKILL.md +58 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +194 -42
- package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
- package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +216 -0
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
- package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/fragments/navbar/_index.json +24 -0
- package/templates/common/.content/fragments/site-footer/_index.json +61 -0
- package/templates/common/.content/pages/404/_index.json +4 -2
- package/templates/common/.content/pages/_index.json +0 -1
- package/templates/common/.content/pages/error/_index.json +4 -2
- package/templates/common/.content/schemas/site.schema.json +11 -1
- package/templates/common/.content/templates/error.json +24 -0
- package/templates/common/.content/templates/home.json +28 -0
- package/templates/common/.content/templates/site.json +8 -0
- package/templates/common/.content/templates/standard.json +24 -0
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -2
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +14 -3
- package/templates/common/starter-content/.content/pages/about/_index.json +2 -2
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -2
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
- package/templates/nextjs/template/.aci.yaml +0 -1
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
- package/templates/nextjs/template/package.json.tmpl +4 -3
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/layout.tsx +12 -7
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
- package/dist/define-layout.d.ts +0 -6
- package/dist/define-layout.js +0 -14
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/edge/personalization-core.js +0 -269
- package/dist/internal/domain/edge/compose.d.ts +0 -24
- package/dist/internal/domain/edge/compose.js +0 -89
- package/dist/internal/domain/edge/record/codec.d.ts +0 -71
- package/dist/internal/domain/edge/record/codec.js +0 -401
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/edge-config.d.ts +0 -1
- package/dist/next/edge-config.js +0 -92
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- package/dist/next/personalization-host.d.ts +0 -134
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
- package/dist/next/vercel-edge-projection-v1.js +0 -880
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- package/dist/types/layout.d.ts +0 -24
- package/src/types/layout.ts +0 -29
- package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
- package/templates/common/starter-content/.content/patterns/layouts/default.json +0 -14
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
- /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Declarative events (design/04): elements opt in via data-aci-event, with
|
|
2
2
|
// data-aci-on=click|visible (visible = first intersection >= 50%, once per
|
|
3
|
-
// element per page view) and data-aci-prop-* string properties.
|
|
3
|
+
// element per page view) and data-aci-prop-* string properties. Every event
|
|
4
|
+
// carries the current page url so ingest can derive path and UTM. One
|
|
4
5
|
// delegated click listener + one IntersectionObserver regardless of element
|
|
5
6
|
// count. Malformed declarations are ignored silently.
|
|
6
7
|
const EVENT_ATTR = "data-aci-event";
|
|
@@ -35,13 +36,13 @@ function extractProps(el) {
|
|
|
35
36
|
}
|
|
36
37
|
return props;
|
|
37
38
|
}
|
|
38
|
-
function declaredEvent(el, debug) {
|
|
39
|
+
function declaredEvent(el, url, debug) {
|
|
39
40
|
const eventName = toCustomEventName(el.getAttribute(EVENT_ATTR));
|
|
40
41
|
if (eventName === undefined) {
|
|
41
42
|
debug?.("capture", { reason: "malformed_declaration", attr: EVENT_ATTR });
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
|
-
return { eventName, properties: extractProps(el) };
|
|
45
|
+
return { eventName, url, properties: extractProps(el) };
|
|
45
46
|
}
|
|
46
47
|
function trigger(el) {
|
|
47
48
|
return el.getAttribute(ON_ATTR)?.trim().toLowerCase() ?? "click";
|
|
@@ -53,6 +54,7 @@ function trigger(el) {
|
|
|
53
54
|
*/
|
|
54
55
|
export function startDeclarative(emit, env, refresh) {
|
|
55
56
|
const { document: doc, debug } = env;
|
|
57
|
+
const loc = doc.defaultView?.location;
|
|
56
58
|
doc.addEventListener("click", (e) => {
|
|
57
59
|
try {
|
|
58
60
|
const target = e.target;
|
|
@@ -66,7 +68,7 @@ export function startDeclarative(emit, env, refresh) {
|
|
|
66
68
|
return;
|
|
67
69
|
if (trigger(el) !== "click")
|
|
68
70
|
return; // visible or malformed trigger
|
|
69
|
-
const event = declaredEvent(el, debug);
|
|
71
|
+
const event = declaredEvent(el, loc?.href, debug);
|
|
70
72
|
if (event)
|
|
71
73
|
emit(event);
|
|
72
74
|
}
|
|
@@ -92,7 +94,7 @@ export function startDeclarative(emit, env, refresh) {
|
|
|
92
94
|
continue;
|
|
93
95
|
fired.add(entry.target);
|
|
94
96
|
obs.unobserve(entry.target); // once per element per page view
|
|
95
|
-
const event = declaredEvent(entry.target, debug);
|
|
97
|
+
const event = declaredEvent(entry.target, loc?.href, debug);
|
|
96
98
|
if (event)
|
|
97
99
|
emit(event);
|
|
98
100
|
}
|
|
@@ -39,12 +39,21 @@ export interface RouteDeps {
|
|
|
39
39
|
marker: () => boolean;
|
|
40
40
|
gapBuffer: GapBuffer;
|
|
41
41
|
batcher: EventSink;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a CMP-preset site's consent decision is still pending and no
|
|
44
|
+
* identity exists (consenthold.ts). Absent means never pending.
|
|
45
|
+
*/
|
|
46
|
+
consentPending?: () => boolean;
|
|
47
|
+
/** In-memory hold for consent-pending events (consenthold.ts). */
|
|
48
|
+
consentHold?: EventSink;
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
|
-
* routeEvent implements the emit routing (design/05): events captured
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
51
|
+
* routeEvent implements the emit routing (design/05): events captured while
|
|
52
|
+
* a CMP's consent decision is pending (and no identity exists) are held in
|
|
53
|
+
* page memory until the decision; events captured in the consent gap —
|
|
54
|
+
* envelope grants personalization AND the marker is absent — are buffered;
|
|
55
|
+
* everything else goes straight to the batcher (the collector enforces
|
|
56
|
+
* consent server-side).
|
|
48
57
|
*/
|
|
49
58
|
export declare function routeEvent(event: CollectEvent, deps: RouteDeps): void;
|
|
50
59
|
/**
|
|
@@ -4,20 +4,23 @@
|
|
|
4
4
|
// response. Bounded at 50 (drop-oldest), persisted per-tab in
|
|
5
5
|
// sessionStorage["aci:buf"], purged on withdrawal, flushed through the
|
|
6
6
|
// normal batcher when the marker appears (checked at init and on every
|
|
7
|
-
// flush tick). Flushed events keep their eventIds.
|
|
7
|
+
// flush tick). Flushed events keep their eventIds. Storage is authoritative:
|
|
8
|
+
// a BFCache-retained document must observe another document's drain/purge.
|
|
8
9
|
export const GAP_BUFFER_KEY = "aci:buf";
|
|
9
10
|
export const GAP_BUFFER_CAP = 50;
|
|
10
11
|
const MINT_MARKER_COOKIE = "aci_m";
|
|
11
12
|
export class GapBuffer {
|
|
12
|
-
events
|
|
13
|
+
// Only retain events in memory when storage is unavailable. A successful
|
|
14
|
+
// storage operation must never leave a second, stale owner of that queue.
|
|
15
|
+
events = [];
|
|
13
16
|
storage;
|
|
14
17
|
debug;
|
|
15
18
|
constructor(env) {
|
|
16
19
|
this.debug = env?.debug;
|
|
17
20
|
this.storage = env?.storage;
|
|
18
|
-
this.events = this.restore();
|
|
19
21
|
}
|
|
20
22
|
push(event) {
|
|
23
|
+
this.events = this.restore();
|
|
21
24
|
this.events.push(event);
|
|
22
25
|
if (this.events.length > GAP_BUFFER_CAP) {
|
|
23
26
|
this.events.splice(0, this.events.length - GAP_BUFFER_CAP);
|
|
@@ -27,13 +30,14 @@ export class GapBuffer {
|
|
|
27
30
|
}
|
|
28
31
|
/** drain returns and clears all buffered events, preserving order and ids. */
|
|
29
32
|
drain() {
|
|
30
|
-
const drained = this.
|
|
33
|
+
const drained = this.restore();
|
|
31
34
|
this.events = [];
|
|
32
35
|
this.persist();
|
|
33
36
|
return drained;
|
|
34
37
|
}
|
|
35
38
|
/** purge empties the buffer (consent withdrawal). */
|
|
36
39
|
purge() {
|
|
40
|
+
this.events = this.restore();
|
|
37
41
|
if (this.events.length > 0) {
|
|
38
42
|
this.debug?.("buffer", { reason: "purged", count: this.events.length });
|
|
39
43
|
}
|
|
@@ -41,12 +45,12 @@ export class GapBuffer {
|
|
|
41
45
|
this.persist();
|
|
42
46
|
}
|
|
43
47
|
size() {
|
|
44
|
-
return this.
|
|
48
|
+
return this.restore().length;
|
|
45
49
|
}
|
|
46
50
|
restore() {
|
|
47
51
|
const storage = this.storage;
|
|
48
52
|
if (!storage)
|
|
49
|
-
return
|
|
53
|
+
return this.events;
|
|
50
54
|
let raw;
|
|
51
55
|
try {
|
|
52
56
|
raw = storage.getItem(GAP_BUFFER_KEY);
|
|
@@ -86,6 +90,7 @@ export class GapBuffer {
|
|
|
86
90
|
else {
|
|
87
91
|
storage.setItem(GAP_BUFFER_KEY, JSON.stringify(this.events));
|
|
88
92
|
}
|
|
93
|
+
this.events = [];
|
|
89
94
|
}
|
|
90
95
|
catch (err) {
|
|
91
96
|
this.storage = undefined;
|
|
@@ -104,12 +109,18 @@ export function hasMintMarker(doc) {
|
|
|
104
109
|
return false;
|
|
105
110
|
}
|
|
106
111
|
/**
|
|
107
|
-
* routeEvent implements the emit routing (design/05): events captured
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
112
|
+
* routeEvent implements the emit routing (design/05): events captured while
|
|
113
|
+
* a CMP's consent decision is pending (and no identity exists) are held in
|
|
114
|
+
* page memory until the decision; events captured in the consent gap —
|
|
115
|
+
* envelope grants personalization AND the marker is absent — are buffered;
|
|
116
|
+
* everything else goes straight to the batcher (the collector enforces
|
|
117
|
+
* consent server-side).
|
|
111
118
|
*/
|
|
112
119
|
export function routeEvent(event, deps) {
|
|
120
|
+
if (deps.consentHold && deps.consentPending?.()) {
|
|
121
|
+
deps.consentHold.push(event);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
113
124
|
if (deps.grants() && !deps.marker()) {
|
|
114
125
|
deps.gapBuffer.push(event);
|
|
115
126
|
return;
|
package/dist/collect/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// init(); every failure is swallowed after a debug record so the page is
|
|
4
4
|
// never affected (QR-2/3).
|
|
5
5
|
import { installCommandQueue } from "./api.js";
|
|
6
|
-
import { createAssignments, withAssignmentContext } from "./assignments.js";
|
|
6
|
+
import { createAssignments, readAssignments, withAssignmentContext } from "./assignments.js";
|
|
7
7
|
import { Batcher } from "./batcher.js";
|
|
8
|
+
import { CONSENT_BANNER_DECIDED_EVENT, consentBannerPending, } from "../consent-banner/index.js";
|
|
8
9
|
import { createDebug, readTagConfig } from "./config.js";
|
|
10
|
+
import { ConsentHold, resolveConsentHold } from "./consenthold.js";
|
|
9
11
|
import { start as startC15t } from "./consent/adapters/c15t.js";
|
|
10
12
|
import { start as startCookiebot } from "./consent/adapters/cookiebot.js";
|
|
11
13
|
import { start as startOneTrust } from "./consent/adapters/onetrust.js";
|
|
@@ -53,6 +55,9 @@ export function init(win) {
|
|
|
53
55
|
w.__aci_loaded = true;
|
|
54
56
|
if (!hasBaseline(w))
|
|
55
57
|
return;
|
|
58
|
+
// Snapshot before exposures or queued commands. Cookie expiry, hydration
|
|
59
|
+
// and BFCache never turn this synthetic document into a genuine visit.
|
|
60
|
+
const measurementDisabled = w.__aci_measurement_disabled === true;
|
|
56
61
|
const cfg = readTagConfig(w.document.currentScript);
|
|
57
62
|
const debug = createDebug(cfg.debug);
|
|
58
63
|
try {
|
|
@@ -74,6 +79,33 @@ export function init(win) {
|
|
|
74
79
|
return false;
|
|
75
80
|
}
|
|
76
81
|
};
|
|
82
|
+
const decided = () => {
|
|
83
|
+
try {
|
|
84
|
+
return readConsentCookie(doc) !== undefined;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// Consent-pending hold (personalization-onboarding REQ-15): a decision
|
|
91
|
+
// is pending when a consent integration has declared it will write the
|
|
92
|
+
// envelope and has not yet. A CMP preset is that declaration; under the
|
|
93
|
+
// manual preset it is ACI's own consent banner, through its state API.
|
|
94
|
+
// Until the decision, and while no identity exists, the collector could
|
|
95
|
+
// only drop what we send, so events wait in page memory instead. A
|
|
96
|
+
// manual site with no banner keeps immediate sending.
|
|
97
|
+
const integrationPending = cfg.preset === "manual"
|
|
98
|
+
? () => {
|
|
99
|
+
try {
|
|
100
|
+
return consentBannerPending(doc);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
: () => true;
|
|
107
|
+
const consentHold = new ConsentHold({ debug });
|
|
108
|
+
const consentPending = () => !decided() && !marker() && integrationPending();
|
|
77
109
|
const transportEnv = {
|
|
78
110
|
navigator: w.navigator,
|
|
79
111
|
fetch: typeof w.fetch === "function"
|
|
@@ -85,9 +117,16 @@ export function init(win) {
|
|
|
85
117
|
batcher = new Batcher({
|
|
86
118
|
send: (body) => send(body, transportEnv),
|
|
87
119
|
debug,
|
|
88
|
-
//
|
|
89
|
-
// tab or
|
|
90
|
-
|
|
120
|
+
// Every flush tick re-checks the consent decision (so a decision
|
|
121
|
+
// written by another tab or site code releases or discards the hold)
|
|
122
|
+
// and then the marker (so a mint by another tab or navigation is
|
|
123
|
+
// noticed). Held events are older than gap-buffered ones: release first.
|
|
124
|
+
onTick: () => {
|
|
125
|
+
if (measurementDisabled)
|
|
126
|
+
return;
|
|
127
|
+
resolveConsentHold(consentHold, { decided, grants, batcher, debug });
|
|
128
|
+
maybeFlushGapBuffer(gapBuffer, batcher, marker, debug);
|
|
129
|
+
},
|
|
91
130
|
timers: {
|
|
92
131
|
setInterval: (fn, ms) => w.setInterval(fn, ms),
|
|
93
132
|
clearInterval: (id) => w.clearInterval(id),
|
|
@@ -95,26 +134,44 @@ export function init(win) {
|
|
|
95
134
|
documentTarget: doc,
|
|
96
135
|
windowTarget: w,
|
|
97
136
|
});
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// one — exposures and explicit conversions carry their own. The handle
|
|
137
|
+
// Every emitted event carries all active assignments (REQ-3) unless it
|
|
138
|
+
// already supplies its explicit context — exposures and explicit conversions carry their own. The handle
|
|
101
139
|
// is created (without emitting) BEFORE emit so every dependency of emit
|
|
102
140
|
// exists before emit can run; exposures flow at start() below.
|
|
103
141
|
const assignments = createAssignments({ document: doc, window: w, debug });
|
|
104
142
|
const emit = (event) => {
|
|
143
|
+
if (measurementDisabled)
|
|
144
|
+
return;
|
|
105
145
|
routeEvent({
|
|
106
146
|
...withAssignmentContext(event, assignments),
|
|
107
147
|
eventId: ulid(undefined, (bytes) => w.crypto.getRandomValues(bytes)),
|
|
108
|
-
}, { grants, marker, gapBuffer, batcher });
|
|
148
|
+
}, { grants, marker, gapBuffer, batcher, consentHold, consentPending });
|
|
109
149
|
};
|
|
150
|
+
// A decision written through the SDK (or announced by the ACI banner)
|
|
151
|
+
// flushes at once: the tick releases (grant) or discards (anything else)
|
|
152
|
+
// the hold, and a released first view leaves before the visitor can
|
|
153
|
+
// navigate away.
|
|
154
|
+
const onDecision = () => batcher.flush();
|
|
155
|
+
doc.addEventListener(CONSENT_BANNER_DECIDED_EVENT, onDecision);
|
|
110
156
|
// Started BEFORE the command-queue drain and page views: pre-load
|
|
111
157
|
// aci('track', ...) calls and the initial rum.page_view must be stamped
|
|
112
158
|
// with the active assignment context, or queued conversions can never
|
|
113
159
|
// match a goal (review-quorum finding). Consent commands drained after
|
|
114
160
|
// this stay safe — events wait in the gap buffer until grants resolve.
|
|
115
161
|
assignments.start(emit);
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
let emitPageView = () => { };
|
|
163
|
+
const navigation = installNavigationHook(w, [
|
|
164
|
+
contexts => assignments.rearm(contexts),
|
|
165
|
+
() => emitPageView(),
|
|
166
|
+
], () => assignments.clear());
|
|
167
|
+
// Astro dispatches this nonbubbling event on document after the router commits.
|
|
168
|
+
doc.addEventListener("astro:page-load", () => navigation.complete({
|
|
169
|
+
url: w.location.href,
|
|
170
|
+
assignments: readAssignments(doc, debug),
|
|
171
|
+
}));
|
|
172
|
+
installCommandQueue(w, {
|
|
173
|
+
navigationComplete: completion => navigation.complete(completion),
|
|
174
|
+
track: (name, properties) => emit({ eventName: name, properties, url: w.location.href }),
|
|
118
175
|
consent: ({ p, src }) => {
|
|
119
176
|
const purposes = sanitizePurposes(p);
|
|
120
177
|
if (purposes === undefined) {
|
|
@@ -126,25 +183,19 @@ export function init(win) {
|
|
|
126
183
|
debug("consent", { reason: "malformed_source" });
|
|
127
184
|
return;
|
|
128
185
|
}
|
|
129
|
-
if (!applyConsent(purposes, source, { document: doc, gapBuffer })) {
|
|
186
|
+
if (!applyConsent(purposes, source, { document: doc, gapBuffer, onDecision })) {
|
|
130
187
|
debug("consent", { reason: "oversized_envelope" });
|
|
131
188
|
}
|
|
132
189
|
},
|
|
133
190
|
}, debug);
|
|
134
|
-
|
|
135
|
-
// The single history patch (one pushState/replaceState wrap, one dedupe
|
|
136
|
-
// state, one popstate listener). Order is load-bearing: the assignments
|
|
137
|
-
// re-arm runs BEFORE the SPA page view so rum.page_view is stamped with
|
|
138
|
-
// the new page's experiment context.
|
|
139
|
-
installNavigationHook(w, [
|
|
140
|
-
() => assignments.rearm(),
|
|
141
|
-
emitPageView,
|
|
142
|
-
]);
|
|
191
|
+
emitPageView = startPageViews(emit, w);
|
|
143
192
|
startDeclarative(emit, {
|
|
144
193
|
document: doc,
|
|
145
194
|
intersectionObserver: w.IntersectionObserver,
|
|
146
195
|
debug,
|
|
147
196
|
}, () => {
|
|
197
|
+
if (measurementDisabled)
|
|
198
|
+
return;
|
|
148
199
|
void refreshClaims(transportEnv).then((refreshed) => {
|
|
149
200
|
if (refreshed)
|
|
150
201
|
w.location.reload();
|
|
@@ -169,6 +220,7 @@ export function init(win) {
|
|
|
169
220
|
document: doc,
|
|
170
221
|
categories: cfg.categories,
|
|
171
222
|
gapBuffer,
|
|
223
|
+
onDecision,
|
|
172
224
|
debug,
|
|
173
225
|
scheduler: {
|
|
174
226
|
setTimeout: (fn, ms) => w.setTimeout(fn, ms),
|
|
@@ -177,7 +229,12 @@ export function init(win) {
|
|
|
177
229
|
});
|
|
178
230
|
}
|
|
179
231
|
batcher.start();
|
|
180
|
-
|
|
232
|
+
// Registered after batcher.start(), so the batcher's own pagehide flush
|
|
233
|
+
// has already released a hold whose decision exists; whatever remains is
|
|
234
|
+
// undecided and never leaves the page.
|
|
235
|
+
w.addEventListener("pagehide", () => consentHold.discard("unload"));
|
|
236
|
+
if (!measurementDisabled)
|
|
237
|
+
maybeFlushGapBuffer(gapBuffer, batcher, marker, debug);
|
|
181
238
|
debug("init", { config: cfg });
|
|
182
239
|
}
|
|
183
240
|
catch (err) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExperimentAssignment } from "./assignments.js";
|
|
1
2
|
import type { PendingEvent } from "./batcher.js";
|
|
2
3
|
export declare const PAGE_VIEW_EVENT = "rum.page_view";
|
|
3
4
|
interface HistoryLike {
|
|
@@ -14,17 +15,16 @@ export interface PageViewWindow {
|
|
|
14
15
|
history: HistoryLike;
|
|
15
16
|
addEventListener(type: string, fn: () => void): void;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export declare function installNavigationHook(win: PageViewWindow, subscribers: ReadonlyArray<() => void>): void;
|
|
18
|
+
export interface NavigationCompletion {
|
|
19
|
+
/** Absolute URL of the page the framework just committed. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Complete committed assignment set, including [] for a base page. */
|
|
22
|
+
assignments: ExperimentAssignment[];
|
|
23
|
+
}
|
|
24
|
+
/** History signals invalidate context; only framework completion arms a view. */
|
|
25
|
+
export declare function installNavigationHook(win: PageViewWindow, subscribers: ReadonlyArray<(assignments: ExperimentAssignment[]) => void>, onStart?: () => void): {
|
|
26
|
+
complete(completion: NavigationCompletion): void;
|
|
27
|
+
};
|
|
28
28
|
/**
|
|
29
29
|
* startPageViews emits the initial page view and returns the emitter for
|
|
30
30
|
* SPA route-change views — init subscribes it to the single navigation hook
|
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
// Page views (design/04): eventName "rum.page_view" with url (the full href
|
|
2
2
|
// including query) and referrer — the SDK never populates utm, ingest
|
|
3
3
|
// derives it from the URL. One event per document load plus one per
|
|
4
|
-
//
|
|
4
|
+
// completed framework route change; hash-only changes and same-URL repeats never
|
|
5
5
|
// double count.
|
|
6
6
|
export const PAGE_VIEW_EVENT = "rum.page_view";
|
|
7
7
|
function stripHash(href) {
|
|
8
8
|
const i = href.indexOf("#");
|
|
9
9
|
return i === -1 ? href : href.slice(0, i);
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* and page views so "page view" means exactly the same thing to both
|
|
17
|
-
* (REQ-2). Subscriber order is the contract: init lists the assignments
|
|
18
|
-
* re-arm BEFORE the page-view emitter so rum.page_view is stamped with the
|
|
19
|
-
* new page's experiment context.
|
|
20
|
-
*/
|
|
21
|
-
export function installNavigationHook(win, subscribers) {
|
|
22
|
-
let last = stripHash(win.location.href);
|
|
11
|
+
/** History signals invalidate context; only framework completion arms a view. */
|
|
12
|
+
export function installNavigationHook(win, subscribers, onStart = () => { }) {
|
|
13
|
+
let observed = stripHash(win.location.href);
|
|
14
|
+
let completed = observed;
|
|
15
|
+
let pending = false;
|
|
23
16
|
const handler = () => {
|
|
24
17
|
const current = stripHash(win.location.href);
|
|
25
|
-
if (current ===
|
|
26
|
-
return;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
if (current === observed)
|
|
19
|
+
return;
|
|
20
|
+
observed = current;
|
|
21
|
+
pending = true;
|
|
22
|
+
onStart();
|
|
30
23
|
};
|
|
31
24
|
const history = win.history;
|
|
32
|
-
const
|
|
25
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
33
26
|
const original = history[method].bind(history);
|
|
34
27
|
history[method] = (...args) => {
|
|
35
28
|
const result = original(...args);
|
|
36
29
|
handler();
|
|
37
30
|
return result;
|
|
38
31
|
};
|
|
39
|
-
}
|
|
40
|
-
wrap("pushState");
|
|
41
|
-
wrap("replaceState");
|
|
32
|
+
}
|
|
42
33
|
win.addEventListener("popstate", handler);
|
|
34
|
+
return {
|
|
35
|
+
complete({ url, assignments }) {
|
|
36
|
+
const current = stripHash(win.location.href);
|
|
37
|
+
if (stripHash(url) !== current)
|
|
38
|
+
return; // late commit for another route
|
|
39
|
+
handler();
|
|
40
|
+
if (!pending && completed === current)
|
|
41
|
+
return;
|
|
42
|
+
pending = false;
|
|
43
|
+
completed = current;
|
|
44
|
+
for (const onNavigate of subscribers)
|
|
45
|
+
onNavigate(assignments);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
43
48
|
}
|
|
44
49
|
/**
|
|
45
50
|
* startPageViews emits the initial page view and returns the emitter for
|
|
@@ -16,6 +16,10 @@ export interface ConsentBannerOptions {
|
|
|
16
16
|
/** Callback fired after a consent decision is written. */
|
|
17
17
|
onDecision?: (granted: boolean) => void;
|
|
18
18
|
}
|
|
19
|
+
/** Rendered with the page by ConsentBanner.astro, before any script runs. */
|
|
20
|
+
export declare const CONSENT_BANNER_MOUNT_ATTR = "data-aci-consent-banner-mount";
|
|
21
|
+
/** Dispatched on the document right after the banner writes a decision. */
|
|
22
|
+
export declare const CONSENT_BANNER_DECIDED_EVENT = "aci:consent-banner-decided";
|
|
19
23
|
/**
|
|
20
24
|
* hasConsentCookie returns true when the aci_consent cookie exists (the user
|
|
21
25
|
* has already made a decision, regardless of whether they granted or denied).
|
|
@@ -23,6 +27,19 @@ export interface ConsentBannerOptions {
|
|
|
23
27
|
export declare function hasConsentCookie(doc: {
|
|
24
28
|
cookie: string;
|
|
25
29
|
}): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* consentBannerPending is the banner's state API for the collect SDK
|
|
32
|
+
* (personalization-onboarding REQ-15): true while this page's ACI consent
|
|
33
|
+
* banner awaits the visitor's decision, meaning the banner is declared (its
|
|
34
|
+
* mount point, which ConsentBanner.astro renders with the HTML) or mounted,
|
|
35
|
+
* and no aci_consent decision exists. Because the mount point ships in the
|
|
36
|
+
* HTML, the answer is already right for the first deferred script to run,
|
|
37
|
+
* whether the collect SDK or the banner's own script loads first.
|
|
38
|
+
*/
|
|
39
|
+
export declare function consentBannerPending(doc: {
|
|
40
|
+
cookie: string;
|
|
41
|
+
querySelector(selectors: string): unknown;
|
|
42
|
+
}): boolean;
|
|
26
43
|
/**
|
|
27
44
|
* mountConsentBanner checks for an existing consent cookie. If none exists,
|
|
28
45
|
* it injects a banner into the document body and wires up accept/reject
|
|
@@ -18,6 +18,10 @@ const DEFAULTS = {
|
|
|
18
18
|
const BANNER_ID = "aci-consent-banner";
|
|
19
19
|
const BANNER_ATTR = "data-aci-consent-banner";
|
|
20
20
|
const BANNER_HIDDEN = "aci-consent-banner--hidden";
|
|
21
|
+
/** Rendered with the page by ConsentBanner.astro, before any script runs. */
|
|
22
|
+
export const CONSENT_BANNER_MOUNT_ATTR = "data-aci-consent-banner-mount";
|
|
23
|
+
/** Dispatched on the document right after the banner writes a decision. */
|
|
24
|
+
export const CONSENT_BANNER_DECIDED_EVENT = "aci:consent-banner-decided";
|
|
21
25
|
/**
|
|
22
26
|
* hasConsentCookie returns true when the aci_consent cookie exists (the user
|
|
23
27
|
* has already made a decision, regardless of whether they granted or denied).
|
|
@@ -25,6 +29,20 @@ const BANNER_HIDDEN = "aci-consent-banner--hidden";
|
|
|
25
29
|
export function hasConsentCookie(doc) {
|
|
26
30
|
return readConsentCookie(doc) !== undefined;
|
|
27
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* consentBannerPending is the banner's state API for the collect SDK
|
|
34
|
+
* (personalization-onboarding REQ-15): true while this page's ACI consent
|
|
35
|
+
* banner awaits the visitor's decision, meaning the banner is declared (its
|
|
36
|
+
* mount point, which ConsentBanner.astro renders with the HTML) or mounted,
|
|
37
|
+
* and no aci_consent decision exists. Because the mount point ships in the
|
|
38
|
+
* HTML, the answer is already right for the first deferred script to run,
|
|
39
|
+
* whether the collect SDK or the banner's own script loads first.
|
|
40
|
+
*/
|
|
41
|
+
export function consentBannerPending(doc) {
|
|
42
|
+
if (hasConsentCookie(doc))
|
|
43
|
+
return false;
|
|
44
|
+
return doc.querySelector(`[${CONSENT_BANNER_MOUNT_ATTR}], [${BANNER_ATTR}]`) !== null;
|
|
45
|
+
}
|
|
28
46
|
/**
|
|
29
47
|
* mountConsentBanner checks for an existing consent cookie. If none exists,
|
|
30
48
|
* it injects a banner into the document body and wires up accept/reject
|
|
@@ -47,6 +65,11 @@ export function mountConsentBanner(doc, opts = {}) {
|
|
|
47
65
|
const purposes = granted ? [PERSONALIZATION_PURPOSE] : [];
|
|
48
66
|
if (!applyConsent(purposes, config.source, { document: doc }))
|
|
49
67
|
return;
|
|
68
|
+
// Tell a collect SDK on this page to act on the decision now rather than
|
|
69
|
+
// at its next flush tick (it releases or discards what it held).
|
|
70
|
+
const EventCtor = doc.defaultView?.Event;
|
|
71
|
+
if (EventCtor)
|
|
72
|
+
doc.dispatchEvent(new EventCtor(CONSENT_BANNER_DECIDED_EVENT));
|
|
50
73
|
banner.classList.add(BANNER_HIDDEN);
|
|
51
74
|
setTimeout(cleanup, 300);
|
|
52
75
|
opts.onDecision?.(granted);
|
package/dist/content/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './routes.js';
|
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './personalization.js';
|
|
5
5
|
export * from './cache.js';
|
|
6
|
-
export
|
|
6
|
+
export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
|
|
7
|
+
export type { FragmentBlock, FragmentData, FragmentLoader } from './resolve-slots.js';
|
|
7
8
|
export * from './default-error-page.js';
|
|
8
9
|
export * from './error-pages.js';
|
package/dist/content/index.js
CHANGED
|
@@ -3,6 +3,9 @@ export * from './routes.js';
|
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './personalization.js';
|
|
5
5
|
export * from './cache.js';
|
|
6
|
-
export
|
|
6
|
+
// Explicit list, not `export *`: fragmentRootScope is runtime-only provenance
|
|
7
|
+
// shared with mintlify/blocks.ts via a direct resolve-slots import and must
|
|
8
|
+
// never leak into the public '@gradial/aci/content' surface.
|
|
9
|
+
export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
|
|
7
10
|
export * from './default-error-page.js';
|
|
8
11
|
export * from './error-pages.js';
|
|
@@ -2,6 +2,7 @@ import type { KernelPage, KernelRouteMetadata, KernelSiteConfig, RenderInput } f
|
|
|
2
2
|
export interface RouteEntry {
|
|
3
3
|
slug: string;
|
|
4
4
|
path: string;
|
|
5
|
+
variantKey: string;
|
|
5
6
|
payloadRef: string;
|
|
6
7
|
digest: string;
|
|
7
8
|
fragmentRefs?: string[];
|
|
@@ -19,8 +20,21 @@ export interface FragmentEntry {
|
|
|
19
20
|
digest: string;
|
|
20
21
|
fragmentRefs?: string[];
|
|
21
22
|
}
|
|
23
|
+
export interface LayoutVariantEntry {
|
|
24
|
+
payloadRef: string;
|
|
25
|
+
digest: string;
|
|
26
|
+
slots: LayoutSlot[];
|
|
27
|
+
defaults?: Record<string, FragmentRef[]>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derived template render plan in the compiled release wire format.
|
|
31
|
+
* This is never an authorable layout: page templates own regions and defaults.
|
|
32
|
+
* The LayoutEntry name and layouts wire key remain for retained release readers.
|
|
33
|
+
*/
|
|
22
34
|
export interface LayoutEntry {
|
|
23
35
|
name: string;
|
|
36
|
+
digest?: string;
|
|
37
|
+
variants?: Record<string, LayoutVariantEntry>;
|
|
24
38
|
payloadRef: string;
|
|
25
39
|
slots: LayoutSlot[];
|
|
26
40
|
defaults?: Record<string, FragmentRef[]>;
|
|
@@ -32,7 +46,6 @@ export interface LayoutSlot {
|
|
|
32
46
|
export interface FragmentRef {
|
|
33
47
|
kind: string;
|
|
34
48
|
fragmentId: string;
|
|
35
|
-
inline: boolean;
|
|
36
49
|
}
|
|
37
50
|
export interface CompiledManifest {
|
|
38
51
|
manifestVersion: string;
|
|
@@ -59,8 +72,9 @@ export declare class LayoutNotFoundError extends Error {
|
|
|
59
72
|
constructor(name: string, cause?: unknown);
|
|
60
73
|
}
|
|
61
74
|
export interface RenderInputOptions {
|
|
75
|
+
variantKey: string;
|
|
62
76
|
domain?: string;
|
|
63
77
|
locale?: string;
|
|
64
78
|
}
|
|
65
|
-
export declare function loadRenderInput(provider: ContentProvider, route
|
|
79
|
+
export declare function loadRenderInput(provider: ContentProvider, route: string | undefined, options: RenderInputOptions): Promise<RenderInput>;
|
|
66
80
|
export declare function routeMetadataForContent(siteConfig: KernelSiteConfig, page: KernelPage | null): KernelRouteMetadata;
|
package/dist/content/provider.js
CHANGED
|
@@ -24,8 +24,11 @@ export class LayoutNotFoundError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
import { getCachedSiteConfig } from './cache.js';
|
|
27
|
-
export async function loadRenderInput(provider, route = '/', options
|
|
27
|
+
export async function loadRenderInput(provider, route = '/', options) {
|
|
28
28
|
const normalized = normalizeRoute(route);
|
|
29
|
+
if (typeof options?.variantKey !== 'string' || !options.variantKey.trim() || options.variantKey !== options.variantKey.trim()) {
|
|
30
|
+
throw new Error(`ACI render variant identity is missing or invalid for route ${normalized}; supply an explicit variantKey`);
|
|
31
|
+
}
|
|
29
32
|
const [siteConfig, page] = await Promise.all([
|
|
30
33
|
getCachedSiteConfig(provider),
|
|
31
34
|
provider.getPage(normalized).catch((error) => {
|
|
@@ -36,6 +39,7 @@ export async function loadRenderInput(provider, route = '/', options = {}) {
|
|
|
36
39
|
]);
|
|
37
40
|
return {
|
|
38
41
|
route: normalized,
|
|
42
|
+
variantKey: options.variantKey,
|
|
39
43
|
domain: options.domain || siteConfig.domain,
|
|
40
44
|
locale: (options.locale || siteConfig.defaultLocale || 'en-us').toLowerCase(),
|
|
41
45
|
siteConfig,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LayoutEntry } from './provider.js';
|
|
2
|
-
import type { KernelPageBlock } from './types.js';
|
|
2
|
+
import type { KernelPageBlock, KernelPageNode } from './types.js';
|
|
3
3
|
export interface FragmentBlock {
|
|
4
4
|
$type: 'component';
|
|
5
5
|
component: string;
|
|
@@ -13,4 +13,26 @@ export interface FragmentData {
|
|
|
13
13
|
props?: Record<string, unknown>;
|
|
14
14
|
}
|
|
15
15
|
export type FragmentLoader = (id: string) => Promise<FragmentData>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Shared Go/TS limit contract (sdk I-21). These values are pinned against
|
|
18
|
+
* internal/domain/content/compiler/testdata/limit-conformance.v1.json by
|
|
19
|
+
* limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
|
|
20
|
+
* compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
|
|
21
|
+
*/
|
|
22
|
+
/** Bounds a selected fragment reference chain, counting its root fragment. */
|
|
23
|
+
export declare const MAX_FRAGMENT_DEPTH = 10;
|
|
24
|
+
/** Bounds rendered component occurrences per page, including repeated placements. */
|
|
25
|
+
export declare const MAX_EXPANDED_BLOCKS = 4096;
|
|
26
|
+
/** Failures in a selected fragment are never eligible for local-content fallback. */
|
|
27
|
+
export declare class FragmentResolutionError extends Error {
|
|
28
|
+
constructor(message: string, options?: ErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
export declare const fragmentRootScope: unique symbol;
|
|
31
|
+
export declare function preserveFragmentScope<T extends object>(source: object, target: T): T;
|
|
32
|
+
export declare function resolveSlots(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
|
|
33
|
+
/** The config-time error seat uses the identical traversal with synchronous file reads. */
|
|
34
|
+
export declare function resolveSlotsSync(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: (id: string) => FragmentData): Record<string, KernelPageBlock[]>;
|
|
35
|
+
/** Resolve compiled references through the exact fragment bindings selected for this page. */
|
|
36
|
+
export declare function resolveFragmentNodes(nodes: readonly KernelPageNode[], getFragment: FragmentLoader): Promise<KernelPageBlock[]>;
|
|
37
|
+
/** Use the same fragment root identity for layout defaults and region placements. */
|
|
38
|
+
export declare function fragmentToBlock(fragment: FragmentData, id: string): KernelPageBlock;
|