@gradial/aci 0.1.30 → 0.1.31
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/bin/aci-darwin-arm64 +0 -0
- package/bin/aci.js +4 -1
- package/dist/content/provider.d.ts +7 -0
- package/dist/next/server.js +26 -1
- package/dist/providers/s3.d.ts +7 -0
- package/dist/providers/s3.js +20 -0
- package/package.json +1 -1
- package/templates/nextjs/template/package.json.tmpl +0 -2
- package/templates/nextjs/template/src/app/layout.tsx +1 -3
- package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +0 -132
package/bin/aci-darwin-arm64
CHANGED
|
Binary file
|
package/bin/aci.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync } from 'node:fs';
|
|
4
4
|
import { cp, mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
@@ -62,6 +62,9 @@ function resolveWorkspaceBinary() {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const binaryPath = resolveBundledBinary() ?? resolvePackagedBinary() ?? resolveWorkspaceBinary();
|
|
65
|
+
if (binaryPath) {
|
|
66
|
+
try { chmodSync(binaryPath, 0o755); } catch {}
|
|
67
|
+
}
|
|
65
68
|
const fallback = binaryPath ? null : resolveGoFallback();
|
|
66
69
|
|
|
67
70
|
if (!binaryPath && !fallback) {
|
|
@@ -5,6 +5,13 @@ export interface RouteEntry {
|
|
|
5
5
|
payloadRef: string;
|
|
6
6
|
digest: string;
|
|
7
7
|
fragmentRefs?: string[];
|
|
8
|
+
variants?: Record<string, RouteVariantEntry>;
|
|
9
|
+
}
|
|
10
|
+
export interface RouteVariantEntry {
|
|
11
|
+
variantKey: string;
|
|
12
|
+
payloadRef: string;
|
|
13
|
+
digest?: string;
|
|
14
|
+
variantValues?: Record<string, string>;
|
|
8
15
|
}
|
|
9
16
|
export interface FragmentEntry {
|
|
10
17
|
id: string;
|
package/dist/next/server.js
CHANGED
|
@@ -10,6 +10,9 @@ export async function getPage(route = '', config = {}) {
|
|
|
10
10
|
if (runtimeInput)
|
|
11
11
|
return runtimeInput.page;
|
|
12
12
|
const provider = await resolveProviderWithFallback(config);
|
|
13
|
+
const variantPage = await pageFromFunctionRef(provider);
|
|
14
|
+
if (variantPage !== null)
|
|
15
|
+
return variantPage;
|
|
13
16
|
return await provider.getPage(route);
|
|
14
17
|
}
|
|
15
18
|
export async function getSiteConfig(config = {}) {
|
|
@@ -53,7 +56,7 @@ export async function getRenderInput(route = '/', config = {}) {
|
|
|
53
56
|
// Cache shared resources (manifest, siteConfig) but not per-page content
|
|
54
57
|
// Page-level caching is handled by CDN
|
|
55
58
|
const [page, siteConfig] = await Promise.all([
|
|
56
|
-
provider.getPage(route),
|
|
59
|
+
pageFromFunctionRef(provider).then((variantPage) => variantPage ?? provider.getPage(route)),
|
|
57
60
|
getCachedSiteConfig(provider),
|
|
58
61
|
]);
|
|
59
62
|
return {
|
|
@@ -73,6 +76,28 @@ export async function getPageRuntimeRenderInput() {
|
|
|
73
76
|
return null;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
// The personalization edge proxies framework-origin requests with
|
|
80
|
+
// X-Baremetal-Function-Ref naming the exact page payload its decision
|
|
81
|
+
// selected (base or variant). When present, the SSR render must use that
|
|
82
|
+
// payload — otherwise every visitor gets the base page and edge decisioning
|
|
83
|
+
// is cosmetic. The S3 provider only serves refs the release manifest
|
|
84
|
+
// declares.
|
|
85
|
+
const BAREMETAL_FUNCTION_REF_HEADER = 'x-baremetal-function-ref';
|
|
86
|
+
async function pageFromFunctionRef(provider) {
|
|
87
|
+
if (!(provider instanceof S3ContentProvider))
|
|
88
|
+
return null;
|
|
89
|
+
let ref = '';
|
|
90
|
+
try {
|
|
91
|
+
const current = await headers();
|
|
92
|
+
ref = (current.get(BAREMETAL_FUNCTION_REF_HEADER) || '').trim();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (!ref)
|
|
98
|
+
return null;
|
|
99
|
+
return await provider.getPageByRef(ref);
|
|
100
|
+
}
|
|
76
101
|
export { isPreviewMode } from './preview-mode.js';
|
|
77
102
|
export async function routeFromNextParams(params) {
|
|
78
103
|
if (!params)
|
package/dist/providers/s3.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export declare class S3ContentProvider implements ContentProvider {
|
|
|
16
16
|
listRoutes(): Promise<RouteEntry[]>;
|
|
17
17
|
getSiteConfig<T = unknown>(): Promise<T>;
|
|
18
18
|
getPage<T = unknown>(route: string): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* getPageByRef serves the exact payload the edge selected (the
|
|
21
|
+
* X-Baremetal-Function-Ref contract): base page or variant. Only refs the
|
|
22
|
+
* release manifest declares are served — the header names published
|
|
23
|
+
* content, it cannot address arbitrary keys.
|
|
24
|
+
*/
|
|
25
|
+
getPageByRef<T = unknown>(payloadRef: string): Promise<T>;
|
|
19
26
|
getFragment<T = unknown>(id: string): Promise<T>;
|
|
20
27
|
getLayout<T = unknown>(name: string): Promise<T>;
|
|
21
28
|
manifest(): Promise<CompiledManifest>;
|
package/dist/providers/s3.js
CHANGED
|
@@ -32,6 +32,26 @@ export class S3ContentProvider {
|
|
|
32
32
|
throw new PageNotFoundError(route, error);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* getPageByRef serves the exact payload the edge selected (the
|
|
37
|
+
* X-Baremetal-Function-Ref contract): base page or variant. Only refs the
|
|
38
|
+
* release manifest declares are served — the header names published
|
|
39
|
+
* content, it cannot address arbitrary keys.
|
|
40
|
+
*/
|
|
41
|
+
async getPageByRef(payloadRef) {
|
|
42
|
+
const manifest = await this.manifest();
|
|
43
|
+
const known = Object.values(manifest.routes).some((entry) => entry.payloadRef === payloadRef ||
|
|
44
|
+
Object.values(entry.variants ?? {}).some((variant) => variant.payloadRef === payloadRef));
|
|
45
|
+
if (!known) {
|
|
46
|
+
throw new PageNotFoundError(payloadRef);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return await this.getJSON(payloadRef);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new PageNotFoundError(payloadRef, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
35
55
|
async getFragment(id) {
|
|
36
56
|
const manifest = await this.manifest();
|
|
37
57
|
const entry = manifest.fragments[id];
|
package/package.json
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { Inter, Space_Grotesk } from 'next/font/google';
|
|
3
3
|
import { DevRefresh } from '@gradial/aci/next';
|
|
4
|
-
import { ConsentManager } from '@/app/_components/ConsentManager';
|
|
5
|
-
import '@c15t/nextjs/styles.css';
|
|
6
4
|
import '@/design-system/styles.css';
|
|
7
5
|
|
|
8
6
|
const inter = Inter({
|
|
@@ -23,7 +21,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
|
|
23
21
|
return (
|
|
24
22
|
<html lang="en" className={`${inter.variable} ${spaceGrotesk.variable}`}>
|
|
25
23
|
<body>
|
|
26
|
-
|
|
24
|
+
{children}
|
|
27
25
|
<DevRefresh />
|
|
28
26
|
</body>
|
|
29
27
|
</html>
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ConsentBanner,
|
|
5
|
-
ConsentDialog,
|
|
6
|
-
ConsentManagerProvider,
|
|
7
|
-
useConsentManager,
|
|
8
|
-
type ConsentManagerProviderProps,
|
|
9
|
-
} from '@c15t/nextjs';
|
|
10
|
-
import { useEffect, useMemo, type ReactNode } from 'react';
|
|
11
|
-
|
|
12
|
-
// Hosted c15t backend (consent.io or self-hosted). Unset = offline mode:
|
|
13
|
-
// consent is stored client-side and no network calls are made.
|
|
14
|
-
const C15T_BACKEND_URL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
|
|
15
|
-
|
|
16
|
-
// The CMP's own categories. The mapping from categories to the Gradial
|
|
17
|
-
// personalization purpose lives in .content/config/site.json
|
|
18
|
-
// (personalization.consentCategories), not here.
|
|
19
|
-
const CONSENT_CATEGORIES = ['necessary', 'measurement', 'marketing'] as const;
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Gradial collect bridge
|
|
23
|
-
//
|
|
24
|
-
// The Gradial collect SDK's c15t preset (the site config's
|
|
25
|
-
// `personalization.consentPreset: "c15t"`, emitted as
|
|
26
|
-
// `data-aci-consent="c15t"` on the collect tag) reads
|
|
27
|
-
// `window.c15t = { consents, onConsentChange }`. c15t itself does not
|
|
28
|
-
// publish that shape, so this bridge mirrors the store onto it — only after
|
|
29
|
-
// a real visitor decision (`consentInfo` set), never for pre-decision
|
|
30
|
-
// defaults, so the SDK cannot mistake an untouched banner for a withdrawal.
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
type Consents = Record<string, boolean>;
|
|
34
|
-
type ConsentListener = (payload: { consents: Consents }) => void;
|
|
35
|
-
|
|
36
|
-
declare global {
|
|
37
|
-
interface Window {
|
|
38
|
-
c15t?: {
|
|
39
|
-
consents?: Consents;
|
|
40
|
-
onConsentChange?: (cb: ConsentListener) => void;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const consentListeners: ConsentListener[] = [];
|
|
46
|
-
|
|
47
|
-
function publishAciConsents(consents: Consents): void {
|
|
48
|
-
const bridge = (window.c15t ??= {});
|
|
49
|
-
bridge.consents = consents;
|
|
50
|
-
bridge.onConsentChange ??= (cb: ConsentListener) => {
|
|
51
|
-
consentListeners.push(cb);
|
|
52
|
-
};
|
|
53
|
-
for (const listener of [...consentListeners]) {
|
|
54
|
-
listener({ consents });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function AciConsentBridge() {
|
|
59
|
-
const { consents, consentInfo } = useConsentManager();
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (!consentInfo || !consents) return;
|
|
62
|
-
publishAciConsents(consents as Consents);
|
|
63
|
-
}, [consents, consentInfo]);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Consent manager
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
function getConsentOptions(): ConsentManagerProviderProps['options'] {
|
|
72
|
-
const baseOptions = {
|
|
73
|
-
consentCategories: [...CONSENT_CATEGORIES],
|
|
74
|
-
legalLinks: {
|
|
75
|
-
privacyPolicy: {
|
|
76
|
-
href: '/privacy/',
|
|
77
|
-
target: '_self' as const,
|
|
78
|
-
label: 'Privacy policy',
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if (C15T_BACKEND_URL) {
|
|
84
|
-
return {
|
|
85
|
-
...baseOptions,
|
|
86
|
-
mode: 'hosted',
|
|
87
|
-
backendURL: C15T_BACKEND_URL,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
...baseOptions,
|
|
93
|
-
mode: 'offline',
|
|
94
|
-
offlinePolicy: {
|
|
95
|
-
policyPacks: [
|
|
96
|
-
{
|
|
97
|
-
id: 'default-opt-in',
|
|
98
|
-
match: { isDefault: true },
|
|
99
|
-
consent: {
|
|
100
|
-
model: 'opt-in',
|
|
101
|
-
categories: [...CONSENT_CATEGORIES],
|
|
102
|
-
},
|
|
103
|
-
ui: { mode: 'banner' },
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function ConsentManager({ children }: { children: ReactNode }) {
|
|
111
|
-
const options = useMemo(() => getConsentOptions(), []);
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<ConsentManagerProvider options={options}>
|
|
115
|
-
{children}
|
|
116
|
-
<AciConsentBridge />
|
|
117
|
-
<ConsentBanner
|
|
118
|
-
title="Cookie preferences"
|
|
119
|
-
description="We use cookies to keep the site working, measure performance, and support marketing."
|
|
120
|
-
acceptButtonText="Accept all"
|
|
121
|
-
rejectButtonText="Reject all"
|
|
122
|
-
customizeButtonText="Customize"
|
|
123
|
-
legalLinks={['privacyPolicy']}
|
|
124
|
-
layout={[['reject', 'accept'], 'customize']}
|
|
125
|
-
primaryButton="accept"
|
|
126
|
-
scrollLock={false}
|
|
127
|
-
hideBranding
|
|
128
|
-
/>
|
|
129
|
-
<ConsentDialog legalLinks={['privacyPolicy']} scrollLock trapFocus hideBranding />
|
|
130
|
-
</ConsentManagerProvider>
|
|
131
|
-
);
|
|
132
|
-
}
|