@preprio/toolkit 0.6.0-alpha.1 → 0.6.0-alpha.10

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.
Files changed (55) hide show
  1. package/README.md +163 -61
  2. package/dist/astro/{chunk-O7N6GPAH.js → chunk-CG2QBHNU.js} +1 -1
  3. package/dist/astro/graphql.cjs +58 -10
  4. package/dist/astro/graphql.d.cts +33 -38
  5. package/dist/astro/graphql.d.ts +33 -38
  6. package/dist/astro/graphql.js +59 -11
  7. package/dist/astro/index.cjs +1 -1
  8. package/dist/astro/index.js +1 -1
  9. package/dist/{chunk-VI6QZTBM.js → chunk-LIDUJWWJ.js} +1 -1
  10. package/dist/{chunk-TBKSBXGH.js → chunk-M3RBYJ5J.js} +52 -5
  11. package/dist/{chunk-3JLWZQ37.js → chunk-SF6TN4JR.js} +1 -1
  12. package/dist/{chunk-YWIKKWBR.js → chunk-TN6SNAR2.js} +1 -1
  13. package/dist/graphql/codegen.cjs +51 -4
  14. package/dist/graphql/codegen.js +3 -3
  15. package/dist/graphql.cjs +89 -10
  16. package/dist/graphql.d.cts +69 -39
  17. package/dist/graphql.d.ts +69 -39
  18. package/dist/graphql.js +38 -9
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +3 -3
  23. package/dist/nextjs/chunk-B4GVZEOL.js +549 -0
  24. package/dist/nextjs/components.cjs +297 -81
  25. package/dist/nextjs/components.d.cts +26 -11
  26. package/dist/nextjs/components.d.ts +26 -11
  27. package/dist/nextjs/components.js +288 -73
  28. package/dist/nextjs/graphql.cjs +856 -266
  29. package/dist/nextjs/graphql.d.cts +184 -116
  30. package/dist/nextjs/graphql.d.ts +184 -116
  31. package/dist/nextjs/graphql.js +543 -266
  32. package/dist/nextjs/index.cjs +116 -54
  33. package/dist/nextjs/index.d.cts +61 -23
  34. package/dist/nextjs/index.d.ts +61 -23
  35. package/dist/nextjs/index.js +97 -299
  36. package/dist/nextjs/{server-CjpELYvq.d.ts → server-BL-sqZvu.d.cts} +1 -1
  37. package/dist/nextjs/{server-CjpELYvq.d.cts → server-BL-sqZvu.d.ts} +1 -1
  38. package/dist/nuxt/{chunk-IQDSBPDN.js → chunk-FMC3DP7N.js} +1 -1
  39. package/dist/nuxt/graphql.cjs +58 -10
  40. package/dist/nuxt/graphql.d.cts +33 -38
  41. package/dist/nuxt/graphql.d.ts +33 -38
  42. package/dist/nuxt/graphql.js +59 -11
  43. package/dist/nuxt/index.cjs +1 -1
  44. package/dist/nuxt/index.js +1 -1
  45. package/dist/react/components.cjs +1 -1
  46. package/dist/react/components.js +1 -1
  47. package/dist/sveltekit/{chunk-OKYZ6HPH.js → chunk-AJPKBAHO.js} +1 -1
  48. package/dist/sveltekit/graphql.cjs +58 -10
  49. package/dist/sveltekit/graphql.d.cts +33 -38
  50. package/dist/sveltekit/graphql.d.ts +33 -38
  51. package/dist/sveltekit/graphql.js +59 -11
  52. package/dist/sveltekit/index.cjs +1 -1
  53. package/dist/sveltekit/index.js +1 -1
  54. package/package.json +1 -1
  55. package/dist/nextjs/chunk-BQ6IGSH6.js +0 -231
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name2 in all)
@@ -15,19 +17,30 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/nextjs/graphql.ts
21
31
  var graphql_exports = {};
22
32
  __export(graphql_exports, {
23
- PREPR_DECISIONS_COOKIE: () => PREPR_DECISIONS_COOKIE,
33
+ PREPR_ANY_ITEM_TAG: () => PREPR_ANY_ITEM_TAG,
24
34
  PREPR_DEFAULT_VARIANT: () => PREPR_DEFAULT_VARIANT,
25
35
  PreprError: () => PreprError,
26
36
  PreprStack: () => PreprStack,
27
37
  createPreprClient: () => createPreprClient2,
38
+ createPreprProxy: () => createPreprProxy,
28
39
  decisionsFrom: () => decisionsFrom,
29
40
  isPersonalizedResponse: () => isPersonalizedResponse,
30
41
  loadPreprSchema: () => loadPreprSchema,
42
+ preprCacheTags: () => preprCacheTags,
43
+ preprModelTag: () => preprModelTag,
31
44
  preprVariantProps: () => preprVariantProps,
32
45
  resolveVariants: () => resolveVariants,
33
46
  splitStack: () => splitStack,
@@ -35,6 +48,78 @@ __export(graphql_exports, {
35
48
  toVariantsQuery: () => toVariantsQuery
36
49
  });
37
50
  module.exports = __toCommonJS(graphql_exports);
51
+ var nextCache = __toESM(require("next/cache"), 1);
52
+
53
+ // src/core/graphql/cache-tags.ts
54
+ var MAX_TAGS = 128;
55
+ var MAX_TAG_LENGTH = 256;
56
+ var PREPR_ANY_ITEM_TAG = "prepr:any";
57
+ var PREPR_SCHEMA_TAG = "prepr:schema";
58
+ var preprItemTag = (id) => `prepr:${id}`;
59
+ var NOT_ALPHANUMERIC = /[^a-z0-9]/g;
60
+ var preprModelTag = (model) => `prepr:model:${model.toLowerCase().replace(NOT_ALPHANUMERIC, "")}`;
61
+ function preprModelTagsFor(document2, data, schema) {
62
+ const operation = document2.definitions.find(
63
+ (definition) => definition.kind === "OperationDefinition" && definition.operation === "query"
64
+ );
65
+ const root = schema.types[schema.queryType];
66
+ if (!operation || !root) return [];
67
+ const models = (type) => {
68
+ const members = schema.types[type]?.possibleTypes ?? [];
69
+ return members.length > 0 ? members : [type];
70
+ };
71
+ const tags = /* @__PURE__ */ new Set();
72
+ for (const selection of operation.selectionSet.selections) {
73
+ if (selection.kind !== "Field") continue;
74
+ const type = root.fields[selection.name.value]?.type;
75
+ const info = type ? schema.types[type] : void 0;
76
+ if (!type || !info) continue;
77
+ const items = info.fields.items?.type;
78
+ const key = (selection.alias ?? selection.name).value;
79
+ const found = typeof data === "object" && data !== null && data[key] != null;
80
+ const affected = items ? models(items) : found ? [] : models(type);
81
+ for (const model of affected) tags.add(preprModelTag(model));
82
+ }
83
+ return [...tags];
84
+ }
85
+ function preprCacheTags(data) {
86
+ const tags = /* @__PURE__ */ new Set();
87
+ const stack = [data];
88
+ while (stack.length > 0) {
89
+ const value = stack.pop();
90
+ if (typeof value !== "object" || value === null) continue;
91
+ const id = value._id;
92
+ if (typeof id === "string" && id !== "") {
93
+ const tag = preprItemTag(id);
94
+ if (tag.length > MAX_TAG_LENGTH) return [PREPR_ANY_ITEM_TAG];
95
+ tags.add(tag);
96
+ if (tags.size > MAX_TAGS) return [PREPR_ANY_ITEM_TAG];
97
+ }
98
+ for (const child of Object.values(value)) stack.push(child);
99
+ }
100
+ return [...tags];
101
+ }
102
+
103
+ // src/version.ts
104
+ var VERSION = "0.6.0-alpha.10";
105
+
106
+ // src/core/features.ts
107
+ function isEnabled(config) {
108
+ if (typeof config === "boolean") return config;
109
+ return config?.enabled ?? true;
110
+ }
111
+ function resolveFeatures(features) {
112
+ return {
113
+ segments: isEnabled(features?.segments),
114
+ abTesting: isEnabled(features?.abTesting),
115
+ editMode: isEnabled(features?.editMode)
116
+ };
117
+ }
118
+ var ALL_FEATURES_ENABLED = Object.freeze({
119
+ segments: true,
120
+ abTesting: true,
121
+ editMode: true
122
+ });
38
123
 
39
124
  // src/core/server.ts
40
125
  var PreprError = class extends Error {
@@ -180,7 +265,10 @@ function decisionSelectionSet(set, typeName, path, ctx) {
180
265
  path,
181
266
  ctx
182
267
  );
183
- if (pruned) {
268
+ if (!pruned) continue;
269
+ if (fragment.typeCondition.name.value === typeName) {
270
+ selections.push(...pruned.selections);
271
+ } else {
184
272
  selections.push(
185
273
  inlineFragment(fragment.typeCondition.name.value, [
186
274
  ...pruned.selections
@@ -190,13 +278,19 @@ function decisionSelectionSet(set, typeName, path, ctx) {
190
278
  continue;
191
279
  }
192
280
  if (selection.kind === "InlineFragment") {
281
+ const condition = selection.typeCondition?.name.value ?? typeName;
193
282
  const pruned = decisionSelectionSet(
194
283
  selection.selectionSet,
195
- selection.typeCondition?.name.value ?? typeName,
284
+ condition,
196
285
  path,
197
286
  ctx
198
287
  );
199
- if (pruned) selections.push({ ...selection, selectionSet: pruned });
288
+ if (!pruned) continue;
289
+ if (condition === typeName && !selection.directives?.length) {
290
+ selections.push(...pruned.selections);
291
+ } else {
292
+ selections.push({ ...selection, selectionSet: pruned });
293
+ }
200
294
  continue;
201
295
  }
202
296
  if (!selection.selectionSet) continue;
@@ -270,20 +364,58 @@ function toDecisionDocument(document2, schema, groupPaths, options) {
270
364
  const used = /* @__PURE__ */ new Set();
271
365
  collectVariables(pruned, used);
272
366
  collectVariables(operation.directives, used);
367
+ const shared = sharedContextFragment(schema);
273
368
  return {
274
369
  kind: "Document",
275
370
  definitions: [
371
+ ...shared ? [shared.definition] : [],
276
372
  {
277
373
  ...operation,
278
374
  name: operation.name && name(`${operation.name.value}Decision`),
279
375
  variableDefinitions: (operation.variableDefinitions ?? []).filter(
280
376
  (definition) => used.has(definition.variable.name.value)
281
377
  ),
282
- selectionSet: pruned
378
+ selectionSet: shared ? shared.apply(pruned) : pruned
283
379
  }
284
380
  ]
285
381
  };
286
382
  }
383
+ function sharedContextFragment(schema) {
384
+ const contextTypes = new Set(
385
+ Object.values(schema.types).flatMap(
386
+ (type) => type.hasContext ? [type.fields._context.type] : []
387
+ )
388
+ );
389
+ if (contextTypes.size !== 1) return null;
390
+ const [contextType] = contextTypes;
391
+ const fragmentName = "c";
392
+ const definition = {
393
+ kind: "FragmentDefinition",
394
+ name: name(fragmentName),
395
+ typeCondition: { kind: "NamedType", name: name(contextType) },
396
+ selectionSet: selectionSet(
397
+ ["kind", "group_id", "variant_id"].map(
398
+ (field) => ({ kind: "Field", name: name(field) })
399
+ )
400
+ )
401
+ };
402
+ const spread = {
403
+ kind: "FragmentSpread",
404
+ name: name(fragmentName)
405
+ };
406
+ const apply = (set) => selectionSet(
407
+ set.selections.map((selection) => {
408
+ if (selection.kind === "Field" && selection.alias?.value === PREPR_CONTEXT_ALIAS) {
409
+ return { ...selection, selectionSet: selectionSet([spread]) };
410
+ }
411
+ return "selectionSet" in selection && selection.selectionSet ? {
412
+ ...selection,
413
+ selectionSet: apply(selection.selectionSet)
414
+ } : selection;
415
+ })
416
+ );
417
+ return { definition, apply };
418
+ }
287
419
 
288
420
  // src/core/variants/probe-document.ts
289
421
  function probeSelectionSet(set, fragments) {
@@ -864,6 +996,7 @@ function createVaryProbe(run, options) {
864
996
  }
865
997
 
866
998
  // src/core/graphql/client.ts
999
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
867
1000
  function isParseableUrl(url) {
868
1001
  try {
869
1002
  new URL(url);
@@ -900,7 +1033,7 @@ function createPreprClient(options) {
900
1033
  ));
901
1034
  const variantsDocuments = /* @__PURE__ */ new Map();
902
1035
  const decisionDocuments = /* @__PURE__ */ new Map();
903
- return {
1036
+ const client = {
904
1037
  async query(document2, variables, queryOptions = {}) {
905
1038
  const { variants, ...rest } = queryOptions;
906
1039
  if (!variants) return run(document2, variables, rest);
@@ -912,10 +1045,10 @@ function createPreprClient(options) {
912
1045
  return run(derived, variables, rest);
913
1046
  },
914
1047
  async decide(document2, variables, queryOptions = {}) {
915
- const { from, topLevelOnly, ...rest } = queryOptions;
1048
+ const { from, ...rest } = queryOptions;
916
1049
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
917
1050
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
918
- const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : topLevelOnly ? "top" : "*";
1051
+ const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : "*";
919
1052
  let byPaths = decisionDocuments.get(document2);
920
1053
  if (!byPaths) {
921
1054
  byPaths = /* @__PURE__ */ new Map();
@@ -923,9 +1056,7 @@ function createPreprClient(options) {
923
1056
  }
924
1057
  let derived = byPaths.get(signature);
925
1058
  if (derived === void 0) {
926
- derived = toDecisionDocument(document2, await loadSchema(), paths, {
927
- topLevelOnly: !paths && topLevelOnly
928
- });
1059
+ derived = toDecisionDocument(document2, await loadSchema(), paths);
929
1060
  byPaths.set(signature, derived);
930
1061
  }
931
1062
  if (!derived) return /* @__PURE__ */ new Map();
@@ -939,6 +1070,8 @@ function createPreprClient(options) {
939
1070
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
940
1071
  )
941
1072
  };
1073
+ clientSchemas.set(client, loadSchema);
1074
+ return client;
942
1075
  }
943
1076
 
944
1077
  // src/core/graphql/schema-loader.ts
@@ -966,25 +1099,62 @@ async function loadPreprSchema(url, options = {}) {
966
1099
  }
967
1100
 
968
1101
  // src/nextjs/decisions.ts
1102
+ var PREPR_DECIDE_HEADER = "Prepr-Decide";
1103
+ var DEFAULT_DECISION = "*";
969
1104
  var PREPR_DECISIONS_COOKIE = "prepr_decisions";
970
- var DECISIONS_MAX_GROUPS = 40;
971
- var SAFE_ID = /^[\w-]+$/;
972
- var SEEN_MAX_PAGES = 1e3;
973
- function parseDecisions(raw) {
974
- const out = /* @__PURE__ */ new Map();
975
- for (const pair of (raw ?? "").split("~")) {
976
- const at = pair.indexOf(":");
977
- const group = pair.slice(0, at);
978
- if (at < 1 || !SAFE_ID.test(group)) continue;
979
- out.set(
980
- group,
981
- pair.slice(at + 1).split("|").filter((variant) => SAFE_ID.test(variant))
982
- );
1105
+ var DECISION_FRESH_MS = 1e4;
1106
+ var DECISION_KEEP_MS = 18e5;
1107
+ var DECISION_KEEP_STATIC_MS = 36e5;
1108
+ var STORED_MAX_ENTRIES = 20;
1109
+ var STORED_MAX_LENGTH = 3e3;
1110
+ function readStoredDecisions(raw) {
1111
+ const stored = /* @__PURE__ */ Object.create(null);
1112
+ if (!raw) return stored;
1113
+ try {
1114
+ const parsed = JSON.parse(raw);
1115
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1116
+ return stored;
1117
+ for (const [key, entry] of Object.entries(parsed)) {
1118
+ if (key.startsWith("/") && Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number") {
1119
+ stored[key] = [entry[0], entry[1]];
1120
+ }
1121
+ }
1122
+ } catch {
1123
+ }
1124
+ return stored;
1125
+ }
1126
+ function usableDecision(stored, key, now) {
1127
+ const entry = stored[key];
1128
+ if (!entry) return void 0;
1129
+ const [value, at] = entry;
1130
+ const age = now - at;
1131
+ const keep = value === DEFAULT_DECISION ? DECISION_FRESH_MS : value === "" ? DECISION_KEEP_STATIC_MS : DECISION_KEEP_MS;
1132
+ return age >= 0 && age < keep ? value : void 0;
1133
+ }
1134
+ function writeStoredDecisions(stored, key, value, now) {
1135
+ const kept = usableDecision(stored, key, now);
1136
+ const entries = Object.entries(stored).filter(
1137
+ ([stale]) => stale !== key && usableDecision(stored, stale, now) !== void 0
1138
+ );
1139
+ entries.push([
1140
+ key,
1141
+ value === DEFAULT_DECISION && kept !== void 0 && kept !== DEFAULT_DECISION ? stored[key] : [value, now]
1142
+ ]);
1143
+ entries.sort((a, b) => b[1][1] - a[1][1]);
1144
+ entries.length = Math.min(entries.length, STORED_MAX_ENTRIES);
1145
+ let json = JSON.stringify(Object.fromEntries(entries));
1146
+ while (entries.length > 1 && encodeURIComponent(json).length > STORED_MAX_LENGTH) {
1147
+ entries.pop();
1148
+ json = JSON.stringify(Object.fromEntries(entries));
983
1149
  }
984
- return out;
1150
+ return json;
985
1151
  }
1152
+ var DECISIONS_MAX_GROUPS = 40;
1153
+ var SAFE_ID = /^[\w-]+$/;
986
1154
  function serializeDecisions(decisions) {
987
- return [...decisions].slice(0, DECISIONS_MAX_GROUPS).map(([group, variants]) => `${group}:${variants.join("|")}`).join("~");
1155
+ return [...decisions].filter(([group]) => SAFE_ID.test(group)).slice(0, DECISIONS_MAX_GROUPS).map(
1156
+ ([group, variants]) => `${group}:${variants.filter((v) => SAFE_ID.test(v)).join("|")}`
1157
+ ).join("~");
988
1158
  }
989
1159
  function forwardedHeaders(request, response) {
990
1160
  const overridden = response.headers.get("x-middleware-override-headers");
@@ -996,60 +1166,17 @@ function forwardedHeaders(request, response) {
996
1166
  }
997
1167
  return getPreprHeadersFromHeaders(headers2);
998
1168
  }
999
- async function resolveDecisionsWith(client, seenGroups, request, response, document2, variables, options) {
1169
+ var preprClientInternals = /* @__PURE__ */ new WeakMap();
1170
+ async function decideView(client, headers2, page, timeoutMs = 1e3) {
1171
+ const init = { signal: AbortSignal.timeout(timeoutMs) };
1000
1172
  try {
1001
- if (options?.onlyIf && !await options.onlyIf()) return;
1002
- if (!await client.hasSegments(document2, variables)) return;
1003
- const headers2 = mergePreprHeaders(forwardedHeaders(request, response));
1004
- const stored = parseDecisions(
1005
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1006
- );
1007
- const pageKey = `${JSON.stringify(variables ?? null)}`;
1008
- let byVariables = seenGroups.get(document2);
1009
- if (!byVariables) {
1010
- byVariables = /* @__PURE__ */ new Map();
1011
- seenGroups.set(document2, byVariables);
1012
- }
1013
- const known = byVariables.get(pageKey);
1014
- const isNewVisitor = headers2.get("Prepr-Customer-Id-Created") === "true";
1015
- const contextChanged = isNewVisitor || headers2.has("Prepr-Segments") || [...request.nextUrl.searchParams.keys()].some(
1016
- (key) => key.startsWith("utm_")
1017
- );
1018
- if (options?.reuse !== false && !contextChanged && known && [...known].every((group) => stored.has(group))) {
1019
- return;
1020
- }
1021
- const decided = await client.decide(document2, variables, {
1022
- topLevelOnly: true,
1023
- headers: headers2,
1024
- init: { signal: AbortSignal.timeout(options?.timeoutMs ?? 1e3) }
1025
- });
1026
- if (!known && byVariables.size >= SEEN_MAX_PAGES) {
1027
- byVariables.delete(byVariables.keys().next().value);
1028
- }
1029
- const groups = known ?? /* @__PURE__ */ new Set();
1030
- for (const group of decided.keys()) groups.add(group);
1031
- byVariables.set(pageKey, groups);
1032
- const next = /* @__PURE__ */ new Map();
1033
- for (const [group, variants] of decided) {
1034
- if (SAFE_ID.test(group)) {
1035
- next.set(
1036
- group,
1037
- variants.filter((v) => SAFE_ID.test(v))
1038
- );
1039
- }
1040
- }
1041
- for (const [group, variants] of stored) {
1042
- if (!next.has(group)) next.set(group, variants);
1043
- }
1044
- if (next.size === 0) return;
1045
- response.cookies.set(PREPR_DECISIONS_COOKIE, serializeDecisions(next), {
1046
- path: "/",
1047
- sameSite: "lax",
1048
- // Read by the inline switch script before first paint.
1049
- httpOnly: false
1050
- });
1173
+ const decided = page.decision ? decisionsFrom(
1174
+ (await client.query(page.decision, page.variables, { headers: headers2, init })).data
1175
+ ) : await client.decide(page.document, page.variables, { headers: headers2, init });
1176
+ return serializeDecisions(decided);
1051
1177
  } catch (error) {
1052
1178
  if (isFrameworkSignal(error)) throw error;
1179
+ return DEFAULT_DECISION;
1053
1180
  }
1054
1181
  }
1055
1182
 
@@ -1063,227 +1190,668 @@ async function getPreprHeaders() {
1063
1190
  return getPreprHeadersFromHeaders(h);
1064
1191
  }
1065
1192
 
1066
- // src/nextjs/stack.ts
1067
- var import_react = require("react");
1193
+ // src/nextjs/proxy.ts
1194
+ var import_server9 = require("next/server");
1068
1195
 
1069
- // src/nextjs/ab-switch.ts
1070
- function applyPreprAbBucket() {
1071
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
1072
- const bucket = match ? match[1] : "A";
1073
- const state = window;
1074
- if (state.__preprAbBucket === bucket) return;
1075
- state.__preprAbBucket = bucket;
1076
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
1077
- try {
1078
- if (!state.__preprAbSheet) {
1079
- const sheet = new CSSStyleSheet();
1080
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1081
- state.__preprAbSheet = sheet;
1196
+ // src/nextjs/draft.ts
1197
+ var import_headers2 = require("next/headers");
1198
+ var import_navigation = require("next/navigation");
1199
+ var import_server7 = require("next/server");
1200
+ var DRAFT_COOKIE = "__prerender_bypass";
1201
+ var RETURN_PARAM = "prepr_draft";
1202
+ var DEFAULT_ROUTE = "/api/prepr/draft";
1203
+ function preprDraftRedirect(request, options) {
1204
+ if (!options.preview) return null;
1205
+ if (request.method !== "GET" && request.method !== "HEAD") return null;
1206
+ const url = request.nextUrl;
1207
+ if (url.pathname === (options.route ?? DEFAULT_ROUTE)) return null;
1208
+ if (request.cookies.has(DRAFT_COOKIE)) {
1209
+ if (!url.searchParams.has(RETURN_PARAM)) return null;
1210
+ const clean = url.clone();
1211
+ clean.searchParams.delete(RETURN_PARAM);
1212
+ return import_server7.NextResponse.redirect(clean, 307);
1213
+ }
1214
+ if (url.searchParams.has(RETURN_PARAM)) return null;
1215
+ const target = new URL(options.route ?? DEFAULT_ROUTE, request.url);
1216
+ target.searchParams.set("to", url.pathname + url.search);
1217
+ return import_server7.NextResponse.redirect(target, 307);
1218
+ }
1219
+
1220
+ // src/nextjs/middleware.ts
1221
+ var import_server8 = require("next/server");
1222
+
1223
+ // src/core/constants.ts
1224
+ var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
1225
+ var COOKIE_SEGMENT = "Prepr-Segments";
1226
+ var COOKIE_VARIANT = "Prepr-ABtesting";
1227
+ var COOKIE_UID = "__prepr_uid";
1228
+ var COOKIE_HUBSPOT = "hubspotutk";
1229
+ var PARAM_SEGMENT = "prepr_preview_segment";
1230
+ var PARAM_VARIANT = "prepr_preview_ab";
1231
+ var PARAM_HIDE_BAR = "prepr_hide_bar";
1232
+ var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
1233
+
1234
+ // src/core/middleware.ts
1235
+ var UTM_PARAMS = [
1236
+ "utm_source",
1237
+ "utm_medium",
1238
+ "utm_term",
1239
+ "utm_content",
1240
+ "utm_campaign"
1241
+ ];
1242
+ var MAX_HEADER_VALUE_LENGTH = 2048;
1243
+ function sanitizeHeaderValue(value) {
1244
+ const stripped = value.replace(/[\u0000-\u001f\u007f]/g, "");
1245
+ return stripped.length > MAX_HEADER_VALUE_LENGTH ? stripped.slice(0, MAX_HEADER_VALUE_LENGTH) : stripped;
1246
+ }
1247
+ function parseCookies(request) {
1248
+ const cookies2 = /* @__PURE__ */ new Map();
1249
+ const header = request.headers.get("cookie");
1250
+ if (!header) return cookies2;
1251
+ for (const part of header.split(";")) {
1252
+ const eq = part.indexOf("=");
1253
+ if (eq === -1) continue;
1254
+ const name2 = part.slice(0, eq).trim();
1255
+ const value = part.slice(eq + 1).trim();
1256
+ if (!name2) continue;
1257
+ try {
1258
+ cookies2.set(name2, decodeURIComponent(value));
1259
+ } catch {
1260
+ cookies2.set(name2, value);
1082
1261
  }
1083
- state.__preprAbSheet.replaceSync(css);
1084
- } catch {
1085
- if (!state.__preprAbSheet) {
1086
- state.__preprAbSheet = document.createElement("style");
1087
- document.head.appendChild(state.__preprAbSheet);
1262
+ }
1263
+ return cookies2;
1264
+ }
1265
+ function processFrameworkRequest(request, opts, framework) {
1266
+ const requestHeaders = new Headers(request.headers);
1267
+ const responseCookies = [];
1268
+ const inboundPreprHeaders = [];
1269
+ requestHeaders.forEach((_value, key) => {
1270
+ if (key.toLowerCase().startsWith("prepr-")) {
1271
+ inboundPreprHeaders.push(key);
1272
+ }
1273
+ });
1274
+ for (const key of inboundPreprHeaders) {
1275
+ requestHeaders.delete(key);
1276
+ }
1277
+ const requestCookies = parseCookies(request);
1278
+ const url = new URL(request.url);
1279
+ const searchParams = url.searchParams;
1280
+ const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
1281
+ const isSecure = forwardedProto ? forwardedProto === "https" : url.protocol === "https:";
1282
+ const crossSite = isSecure ? { sameSite: "None", secure: true } : {};
1283
+ for (const key of UTM_PARAMS) {
1284
+ const value = searchParams.get(key);
1285
+ if (value !== null) {
1286
+ requestHeaders.set(`Prepr-Context-${key}`, sanitizeHeaderValue(value));
1287
+ }
1288
+ }
1289
+ const referer = request.headers.get("referer");
1290
+ if (referer) {
1291
+ requestHeaders.set(
1292
+ "Prepr-Context-initial_referral",
1293
+ sanitizeHeaderValue(referer)
1294
+ );
1295
+ }
1296
+ const userAgent = request.headers.get("user-agent");
1297
+ if (userAgent) {
1298
+ requestHeaders.set("User-Agent", sanitizeHeaderValue(userAgent));
1299
+ }
1300
+ const packageParts = [`@preprio/toolkit@${opts?.version ?? VERSION}`];
1301
+ if (framework) packageParts.push(`framework=${framework}`);
1302
+ packageParts.push(`preview=${opts?.preview ? "true" : "false"}`);
1303
+ requestHeaders.set("Prepr-Package", packageParts.join("; "));
1304
+ const ip = request.headers.get("Cf-Connecting-Ip") ?? request.headers.get("x-real-ip");
1305
+ if (ip) {
1306
+ requestHeaders.set("Prepr-Visitor-IP", sanitizeHeaderValue(ip));
1307
+ }
1308
+ const hutkCookie = requestCookies.get(COOKIE_HUBSPOT);
1309
+ if (hutkCookie) {
1310
+ requestHeaders.set("Prepr-Hubspot-Id", sanitizeHeaderValue(hutkCookie));
1311
+ }
1312
+ let uid = sanitizeHeaderValue(requestCookies.get(COOKIE_UID) ?? "");
1313
+ if (!uid) {
1314
+ uid = crypto.randomUUID();
1315
+ responseCookies.push({
1316
+ name: COOKIE_UID,
1317
+ value: uid,
1318
+ maxAge: ONE_YEAR_SECONDS,
1319
+ path: "/"
1320
+ });
1321
+ requestHeaders.set("Prepr-Customer-Id-Created", "true");
1322
+ }
1323
+ requestHeaders.set("Prepr-Customer-Id", uid);
1324
+ if (!opts?.preview) {
1325
+ return { requestHeaders, responseCookies };
1326
+ }
1327
+ const features = resolveFeatures(opts?.features);
1328
+ const hasPreviewParams = features.segments && searchParams.has(PARAM_SEGMENT) || features.abTesting && searchParams.has(PARAM_VARIANT);
1329
+ if (!hasPreviewParams) {
1330
+ const previewModeCookie = requestCookies.get(COOKIE_PREVIEW_MODE);
1331
+ const previewModeEnabled = previewModeCookie !== "false";
1332
+ if (!previewModeEnabled) {
1333
+ return { requestHeaders, responseCookies };
1334
+ }
1335
+ }
1336
+ requestHeaders.set("Prepr-Preview-Bar", "true");
1337
+ const isLivePreview = searchParams.get(PARAM_HIDE_BAR) === "true";
1338
+ if (!isLivePreview && !requestCookies.has(COOKIE_PREVIEW_MODE)) {
1339
+ responseCookies.push({
1340
+ name: COOKIE_PREVIEW_MODE,
1341
+ value: "true",
1342
+ maxAge: ONE_YEAR_SECONDS,
1343
+ path: "/",
1344
+ ...crossSite
1345
+ });
1346
+ }
1347
+ if (!isLivePreview) {
1348
+ const segmentCookie = features.segments ? requestCookies.get(COOKIE_SEGMENT) : void 0;
1349
+ if (segmentCookie) {
1350
+ requestHeaders.set("Prepr-Segments", sanitizeHeaderValue(segmentCookie));
1351
+ }
1352
+ const abCookie = features.abTesting ? requestCookies.get(COOKIE_VARIANT) : void 0;
1353
+ if (abCookie) {
1354
+ requestHeaders.set("Prepr-ABtesting", sanitizeHeaderValue(abCookie));
1355
+ }
1356
+ }
1357
+ const previewAb = features.abTesting ? searchParams.get(PARAM_VARIANT) : null;
1358
+ if (previewAb !== null) {
1359
+ requestHeaders.set("Prepr-ABtesting", sanitizeHeaderValue(previewAb));
1360
+ if (!isLivePreview) {
1361
+ responseCookies.push({
1362
+ name: COOKIE_VARIANT,
1363
+ value: previewAb,
1364
+ maxAge: ONE_YEAR_SECONDS,
1365
+ path: "/",
1366
+ ...crossSite
1367
+ });
1368
+ }
1369
+ }
1370
+ const previewSegment = features.segments ? searchParams.get(PARAM_SEGMENT) : null;
1371
+ if (previewSegment !== null) {
1372
+ requestHeaders.set("Prepr-Segments", sanitizeHeaderValue(previewSegment));
1373
+ if (!isLivePreview) {
1374
+ responseCookies.push({
1375
+ name: COOKIE_SEGMENT,
1376
+ value: previewSegment,
1377
+ maxAge: ONE_YEAR_SECONDS,
1378
+ path: "/",
1379
+ ...crossSite
1380
+ });
1088
1381
  }
1089
- state.__preprAbSheet.textContent = css;
1090
1382
  }
1383
+ if (requestHeaders.has("Prepr-Segments")) {
1384
+ requestHeaders.delete("Prepr-Customer-Id");
1385
+ requestHeaders.delete("Prepr-Customer-Id-Created");
1386
+ }
1387
+ return { requestHeaders, responseCookies };
1091
1388
  }
1092
- function applyPreprDecisions() {
1093
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
1094
- let raw;
1095
- try {
1096
- raw = match ? decodeURIComponent(match[1]) : "";
1097
- } catch {
1098
- raw = "";
1389
+
1390
+ // src/nextjs/middleware.ts
1391
+ function createPreprMiddleware(request, responseOrOptions, maybeOptions) {
1392
+ let chainedResponse;
1393
+ let options;
1394
+ if (responseOrOptions instanceof import_server8.NextResponse) {
1395
+ chainedResponse = responseOrOptions;
1396
+ options = maybeOptions;
1397
+ } else {
1398
+ options = responseOrOptions;
1099
1399
  }
1100
- const state = window;
1101
- if (state.__preprDecisions === raw) return;
1102
- state.__preprDecisions = raw;
1103
- const id = /^[\w-]+$/;
1104
- let css = "";
1105
- raw.split("~").forEach((pair) => {
1106
- const at = pair.indexOf(":");
1107
- const group = pair.slice(0, at);
1108
- if (at < 1 || !id.test(group)) return;
1109
- const selector = '[data-prepr-group="' + group + '"]';
1110
- css += selector + "{display:none!important}";
1111
- pair.slice(at + 1).split("|").forEach((variant) => {
1112
- if (id.test(variant)) {
1113
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
1400
+ const { requestHeaders, responseCookies } = processFrameworkRequest(
1401
+ request,
1402
+ options,
1403
+ "nextjs"
1404
+ );
1405
+ const response = chainedResponse ?? import_server8.NextResponse.next({ request: { headers: requestHeaders } });
1406
+ if (chainedResponse) {
1407
+ const existing = response.headers.get("x-middleware-override-headers");
1408
+ const names = new Set(
1409
+ existing ? existing.split(",").map((name2) => name2.trim()).filter(Boolean) : []
1410
+ );
1411
+ requestHeaders.forEach((value, key) => {
1412
+ names.add(key);
1413
+ response.headers.set(`x-middleware-request-${key}`, value);
1414
+ });
1415
+ response.headers.set("x-middleware-override-headers", [...names].join(","));
1416
+ }
1417
+ for (const cookie of responseCookies) {
1418
+ response.cookies.set(cookie.name, cookie.value, {
1419
+ maxAge: cookie.maxAge,
1420
+ path: cookie.path,
1421
+ // Lower-cased: Next's cookie API expects 'none' | 'lax' | 'strict'.
1422
+ ...cookie.sameSite ? {
1423
+ sameSite: cookie.sameSite.toLowerCase()
1424
+ } : {},
1425
+ ...cookie.secure ? { secure: true } : {}
1426
+ });
1427
+ }
1428
+ return response;
1429
+ }
1430
+
1431
+ // src/nextjs/proxy.ts
1432
+ function isRouterFetch(request) {
1433
+ return request.headers.has("rsc") || request.headers.has("next-url") || request.headers.get("sec-fetch-dest") === "empty";
1434
+ }
1435
+ var STATIC_PAGE_MS = 3e5;
1436
+ var STATIC_PAGES_MAX = 1e3;
1437
+ var staticPages = /* @__PURE__ */ new Map();
1438
+ function isPagePath(pathname) {
1439
+ return !pathname.startsWith("/_next/") && !pathname.startsWith("/api/") && !/\.[^/]*$/.test(pathname);
1440
+ }
1441
+ function createPreprProxy(options) {
1442
+ const internals = preprClientInternals.get(options.client);
1443
+ if (!internals) {
1444
+ throw new PreprError(
1445
+ "createPreprProxy needs a client made by createPreprClient from @preprio/toolkit/nextjs/graphql",
1446
+ "CONTEXT_ERROR",
1447
+ "createPreprProxy"
1448
+ );
1449
+ }
1450
+ const { client } = internals;
1451
+ const preview = options.preview ?? false;
1452
+ const allowed = async (request) => {
1453
+ if (!options.onlyIf) return true;
1454
+ try {
1455
+ return await options.onlyIf(request);
1456
+ } catch {
1457
+ return false;
1458
+ }
1459
+ };
1460
+ const lookup = (target) => {
1461
+ try {
1462
+ return options.page(target) ?? null;
1463
+ } catch {
1464
+ return null;
1465
+ }
1466
+ };
1467
+ async function answerDecision(request, response, target) {
1468
+ let decision = DEFAULT_DECISION;
1469
+ const site = request.headers.get("sec-fetch-site");
1470
+ if (target && request.method === "GET" && (site === null || site === "same-origin") && await allowed(request)) {
1471
+ decision = await decideView(
1472
+ client,
1473
+ mergePreprHeaders(forwardedHeaders(request, response)),
1474
+ target,
1475
+ options.timeoutMs
1476
+ );
1477
+ }
1478
+ const answer = new import_server9.NextResponse(decision, {
1479
+ headers: {
1480
+ "Cache-Control": "private, no-store",
1481
+ "Content-Type": "text/plain; charset=utf-8"
1482
+ }
1483
+ });
1484
+ for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1485
+ return answer;
1486
+ }
1487
+ async function decidePageLoad(request, response, target) {
1488
+ const { basePath, pathname, search } = request.nextUrl;
1489
+ const key = basePath + pathname + search;
1490
+ const now = Date.now();
1491
+ if ((staticPages.get(key) ?? 0) > now) return response;
1492
+ const stored = readStoredDecisions(
1493
+ request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1494
+ );
1495
+ if (!preview && usableDecision(stored, key, now) !== void 0)
1496
+ return response;
1497
+ if (!await allowed(request)) return response;
1498
+ const decision = await decideView(
1499
+ client,
1500
+ mergePreprHeaders(forwardedHeaders(request, response)),
1501
+ target,
1502
+ options.timeoutMs
1503
+ );
1504
+ if (decision === "") {
1505
+ if (staticPages.size >= STATIC_PAGES_MAX) staticPages.clear();
1506
+ staticPages.set(key, now + STATIC_PAGE_MS);
1507
+ return response;
1508
+ }
1509
+ response.cookies.set(
1510
+ PREPR_DECISIONS_COOKIE,
1511
+ writeStoredDecisions(stored, key, decision, now),
1512
+ {
1513
+ path: "/",
1514
+ maxAge: 3600,
1515
+ sameSite: "lax",
1516
+ secure: request.nextUrl.protocol === "https:"
1114
1517
  }
1518
+ );
1519
+ return response;
1520
+ }
1521
+ return async (request) => {
1522
+ const rendersPage = (request.method === "GET" || request.method === "HEAD") && isPagePath(request.nextUrl.pathname);
1523
+ const target = rendersPage ? lookup(request.nextUrl) : null;
1524
+ const decisionRequest = request.headers.has(PREPR_DECIDE_HEADER);
1525
+ if (target && preview && !decisionRequest) {
1526
+ const draft = preprDraftRedirect(request, { preview });
1527
+ if (draft) return draft;
1528
+ }
1529
+ const response = createPreprMiddleware(request, {
1530
+ preview,
1531
+ features: options.features
1532
+ });
1533
+ if (decisionRequest) return answerDecision(request, response, target);
1534
+ if (target && request.method === "GET" && !isRouterFetch(request))
1535
+ return decidePageLoad(request, response, target);
1536
+ return response;
1537
+ };
1538
+ }
1539
+
1540
+ // src/nextjs/stack.ts
1541
+ var import_react = require("react");
1542
+
1543
+ // src/nextjs/decision-switch.ts
1544
+ function applyPreprDecisions(decision, options = {}) {
1545
+ const state = window;
1546
+ const key = location.pathname + location.search;
1547
+ const previous = state.__preprDecision;
1548
+ let value = decision;
1549
+ if (value !== void 0 && value !== "*" && options.refresh && previous && previous.key === key && previous.value !== "*") {
1550
+ const pairs = (from) => {
1551
+ const out = {};
1552
+ from.split("~").forEach((pair) => {
1553
+ const colon = pair.indexOf(":");
1554
+ if (colon > 0) out[pair.slice(0, colon)] = pair;
1555
+ });
1556
+ return out;
1557
+ };
1558
+ const before = pairs(previous.value);
1559
+ const after = pairs(value);
1560
+ document.querySelectorAll("[data-prepr-v][data-prepr-show]").forEach((element) => {
1561
+ const range = document.createRange();
1562
+ range.selectNodeContents(element);
1563
+ if (range.getBoundingClientRect().top >= window.innerHeight) return;
1564
+ (element.getAttribute("data-prepr-v") || "").split(" ").forEach((token) => {
1565
+ const group = token.slice(0, token.indexOf(":"));
1566
+ if (before[group]) after[group] = before[group];
1567
+ });
1115
1568
  });
1569
+ value = Object.keys(after).map((group) => after[group]).join("~");
1570
+ }
1571
+ if (value !== void 0) state.__preprDecision = { key, value };
1572
+ const current = state.__preprDecision;
1573
+ if (!current || current.key !== key) return false;
1574
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
1575
+ marker.setAttribute("data-prepr-decided", "");
1116
1576
  });
1117
- try {
1118
- if (!state.__preprDecisionSheet) {
1119
- const sheet = new CSSStyleSheet();
1120
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1121
- state.__preprDecisionSheet = sheet;
1577
+ const defaults = current.value === "*";
1578
+ const chosen = {};
1579
+ current.value.split("~").forEach((pair) => {
1580
+ const colon = pair.indexOf(":");
1581
+ if (colon > 0)
1582
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
1583
+ });
1584
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
1585
+ const nested = element.hasAttribute("data-prepr-nested");
1586
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
1587
+ const colon = token.indexOf(":");
1588
+ const variant = token.slice(colon + 1);
1589
+ const variants = chosen[token.slice(0, colon)];
1590
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
1591
+ });
1592
+ if (show) element.setAttribute("data-prepr-show", "");
1593
+ else element.removeAttribute("data-prepr-show");
1594
+ });
1595
+ return true;
1596
+ }
1597
+ function requestPreprDecision(key, options = {}) {
1598
+ const fresh = 1e4;
1599
+ const keep = 18e5;
1600
+ const keepStatic = 36e5;
1601
+ const state = window;
1602
+ const asking = state.__preprAsking = state.__preprAsking || {};
1603
+ const read = () => {
1604
+ const all = {};
1605
+ try {
1606
+ const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
1607
+ const parsed = match ? JSON.parse(decodeURIComponent(match[1])) : null;
1608
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
1609
+ Object.keys(parsed).forEach((stored) => {
1610
+ const entry = parsed[stored];
1611
+ if (stored.charAt(0) === "/" && Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number")
1612
+ all[stored] = [entry[0], entry[1]];
1613
+ });
1614
+ } catch {
1122
1615
  }
1123
- state.__preprDecisionSheet.replaceSync(css);
1124
- } catch {
1125
- if (!state.__preprDecisionSheet) {
1126
- state.__preprDecisionSheet = document.createElement("style");
1127
- document.head.appendChild(state.__preprDecisionSheet);
1616
+ return all;
1617
+ };
1618
+ const usable = (entry, now) => {
1619
+ if (!entry) return false;
1620
+ const age = now - entry[1];
1621
+ return age >= 0 && age < (entry[0] === "*" ? fresh : entry[0] === "" ? keepStatic : keep);
1622
+ };
1623
+ const store = (answer) => {
1624
+ const all = read();
1625
+ const now = Date.now();
1626
+ const kept = all[key];
1627
+ const entries = Object.keys(all).filter((stored) => stored !== key && usable(all[stored], now)).map((stored) => [stored, all[stored]]);
1628
+ entries.push([
1629
+ key,
1630
+ answer === "*" && kept && usable(kept, now) && kept[0] !== "*" ? kept : [answer, now]
1631
+ ]);
1632
+ entries.sort((a, b) => b[1][1] - a[1][1]);
1633
+ entries.length = Math.min(entries.length, 20);
1634
+ const encode = () => {
1635
+ const out = {};
1636
+ entries.forEach((entry) => {
1637
+ out[entry[0]] = entry[1];
1638
+ });
1639
+ return encodeURIComponent(JSON.stringify(out));
1640
+ };
1641
+ let value = encode();
1642
+ while (entries.length > 1 && value.length > 3e3) {
1643
+ entries.pop();
1644
+ value = encode();
1645
+ }
1646
+ document.cookie = "prepr_decisions=" + value + "; path=/; max-age=3600; samesite=lax" + (location.protocol === "https:" ? "; secure" : "");
1647
+ };
1648
+ const ask = () => {
1649
+ if (!asking[key]) {
1650
+ asking[key] = fetch(key, {
1651
+ headers: { "Prepr-Decide": "1" },
1652
+ cache: "no-store",
1653
+ credentials: "same-origin",
1654
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1655
+ // covers a proxy that never answers.
1656
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1657
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1658
+ delete asking[key];
1659
+ try {
1660
+ store(answer);
1661
+ } catch {
1662
+ }
1663
+ return answer;
1664
+ });
1128
1665
  }
1129
- state.__preprDecisionSheet.textContent = css;
1666
+ return asking[key];
1667
+ };
1668
+ const last = read()[key];
1669
+ if (last && !(last[0] === "" && options.personalized) && usable(last, Date.now())) {
1670
+ const value = last[0];
1671
+ if (value !== "" && Date.now() - last[1] >= fresh) {
1672
+ ask().then((answer) => {
1673
+ if (answer !== "*" && answer !== value && options.onChange)
1674
+ options.onChange(answer);
1675
+ });
1676
+ }
1677
+ return { result: value, value: Promise.resolve(value) };
1130
1678
  }
1679
+ const pending = {
1680
+ value: ask()
1681
+ };
1682
+ pending.value.then((answer) => {
1683
+ pending.result = answer;
1684
+ });
1685
+ return pending;
1686
+ }
1687
+ function startPreprDecision() {
1688
+ const state = window;
1689
+ const key = location.pathname + location.search;
1690
+ const apply = (answer, refresh) => {
1691
+ if (location.pathname + location.search === key)
1692
+ state.__preprApply(answer, { refresh });
1693
+ };
1694
+ const pending = state.__preprRequest(key, {
1695
+ onChange: (answer) => apply(answer, true),
1696
+ personalized: true
1697
+ });
1698
+ if (pending.result !== void 0) apply(pending.result);
1699
+ else pending.value.then((answer) => apply(answer));
1131
1700
  }
1132
1701
 
1133
1702
  // src/nextjs/stack.ts
1134
1703
  var import_components = require("./components.cjs");
1135
- var AB_ATTRIBUTE = "data-prepr-ab";
1136
- var AB_BUCKETS = ["A", "B"];
1137
- var AB_BASE_CSS = `[${AB_ATTRIBUTE}]{display:contents}[${AB_ATTRIBUTE}]:not([${AB_ATTRIBUTE}="A"]){display:none}`;
1138
- var AB_SWITCH_SCRIPT = `(${applyPreprAbBucket.toString()})();(${applyPreprDecisions.toString()})()`;
1139
- var GROUP_ATTRIBUTE = "data-prepr-group";
1140
- var VARIANT_ATTRIBUTE = "data-prepr-variant";
1704
+ var VARIANT_ATTRIBUTE = "data-prepr-v";
1141
1705
  var DEFAULT_ATTRIBUTE = "data-prepr-default";
1142
- var GROUP_BASE_CSS = `[${GROUP_ATTRIBUTE}]{display:contents}[${GROUP_ATTRIBUTE}]:not([${DEFAULT_ATTRIBUTE}]){display:none}`;
1706
+ var NESTED_ATTRIBUTE = "data-prepr-nested";
1707
+ var GROUP_BASE_CSS = `[${VARIANT_ATTRIBUTE}]{display:none}[${VARIANT_ATTRIBUTE}][data-prepr-show]{display:contents}[data-prepr-decision]:not([data-prepr-decided])~[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents;visibility:hidden}`;
1708
+ var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1709
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1710
+ var APPLY_SCRIPT = "__preprApply()";
1143
1711
  function variantKeysOf(items) {
1144
1712
  return items.flatMap((item) => {
1145
- const key = preprVariantProps(item)["data-prepr-variant-key"];
1713
+ const key = preprVariantProps(item)["data-prepr-variant-key"] ?? nestedKeyOf(item);
1146
1714
  return key ? [key] : [];
1147
1715
  });
1148
1716
  }
1149
- function renderBuckets(items, render, options) {
1150
- const {
1151
- bucket,
1152
- decisions,
1153
- abTest,
1154
- index,
1155
- track,
1156
- placeholder = false
1157
- } = options;
1158
- const renderVariant = (variant, key) => {
1159
- const resolved = resolveVariants(items, { decisions, bucket: variant });
1160
- const content = render(resolved, { index, placeholder });
1161
- const variantKeys = track ? variantKeysOf(resolved) : [];
1162
- const ab = abTest && !bucket ? variant : void 0;
1163
- if (variantKeys.length > 0) {
1164
- return (0, import_react.createElement)(
1165
- import_components.PreprVariantTracking,
1166
- { key, variantKeys, ab },
1167
- content
1168
- );
1717
+ function nestedKeyOf(value) {
1718
+ if (!value || typeof value !== "object") return void 0;
1719
+ for (const child of Object.values(value)) {
1720
+ const items = Array.isArray(child) ? child : [child];
1721
+ for (const item of items) {
1722
+ const key = contextOf(item)?.variant_key ?? (item && nestedKeyOf(item));
1723
+ if (key) return key;
1724
+ }
1725
+ }
1726
+ return void 0;
1727
+ }
1728
+ function renderItems(items, render, options) {
1729
+ const resolved = resolveVariants(items, {
1730
+ decisions: options.decisions,
1731
+ bucket: options.bucket
1732
+ });
1733
+ const content = render(resolved, {
1734
+ index: options.index,
1735
+ defaultVariant: options.defaultVariant
1736
+ });
1737
+ const variantKeys = options.track ? variantKeysOf(resolved) : [];
1738
+ return variantKeys.length > 0 ? (0, import_react.createElement)(import_components.PreprVariantTracking, { variantKeys }, content) : content;
1739
+ }
1740
+ function decidedGroups(items, bucket) {
1741
+ const groups = /* @__PURE__ */ new Map();
1742
+ const walk = (value) => {
1743
+ if (Array.isArray(value)) return value.forEach(walk);
1744
+ if (!value || typeof value !== "object") return;
1745
+ const ctx = contextOf(value);
1746
+ const decided = ctx?.kind === "PERSONALIZATION" || ctx?.kind === "AB_TEST" && !bucket;
1747
+ if (decided && ctx.variant_id) {
1748
+ const group = groups.get(ctx.group_id) ?? {
1749
+ variants: /* @__PURE__ */ new Set(),
1750
+ fallback: ctx.kind === "AB_TEST" ? "A" : PREPR_DEFAULT_VARIANT
1751
+ };
1752
+ group.variants.add(ctx.variant_id);
1753
+ groups.set(ctx.group_id, group);
1169
1754
  }
1170
- if (ab) return (0, import_react.createElement)("div", { key, [AB_ATTRIBUTE]: ab }, content);
1171
- return content;
1755
+ Object.values(value).forEach(walk);
1172
1756
  };
1173
- if (!abTest || bucket) return renderVariant(bucket);
1174
- return AB_BUCKETS.map((variant) => renderVariant(variant, variant));
1175
- }
1176
- function renderAllVariants(block, render, options) {
1177
- const group = block.key.slice("group:".length);
1178
- const variants = [
1179
- ...new Set(
1180
- block.items.flatMap((item) => {
1181
- const ctx = item[PREPR_CONTEXT_ALIAS];
1182
- return ctx?.variant_id ? [ctx.variant_id] : [];
1183
- })
1184
- )
1185
- ];
1186
- return variants.map(
1187
- (variant) => (0, import_react.createElement)(
1757
+ walk(items);
1758
+ return groups;
1759
+ }
1760
+ function renderVariants(block, groups, render, options) {
1761
+ const top = block.key.startsWith("group:") ? block.key.slice("group:".length) : void 0;
1762
+ const nested = !(top !== void 0 && groups.size === 1);
1763
+ const choices = [...groups].map(([group, { variants, fallback }]) => {
1764
+ const ids = nested ? [fallback, ...variants] : [...variants];
1765
+ return [...new Set(ids)].map((variant) => [group, variant]);
1766
+ });
1767
+ const combinations = choices.reduce(
1768
+ (all, pairs) => all.flatMap((combo) => pairs.map((p) => [...combo, p])),
1769
+ [[]]
1770
+ );
1771
+ return combinations.map((combination) => {
1772
+ const pairs = combination.map(([group, variant]) => `${group}:${variant}`);
1773
+ const isDefault = combination.every(
1774
+ ([group, variant]) => variant === groups.get(group).fallback
1775
+ );
1776
+ return (0, import_react.createElement)(
1188
1777
  "div",
1189
1778
  {
1190
- key: variant,
1191
- [GROUP_ATTRIBUTE]: group,
1192
- [VARIANT_ATTRIBUTE]: variant,
1193
- ...variant === PREPR_DEFAULT_VARIANT ? { [DEFAULT_ATTRIBUTE]: "" } : {}
1779
+ key: pairs.join(" "),
1780
+ [VARIANT_ATTRIBUTE]: pairs.join(" "),
1781
+ ...isDefault ? { [DEFAULT_ATTRIBUTE]: "" } : {},
1782
+ ...nested ? { [NESTED_ATTRIBUTE]: "" } : {},
1783
+ // The decision script marks the shown wrapper before hydration.
1784
+ suppressHydrationWarning: true
1194
1785
  },
1195
- renderBuckets(block.items, render, {
1786
+ renderItems(block.items, render, {
1196
1787
  bucket: options.bucket,
1197
- decisions: /* @__PURE__ */ new Map([[group, [variant]]]),
1198
- abTest: false,
1788
+ decisions: new Map(combination.map(([g, v]) => [g, [v]])),
1199
1789
  index: block.index,
1200
- track: options.track
1790
+ track: options.track,
1791
+ defaultVariant: isDefault
1201
1792
  })
1202
- )
1203
- );
1204
- }
1205
- async function PersonalizedBlock({
1206
- items,
1207
- decisions,
1208
- bucket,
1209
- abTest,
1210
- index,
1211
- render,
1212
- track
1213
- }) {
1214
- const decided = await decisions.catch((error) => {
1215
- if (isFrameworkSignal(error)) throw error;
1216
- return void 0;
1217
- });
1218
- return renderBuckets(items, render, {
1219
- bucket,
1220
- decisions: decided,
1221
- abTest,
1222
- index,
1223
- track
1793
+ );
1224
1794
  });
1225
1795
  }
1226
1796
  function PreprStack({
1227
1797
  items,
1228
- decisions,
1229
1798
  render,
1230
1799
  bucket,
1231
1800
  nonce,
1232
- trackVariants = false,
1233
- aboveTheFold = 0
1801
+ trackVariants = false
1234
1802
  }) {
1235
- decisions.catch(() => void 0);
1236
- const blocks = splitStack(items);
1237
- const decidedInProxy = (block) => block.index < aboveTheFold && block.personalized && !block.abTest && block.key.startsWith("group:");
1238
- const hasDecided = blocks.some(decidedInProxy);
1239
- const needsSwitch = !bucket && blocks.some((block) => block.abTest) || hasDecided;
1803
+ const blocks = splitStack(items).map((block) => ({
1804
+ ...block,
1805
+ groups: decidedGroups(block.items, bucket)
1806
+ }));
1807
+ const varies = blocks.some((block) => block.groups.size > 0);
1240
1808
  return (0, import_react.createElement)(
1241
1809
  import_react.Fragment,
1242
1810
  null,
1243
- needsSwitch && (0, import_react.createElement)(
1811
+ varies && (0, import_react.createElement)(
1244
1812
  import_react.Fragment,
1245
- { key: "prepr-ab" },
1813
+ { key: "prepr-switch" },
1246
1814
  (0, import_react.createElement)("style", {
1247
1815
  nonce,
1248
- dangerouslySetInnerHTML: {
1249
- __html: AB_BASE_CSS + (hasDecided ? GROUP_BASE_CSS : "")
1250
- }
1816
+ dangerouslySetInnerHTML: { __html: GROUP_BASE_CSS }
1817
+ }),
1818
+ (0, import_react.createElement)("noscript", {
1819
+ dangerouslySetInnerHTML: { __html: NOSCRIPT_HTML }
1251
1820
  }),
1252
1821
  (0, import_react.createElement)("script", {
1253
1822
  nonce,
1254
- dangerouslySetInnerHTML: { __html: AB_SWITCH_SCRIPT }
1823
+ dangerouslySetInnerHTML: { __html: DECISION_SCRIPT }
1255
1824
  }),
1256
- (0, import_react.createElement)(import_components.PreprAbSwitch)
1825
+ (0, import_react.createElement)(import_components.PreprDecisionApplier),
1826
+ // Marked decided by the decision switch; never laid out.
1827
+ (0, import_react.createElement)("template", {
1828
+ "data-prepr-decision": "",
1829
+ suppressHydrationWarning: true
1830
+ })
1257
1831
  ),
1258
1832
  ...blocks.map((block) => {
1259
- if (decidedInProxy(block)) {
1833
+ if (block.groups.size === 0) {
1260
1834
  return (0, import_react.createElement)(
1261
1835
  import_react.Fragment,
1262
1836
  { key: block.key },
1263
- renderAllVariants(block, render, { bucket, track: trackVariants })
1837
+ renderItems(block.items, render, {
1838
+ bucket,
1839
+ index: block.index,
1840
+ track: trackVariants,
1841
+ defaultVariant: true
1842
+ })
1264
1843
  );
1265
1844
  }
1266
- const fallback = renderBuckets(block.items, render, {
1267
- bucket,
1268
- abTest: block.abTest,
1269
- index: block.index,
1270
- track: trackVariants && !block.personalized,
1271
- placeholder: block.personalized
1272
- });
1273
- if (!block.personalized) {
1274
- return (0, import_react.createElement)(import_react.Fragment, { key: block.key }, fallback);
1275
- }
1276
1845
  return (0, import_react.createElement)(
1277
- import_react.Suspense,
1278
- { key: block.key, fallback },
1279
- (0, import_react.createElement)(PersonalizedBlock, {
1280
- items: block.items,
1281
- decisions,
1846
+ import_react.Fragment,
1847
+ { key: block.key },
1848
+ renderVariants(block, block.groups, render, {
1282
1849
  bucket,
1283
- abTest: block.abTest,
1284
- index: block.index,
1285
- render,
1286
1850
  track: trackVariants
1851
+ }),
1852
+ (0, import_react.createElement)("script", {
1853
+ nonce,
1854
+ dangerouslySetInnerHTML: { __html: APPLY_SCRIPT }
1287
1855
  })
1288
1856
  );
1289
1857
  })
@@ -1295,33 +1863,59 @@ function createPreprClient2(options) {
1295
1863
  const { cacheTags = ["prepr"], revalidate, ...coreOptions } = options;
1296
1864
  const client = createPreprClient({
1297
1865
  // The schema introspection goes through the Data Cache: shared across
1298
- // server instances, refreshed hourly and by revalidateTag on publish.
1866
+ // server instances, refreshed hourly and by a `schema.changed` webhook.
1299
1867
  schemaInit: {
1300
- next: { tags: cacheTags, revalidate: 3600 }
1868
+ next: { tags: [...cacheTags, PREPR_SCHEMA_TAG], revalidate: 3600 }
1301
1869
  },
1302
1870
  ...coreOptions
1303
1871
  });
1304
- const seenGroups = /* @__PURE__ */ new Map();
1305
- return {
1872
+ const nextClient = {
1306
1873
  async query(document2, variables, queryOptions = {}) {
1307
- const { personalize = false, ...rest } = queryOptions;
1308
- const init = rest.init;
1309
- const tagged = {
1874
+ const { personalize = false, tagItems = false, ...rest } = queryOptions;
1875
+ const { next, ...init } = rest.init ?? {};
1876
+ if (tagItems && typeof nextCache.cacheTag !== "function") {
1877
+ throw new Error("tagItems requires Next.js 16 or later.");
1878
+ }
1879
+ const options2 = {
1310
1880
  ...rest,
1311
- init: {
1881
+ // With tagItems, `next` is dropped too: tags or revalidate alongside
1882
+ // `no-store` are a conflict Next.js resolves by ignoring the store.
1883
+ // Otherwise tags are attached unconditionally; the core client drops
1884
+ // them again whenever the request ends up `no-store` (personalized,
1885
+ // per-request headers, or preview).
1886
+ init: tagItems ? { ...init, cache: "no-store" } : {
1312
1887
  ...init,
1313
1888
  next: {
1314
- tags: cacheTags,
1315
1889
  ...revalidate === void 0 ? {} : { revalidate },
1316
- ...init?.next
1890
+ ...next,
1891
+ // Merged, so a per-query tag never drops the client's own,
1892
+ // which the publish webhook relies on.
1893
+ tags: [...cacheTags, ...next?.tags ?? []]
1317
1894
  }
1318
1895
  }
1319
1896
  };
1320
- if (!personalize) return client.query(document2, variables, tagged);
1321
- return client.query(document2, variables, {
1322
- ...tagged,
1323
- headers: mergePreprHeaders(await getPreprHeaders(), rest.headers)
1324
- });
1897
+ if (personalize) {
1898
+ options2.headers = mergePreprHeaders(
1899
+ await getPreprHeaders(),
1900
+ rest.headers
1901
+ );
1902
+ }
1903
+ const result = await client.query(document2, variables, options2);
1904
+ if (tagItems) {
1905
+ let modelTags;
1906
+ try {
1907
+ modelTags = preprModelTagsFor(
1908
+ documentOf(document2),
1909
+ result.data,
1910
+ await clientSchemas.get(client)()
1911
+ );
1912
+ } catch {
1913
+ modelTags = [PREPR_ANY_ITEM_TAG];
1914
+ }
1915
+ nextCache.cacheTag(...cacheTags, ...next?.tags ?? [], ...modelTags);
1916
+ nextCache.cacheTag(...preprCacheTags(result.data));
1917
+ }
1918
+ return result;
1325
1919
  },
1326
1920
  async decide(document2, variables, queryOptions = {}) {
1327
1921
  return client.decide(document2, variables, {
@@ -1332,28 +1926,24 @@ function createPreprClient2(options) {
1332
1926
  )
1333
1927
  });
1334
1928
  },
1335
- hasAbTests: (document2, variables) => client.hasAbTests(document2, variables),
1336
- resolveDecisions: (request, response, document2, variables, options2) => resolveDecisionsWith(
1337
- client,
1338
- seenGroups,
1339
- request,
1340
- response,
1341
- document2,
1342
- variables,
1343
- options2
1344
- )
1929
+ hasAbTests: (document2, variables) => client.hasAbTests(document2, variables)
1345
1930
  };
1931
+ preprClientInternals.set(nextClient, { client });
1932
+ return nextClient;
1346
1933
  }
1347
1934
  // Annotate the CommonJS export names for ESM import in node:
1348
1935
  0 && (module.exports = {
1349
- PREPR_DECISIONS_COOKIE,
1936
+ PREPR_ANY_ITEM_TAG,
1350
1937
  PREPR_DEFAULT_VARIANT,
1351
1938
  PreprError,
1352
1939
  PreprStack,
1353
1940
  createPreprClient,
1941
+ createPreprProxy,
1354
1942
  decisionsFrom,
1355
1943
  isPersonalizedResponse,
1356
1944
  loadPreprSchema,
1945
+ preprCacheTags,
1946
+ preprModelTag,
1357
1947
  preprVariantProps,
1358
1948
  resolveVariants,
1359
1949
  splitStack,