@preprio/toolkit 0.6.0-alpha.5 → 0.6.0-alpha.7

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 (52) hide show
  1. package/README.md +138 -52
  2. package/dist/astro/{chunk-CD4LXNR3.js → chunk-3EXK2LHI.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-5O7RGYHX.js → chunk-3QGWCYTL.js} +1 -1
  10. package/dist/{chunk-ZWKHACNA.js → chunk-PFJO4TYL.js} +52 -5
  11. package/dist/{chunk-HPWTLDLR.js → chunk-U7TRFVK7.js} +1 -1
  12. package/dist/{chunk-OH2PPC76.js → chunk-VQIPTUN4.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-L4L6X3JJ.js → chunk-CZK7SMBD.js} +58 -53
  24. package/dist/nextjs/components.cjs +175 -242
  25. package/dist/nextjs/components.d.cts +25 -21
  26. package/dist/nextjs/components.d.ts +25 -21
  27. package/dist/nextjs/components.js +167 -236
  28. package/dist/nextjs/graphql.cjs +374 -510
  29. package/dist/nextjs/graphql.d.cts +131 -135
  30. package/dist/nextjs/graphql.d.ts +131 -135
  31. package/dist/nextjs/graphql.js +309 -457
  32. package/dist/nextjs/index.cjs +114 -54
  33. package/dist/nextjs/index.d.cts +57 -33
  34. package/dist/nextjs/index.d.ts +57 -33
  35. package/dist/nextjs/index.js +108 -5
  36. package/dist/nuxt/{chunk-PJZMLKJ2.js → chunk-JL33ENCM.js} +1 -1
  37. package/dist/nuxt/graphql.cjs +58 -10
  38. package/dist/nuxt/graphql.d.cts +33 -38
  39. package/dist/nuxt/graphql.d.ts +33 -38
  40. package/dist/nuxt/graphql.js +59 -11
  41. package/dist/nuxt/index.cjs +1 -1
  42. package/dist/nuxt/index.js +1 -1
  43. package/dist/react/components.cjs +1 -1
  44. package/dist/react/components.js +1 -1
  45. package/dist/sveltekit/{chunk-SKBW7RBD.js → chunk-S7S276V7.js} +1 -1
  46. package/dist/sveltekit/graphql.cjs +58 -10
  47. package/dist/sveltekit/graphql.d.cts +33 -38
  48. package/dist/sveltekit/graphql.d.ts +33 -38
  49. package/dist/sveltekit/graphql.js +59 -11
  50. package/dist/sveltekit/index.cjs +1 -1
  51. package/dist/sveltekit/index.js +1 -1
  52. package/package.json +1 -1
@@ -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,12 +17,20 @@ 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,
@@ -29,6 +39,8 @@ __export(graphql_exports, {
29
39
  decisionsFrom: () => decisionsFrom,
30
40
  isPersonalizedResponse: () => isPersonalizedResponse,
31
41
  loadPreprSchema: () => loadPreprSchema,
42
+ preprCacheTags: () => preprCacheTags,
43
+ preprModelTag: () => preprModelTag,
32
44
  preprVariantProps: () => preprVariantProps,
33
45
  resolveVariants: () => resolveVariants,
34
46
  splitStack: () => splitStack,
@@ -36,9 +48,60 @@ __export(graphql_exports, {
36
48
  toVariantsQuery: () => toVariantsQuery
37
49
  });
38
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
+ }
39
102
 
40
103
  // src/version.ts
41
- var VERSION = "0.6.0-alpha.5";
104
+ var VERSION = "0.6.0-alpha.7";
42
105
 
43
106
  // src/core/features.ts
44
107
  function isEnabled(config) {
@@ -202,7 +265,10 @@ function decisionSelectionSet(set, typeName, path, ctx) {
202
265
  path,
203
266
  ctx
204
267
  );
205
- if (pruned) {
268
+ if (!pruned) continue;
269
+ if (fragment.typeCondition.name.value === typeName) {
270
+ selections.push(...pruned.selections);
271
+ } else {
206
272
  selections.push(
207
273
  inlineFragment(fragment.typeCondition.name.value, [
208
274
  ...pruned.selections
@@ -212,13 +278,19 @@ function decisionSelectionSet(set, typeName, path, ctx) {
212
278
  continue;
213
279
  }
214
280
  if (selection.kind === "InlineFragment") {
281
+ const condition = selection.typeCondition?.name.value ?? typeName;
215
282
  const pruned = decisionSelectionSet(
216
283
  selection.selectionSet,
217
- selection.typeCondition?.name.value ?? typeName,
284
+ condition,
218
285
  path,
219
286
  ctx
220
287
  );
221
- 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
+ }
222
294
  continue;
223
295
  }
224
296
  if (!selection.selectionSet) continue;
@@ -292,20 +364,58 @@ function toDecisionDocument(document2, schema, groupPaths, options) {
292
364
  const used = /* @__PURE__ */ new Set();
293
365
  collectVariables(pruned, used);
294
366
  collectVariables(operation.directives, used);
367
+ const shared = sharedContextFragment(schema);
295
368
  return {
296
369
  kind: "Document",
297
370
  definitions: [
371
+ ...shared ? [shared.definition] : [],
298
372
  {
299
373
  ...operation,
300
374
  name: operation.name && name(`${operation.name.value}Decision`),
301
375
  variableDefinitions: (operation.variableDefinitions ?? []).filter(
302
376
  (definition) => used.has(definition.variable.name.value)
303
377
  ),
304
- selectionSet: pruned
378
+ selectionSet: shared ? shared.apply(pruned) : pruned
305
379
  }
306
380
  ]
307
381
  };
308
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
+ }
309
419
 
310
420
  // src/core/variants/probe-document.ts
311
421
  function probeSelectionSet(set, fragments) {
@@ -886,6 +996,7 @@ function createVaryProbe(run, options) {
886
996
  }
887
997
 
888
998
  // src/core/graphql/client.ts
999
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
889
1000
  function isParseableUrl(url) {
890
1001
  try {
891
1002
  new URL(url);
@@ -922,7 +1033,7 @@ function createPreprClient(options) {
922
1033
  ));
923
1034
  const variantsDocuments = /* @__PURE__ */ new Map();
924
1035
  const decisionDocuments = /* @__PURE__ */ new Map();
925
- return {
1036
+ const client = {
926
1037
  async query(document2, variables, queryOptions = {}) {
927
1038
  const { variants, ...rest } = queryOptions;
928
1039
  if (!variants) return run(document2, variables, rest);
@@ -934,10 +1045,10 @@ function createPreprClient(options) {
934
1045
  return run(derived, variables, rest);
935
1046
  },
936
1047
  async decide(document2, variables, queryOptions = {}) {
937
- const { from, topLevelOnly, ...rest } = queryOptions;
1048
+ const { from, ...rest } = queryOptions;
938
1049
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
939
1050
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
940
- 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("|") : "*";
941
1052
  let byPaths = decisionDocuments.get(document2);
942
1053
  if (!byPaths) {
943
1054
  byPaths = /* @__PURE__ */ new Map();
@@ -945,9 +1056,7 @@ function createPreprClient(options) {
945
1056
  }
946
1057
  let derived = byPaths.get(signature);
947
1058
  if (derived === void 0) {
948
- derived = toDecisionDocument(document2, await loadSchema(), paths, {
949
- topLevelOnly: !paths && topLevelOnly
950
- });
1059
+ derived = toDecisionDocument(document2, await loadSchema(), paths);
951
1060
  byPaths.set(signature, derived);
952
1061
  }
953
1062
  if (!derived) return /* @__PURE__ */ new Map();
@@ -961,6 +1070,8 @@ function createPreprClient(options) {
961
1070
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
962
1071
  )
963
1072
  };
1073
+ clientSchemas.set(client, loadSchema);
1074
+ return client;
964
1075
  }
965
1076
 
966
1077
  // src/core/graphql/schema-loader.ts
@@ -987,69 +1098,15 @@ async function loadPreprSchema(url, options = {}) {
987
1098
  return schema;
988
1099
  }
989
1100
 
990
- // src/core/refresh.ts
991
- var PREPR_REFRESH_COOKIE = "prepr_refresh";
992
- var PREPR_REFRESH_HEADER = "Prepr-Refresh";
993
- var REFRESH_MAX_PATHS = 5;
994
- var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
995
- function safePath(path) {
996
- return SAFE_PATH.test(path) ? path : void 0;
997
- }
998
- function isFresh(at, freshFor, now = Date.now()) {
999
- const age = now - at;
1000
- return at > 0 && age >= 0 && age < freshFor;
1001
- }
1002
- function cleanEntries(entries) {
1003
- const out = [];
1004
- for (const entry of entries) {
1005
- if (out.length === REFRESH_MAX_PATHS) break;
1006
- if (safePath(entry.path) && !out.some((e) => e.path === entry.path)) {
1007
- out.push(entry);
1008
- }
1009
- }
1010
- return out;
1011
- }
1012
- function parseRefresh(raw) {
1013
- return cleanEntries(
1014
- (raw ?? "").split("|").map((pair) => {
1015
- const split = pair.lastIndexOf("~");
1016
- const at = Number(split < 0 ? "" : pair.slice(split + 1));
1017
- return {
1018
- path: split < 0 ? pair : pair.slice(0, split),
1019
- at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
1020
- };
1021
- })
1022
- );
1023
- }
1024
- function serializeRefresh(entries) {
1025
- return cleanEntries(entries).map((entry) => `${entry.path}~${Math.floor(entry.at)}`).join("|");
1026
- }
1027
- function parseRefreshPaths(header) {
1028
- return cleanEntries(
1029
- (header ?? "").split("|").map((path) => ({ path, at: 0 }))
1030
- ).map((entry) => entry.path);
1031
- }
1032
-
1033
1101
  // src/nextjs/decisions.ts
1034
- var PREPR_DECISIONS_COOKIE = "prepr_decisions";
1102
+ var PREPR_DECIDE_HEADER = "Prepr-Decide";
1103
+ var DEFAULT_DECISION = "*";
1035
1104
  var DECISIONS_MAX_GROUPS = 40;
1036
1105
  var SAFE_ID = /^[\w-]+$/;
1037
- var SEEN_MAX_PAGES = 1e3;
1038
- function parseDecisions(raw) {
1039
- const out = /* @__PURE__ */ new Map();
1040
- for (const pair of (raw ?? "").split("~")) {
1041
- const at = pair.indexOf(":");
1042
- const group = pair.slice(0, at);
1043
- if (at < 1 || !SAFE_ID.test(group)) continue;
1044
- out.set(
1045
- group,
1046
- pair.slice(at + 1).split("|").filter((variant) => SAFE_ID.test(variant))
1047
- );
1048
- }
1049
- return out;
1050
- }
1051
1106
  function serializeDecisions(decisions) {
1052
- return [...decisions].slice(0, DECISIONS_MAX_GROUPS).map(([group, variants]) => `${group}:${variants.join("|")}`).join("~");
1107
+ return [...decisions].filter(([group]) => SAFE_ID.test(group)).slice(0, DECISIONS_MAX_GROUPS).map(
1108
+ ([group, variants]) => `${group}:${variants.filter((v) => SAFE_ID.test(v)).join("|")}`
1109
+ ).join("~");
1053
1110
  }
1054
1111
  function forwardedHeaders(request, response) {
1055
1112
  const overridden = response.headers.get("x-middleware-override-headers");
@@ -1062,109 +1119,16 @@ function forwardedHeaders(request, response) {
1062
1119
  return getPreprHeadersFromHeaders(headers2);
1063
1120
  }
1064
1121
  var preprClientInternals = /* @__PURE__ */ new WeakMap();
1065
- var DEFAULT_FRESH_FOR_MS = 1e4;
1066
- async function decidePage(client, seenGroups, headers2, document2, variables, timeoutMs = 1e3) {
1067
- if (!await client.hasSegments(document2, variables)) return void 0;
1068
- const decided = await client.decide(document2, variables, {
1069
- topLevelOnly: true,
1070
- headers: headers2,
1071
- init: { signal: AbortSignal.timeout(timeoutMs) }
1072
- });
1073
- let byVariables = seenGroups.get(document2);
1074
- if (!byVariables) {
1075
- byVariables = /* @__PURE__ */ new Map();
1076
- seenGroups.set(document2, byVariables);
1077
- }
1078
- const pageKey = JSON.stringify(variables ?? null);
1079
- let groups = byVariables.get(pageKey);
1080
- if (!groups) {
1081
- if (byVariables.size >= SEEN_MAX_PAGES) {
1082
- byVariables.delete(byVariables.keys().next().value);
1083
- }
1084
- groups = /* @__PURE__ */ new Set();
1085
- byVariables.set(pageKey, groups);
1086
- }
1087
- for (const group of decided.keys()) groups.add(group);
1088
- return decided;
1089
- }
1090
- function writeDecisions(request, response, decided, paths) {
1091
- const next = /* @__PURE__ */ new Map();
1092
- for (const decisions of decided) {
1093
- for (const [group, variants] of decisions) {
1094
- if (SAFE_ID.test(group) && !next.has(group)) {
1095
- next.set(
1096
- group,
1097
- variants.filter((v) => SAFE_ID.test(v))
1098
- );
1099
- }
1100
- }
1101
- }
1102
- for (const [group, variants] of parseDecisions(
1103
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1104
- )) {
1105
- if (!next.has(group)) next.set(group, variants);
1106
- }
1107
- const options = {
1108
- path: "/",
1109
- sameSite: "lax",
1110
- httpOnly: false,
1111
- secure: request.nextUrl.protocol === "https:"
1112
- };
1113
- if (next.size > 0) {
1114
- response.cookies.set(
1115
- PREPR_DECISIONS_COOKIE,
1116
- serializeDecisions(next),
1117
- options
1118
- );
1119
- }
1120
- if (paths.length > 0) {
1121
- const at = Date.now();
1122
- response.cookies.set(
1123
- PREPR_REFRESH_COOKIE,
1124
- serializeRefresh([
1125
- ...paths.map((path) => ({ path, at })),
1126
- ...parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value)
1127
- ]),
1128
- options
1129
- );
1130
- }
1131
- }
1132
- function pagePath(request) {
1133
- return request.nextUrl.basePath + request.nextUrl.pathname;
1134
- }
1135
- function contextChanged(request, headers2) {
1136
- return headers2.get("Prepr-Customer-Id-Created") === "true" || headers2.has("Prepr-Segments") || [...request.nextUrl.searchParams.keys()].some(
1137
- (key) => key.startsWith("utm_")
1138
- );
1139
- }
1140
- async function resolveDecisionsWith(client, seenGroups, request, response, document2, variables, options) {
1122
+ async function decideView(client, headers2, page, timeoutMs = 3e3) {
1123
+ const init = { signal: AbortSignal.timeout(timeoutMs) };
1141
1124
  try {
1142
- if (options?.onlyIf && !await options.onlyIf()) return;
1143
- const known = seenGroups.get(document2)?.get(JSON.stringify(variables ?? null));
1144
- const path = pagePath(request);
1145
- const headers2 = mergePreprHeaders(forwardedHeaders(request, response));
1146
- const stored = parseDecisions(
1147
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1148
- );
1149
- const decidedAt = parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value).find(
1150
- (entry) => entry.path === path
1151
- )?.at ?? 0;
1152
- const fresh = isFresh(decidedAt, options?.freshFor ?? DEFAULT_FRESH_FOR_MS);
1153
- if (fresh && known && !contextChanged(request, headers2) && [...known].every((group) => stored.has(group))) {
1154
- return;
1155
- }
1156
- const decided = await decidePage(
1157
- client,
1158
- seenGroups,
1159
- headers2,
1160
- document2,
1161
- variables,
1162
- options?.timeoutMs
1163
- );
1164
- if (!decided || decided.size === 0) return;
1165
- writeDecisions(request, response, [decided], [path]);
1125
+ const decided = page.decision ? decisionsFrom(
1126
+ (await client.query(page.decision, page.variables, { headers: headers2, init })).data
1127
+ ) : await client.decide(page.document, page.variables, { headers: headers2, init });
1128
+ return serializeDecisions(decided);
1166
1129
  } catch (error) {
1167
1130
  if (isFrameworkSignal(error)) throw error;
1131
+ return DEFAULT_DECISION;
1168
1132
  }
1169
1133
  }
1170
1134
 
@@ -1181,66 +1145,6 @@ async function getPreprHeaders() {
1181
1145
  // src/nextjs/proxy.ts
1182
1146
  var import_server9 = require("next/server");
1183
1147
 
1184
- // src/core/constants.ts
1185
- var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
1186
- var COOKIE_SEGMENT = "Prepr-Segments";
1187
- var COOKIE_VARIANT = "Prepr-ABtesting";
1188
- var COOKIE_UID = "__prepr_uid";
1189
- var COOKIE_HUBSPOT = "hubspotutk";
1190
- var PARAM_SEGMENT = "prepr_preview_segment";
1191
- var PARAM_VARIANT = "prepr_preview_ab";
1192
- var PARAM_HIDE_BAR = "prepr_hide_bar";
1193
- var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
1194
-
1195
- // src/nextjs/bucket.ts
1196
- var PREPR_BUCKET_COOKIE = "prepr_ab_bucket";
1197
- var isBucket = (value) => value === "A" || value === "B";
1198
- function setBucketCookie(request, response, bucket) {
1199
- response.cookies.set(PREPR_BUCKET_COOKIE, bucket, {
1200
- path: "/",
1201
- sameSite: "lax",
1202
- // Readable on purpose: PreprStack's inline switch reads it before first
1203
- // paint. It holds only "A" or "B".
1204
- httpOnly: false,
1205
- secure: request.nextUrl.protocol === "https:",
1206
- maxAge: 60 * 60 * 24 * 365
1207
- });
1208
- }
1209
- async function resolvePreprBucket(request, response, options) {
1210
- const stored = request.cookies.get(PREPR_BUCKET_COOKIE)?.value;
1211
- const forced = response.headers.get("x-middleware-request-prepr-abtesting") ?? request.cookies.get(COOKIE_VARIANT)?.value;
1212
- if (options.preview && isBucket(forced)) {
1213
- if (forced !== stored) setBucketCookie(request, response, forced);
1214
- return forced;
1215
- }
1216
- if (isBucket(stored)) return stored;
1217
- if (options.onlyIf) {
1218
- const needed = await Promise.resolve().then(options.onlyIf).catch(() => true);
1219
- if (!needed) return "A";
1220
- }
1221
- const customerId = request.cookies.get(COOKIE_UID)?.value ?? response.cookies.get(COOKIE_UID)?.value;
1222
- if (!customerId) return "A";
1223
- try {
1224
- const res = await fetch(options.url, {
1225
- method: "POST",
1226
- headers: {
1227
- "Content-Type": "application/json",
1228
- "Prepr-Customer-Id": customerId
1229
- },
1230
- body: JSON.stringify({ query: "{ __typename }" }),
1231
- cache: "no-store",
1232
- signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
1233
- });
1234
- const bucket = res.headers.get("x-prepr-customer-bucket");
1235
- if (isBucket(bucket)) {
1236
- setBucketCookie(request, response, bucket);
1237
- return bucket;
1238
- }
1239
- } catch {
1240
- }
1241
- return "A";
1242
- }
1243
-
1244
1148
  // src/nextjs/draft.ts
1245
1149
  var import_headers2 = require("next/headers");
1246
1150
  var import_navigation = require("next/navigation");
@@ -1268,6 +1172,17 @@ function preprDraftRedirect(request, options) {
1268
1172
  // src/nextjs/middleware.ts
1269
1173
  var import_server8 = require("next/server");
1270
1174
 
1175
+ // src/core/constants.ts
1176
+ var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
1177
+ var COOKIE_SEGMENT = "Prepr-Segments";
1178
+ var COOKIE_VARIANT = "Prepr-ABtesting";
1179
+ var COOKIE_UID = "__prepr_uid";
1180
+ var COOKIE_HUBSPOT = "hubspotutk";
1181
+ var PARAM_SEGMENT = "prepr_preview_segment";
1182
+ var PARAM_VARIANT = "prepr_preview_ab";
1183
+ var PARAM_HIDE_BAR = "prepr_hide_bar";
1184
+ var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
1185
+
1271
1186
  // src/core/middleware.ts
1272
1187
  var UTM_PARAMS = [
1273
1188
  "utm_source",
@@ -1478,7 +1393,7 @@ function createPreprProxy(options) {
1478
1393
  "createPreprProxy"
1479
1394
  );
1480
1395
  }
1481
- const { client, seenGroups, url } = internals;
1396
+ const { client } = internals;
1482
1397
  const preview = options.preview ?? false;
1483
1398
  const allowed = async (request) => {
1484
1399
  if (!options.onlyIf) return true;
@@ -1495,65 +1410,31 @@ function createPreprProxy(options) {
1495
1410
  return null;
1496
1411
  }
1497
1412
  };
1498
- async function refresh(request, response, paths) {
1499
- const answer = new import_server9.NextResponse(null, {
1500
- status: 204,
1501
- headers: { "Cache-Control": "no-store" }
1502
- });
1503
- for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1413
+ async function answerDecision(request, response, target) {
1414
+ let decision = DEFAULT_DECISION;
1504
1415
  const site = request.headers.get("sec-fetch-site");
1505
- if (request.method !== "GET" || site !== null && site !== "same-origin" || !await allowed(request)) {
1506
- return answer;
1416
+ if (target && request.method === "GET" && (site === null || site === "same-origin") && await allowed(request)) {
1417
+ decision = await decideView(
1418
+ client,
1419
+ mergePreprHeaders(forwardedHeaders(request, response)),
1420
+ target,
1421
+ options.timeoutMs
1422
+ );
1507
1423
  }
1508
- const own = new Set(
1509
- parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value).map(
1510
- (entry) => entry.path
1511
- )
1512
- );
1513
- const basePath = request.nextUrl.basePath;
1514
- const headers2 = mergePreprHeaders(forwardedHeaders(request, response));
1515
- const decided = await Promise.all(
1516
- paths.map(async (path) => {
1517
- if (!own.has(path) || !path.startsWith(basePath)) return void 0;
1518
- const pathname = path.slice(basePath.length) || "/";
1519
- if (!isPagePath(pathname)) return void 0;
1520
- const target = lookup(new URL(pathname, request.url));
1521
- if (!target) return void 0;
1522
- try {
1523
- const decisions = await decidePage(
1524
- client,
1525
- seenGroups,
1526
- headers2,
1527
- target.document,
1528
- target.variables,
1529
- options.timeoutMs
1530
- );
1531
- return decisions?.size ? [path, decisions] : void 0;
1532
- } catch (error) {
1533
- if (isFrameworkSignal(error)) throw error;
1534
- return void 0;
1535
- }
1536
- })
1537
- );
1538
- const found = decided.filter((entry) => entry !== void 0);
1539
- writeDecisions(
1540
- request,
1541
- answer,
1542
- found.map(([, decisions]) => decisions),
1543
- found.map(([path]) => path)
1544
- );
1424
+ const answer = new import_server9.NextResponse(decision, {
1425
+ headers: {
1426
+ "Cache-Control": "private, no-store",
1427
+ "Content-Type": "text/plain; charset=utf-8"
1428
+ }
1429
+ });
1430
+ for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1545
1431
  return answer;
1546
1432
  }
1547
1433
  return async (request) => {
1548
- if (request.headers.has(PREPR_REFRESH_HEADER)) {
1549
- return refresh(
1550
- request,
1551
- createPreprMiddleware(request, { preview, features: options.features }),
1552
- parseRefreshPaths(request.headers.get(PREPR_REFRESH_HEADER))
1553
- );
1554
- }
1555
- const target = isPagePath(request.nextUrl.pathname) ? lookup(request.nextUrl) : null;
1556
- if (target && preview) {
1434
+ const rendersPage = (request.method === "GET" || request.method === "HEAD") && isPagePath(request.nextUrl.pathname);
1435
+ const target = rendersPage ? lookup(request.nextUrl) : null;
1436
+ const decisionRequest = request.headers.has(PREPR_DECIDE_HEADER);
1437
+ if (target && preview && !decisionRequest) {
1557
1438
  const draft = preprDraftRedirect(request, { preview });
1558
1439
  if (draft) return draft;
1559
1440
  }
@@ -1561,24 +1442,7 @@ function createPreprProxy(options) {
1561
1442
  preview,
1562
1443
  features: options.features
1563
1444
  });
1564
- if (target && await allowed(request)) {
1565
- await Promise.all([
1566
- resolvePreprBucket(request, response, {
1567
- url,
1568
- preview,
1569
- onlyIf: () => options.client.hasAbTests(target.document, target.variables)
1570
- }),
1571
- resolveDecisionsWith(
1572
- client,
1573
- seenGroups,
1574
- request,
1575
- response,
1576
- target.document,
1577
- target.variables,
1578
- { freshFor: options.freshFor, timeoutMs: options.timeoutMs }
1579
- )
1580
- ]);
1581
- }
1445
+ if (decisionRequest) return answerDecision(request, response, target);
1582
1446
  return response;
1583
1447
  };
1584
1448
  }
@@ -1586,235 +1450,220 @@ function createPreprProxy(options) {
1586
1450
  // src/nextjs/stack.ts
1587
1451
  var import_react = require("react");
1588
1452
 
1589
- // src/nextjs/ab-switch.ts
1590
- function applyPreprAbBucket() {
1591
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
1592
- const bucket = match ? match[1] : "A";
1593
- const state = window;
1594
- if (state.__preprAbBucket === bucket) return;
1595
- state.__preprAbBucket = bucket;
1596
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
1597
- try {
1598
- if (!state.__preprAbSheet) {
1599
- const sheet = new CSSStyleSheet();
1600
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1601
- state.__preprAbSheet = sheet;
1602
- }
1603
- state.__preprAbSheet.replaceSync(css);
1604
- } catch {
1605
- if (!state.__preprAbSheet) {
1606
- state.__preprAbSheet = document.createElement("style");
1607
- document.head.appendChild(state.__preprAbSheet);
1608
- }
1609
- state.__preprAbSheet.textContent = css;
1610
- }
1611
- }
1612
- function applyPreprDecisions(view) {
1613
- const viewState = window;
1614
- const current = view === void 0 ? location.pathname + location.search : view;
1615
- if (viewState.__preprDecisionsView === current) return;
1616
- viewState.__preprDecisionsView = current;
1617
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
1618
- let raw;
1619
- try {
1620
- raw = match ? decodeURIComponent(match[1]) : "";
1621
- } catch {
1622
- raw = "";
1623
- }
1453
+ // src/nextjs/decision-switch.ts
1454
+ function applyPreprDecisions(decision) {
1624
1455
  const state = window;
1625
- if (state.__preprDecisions === raw) return;
1626
- state.__preprDecisions = raw;
1627
- const id = /^[\w-]+$/;
1628
- let css = "";
1629
- raw.split("~").forEach((pair) => {
1630
- const at = pair.indexOf(":");
1631
- const group = pair.slice(0, at);
1632
- if (at < 1 || !id.test(group)) return;
1633
- const selector = '[data-prepr-group="' + group + '"]';
1634
- css += selector + "{display:none!important}";
1635
- pair.slice(at + 1).split("|").forEach((variant) => {
1636
- if (id.test(variant)) {
1637
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
1638
- }
1456
+ const key = location.pathname + location.search;
1457
+ if (decision !== void 0) state.__preprDecision = { key, value: decision };
1458
+ const current = state.__preprDecision;
1459
+ if (!current || current.key !== key) return false;
1460
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
1461
+ marker.setAttribute("data-prepr-decided", "");
1462
+ });
1463
+ const defaults = current.value === "*";
1464
+ const chosen = {};
1465
+ current.value.split("~").forEach((pair) => {
1466
+ const colon = pair.indexOf(":");
1467
+ if (colon > 0)
1468
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
1469
+ });
1470
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
1471
+ const nested = element.hasAttribute("data-prepr-nested");
1472
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
1473
+ const colon = token.indexOf(":");
1474
+ const variant = token.slice(colon + 1);
1475
+ const variants = chosen[token.slice(0, colon)];
1476
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
1639
1477
  });
1478
+ if (show) element.setAttribute("data-prepr-show", "");
1479
+ else element.removeAttribute("data-prepr-show");
1640
1480
  });
1481
+ return true;
1482
+ }
1483
+ function startPreprDecision() {
1484
+ const state = window;
1485
+ const key = location.pathname + location.search;
1486
+ if (state.__preprPending && state.__preprPending.key === key) return;
1487
+ let last;
1641
1488
  try {
1642
- if (!state.__preprDecisionSheet) {
1643
- const sheet = new CSSStyleSheet();
1644
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1645
- state.__preprDecisionSheet = sheet;
1646
- }
1647
- state.__preprDecisionSheet.replaceSync(css);
1489
+ last = JSON.parse(sessionStorage.getItem("prepr_decision") || "{}");
1648
1490
  } catch {
1649
- if (!state.__preprDecisionSheet) {
1650
- state.__preprDecisionSheet = document.createElement("style");
1651
- document.head.appendChild(state.__preprDecisionSheet);
1491
+ last = {};
1492
+ }
1493
+ const age = Date.now() - (last.at || 0);
1494
+ const value = last.key === key && typeof last.value === "string" && age >= 0 && age < 1e4 ? Promise.resolve(last.value) : fetch(key, {
1495
+ headers: { "Prepr-Decide": "1" },
1496
+ cache: "no-store",
1497
+ credentials: "same-origin"
1498
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1499
+ try {
1500
+ sessionStorage.setItem(
1501
+ "prepr_decision",
1502
+ JSON.stringify({ key, value: answer, at: Date.now() })
1503
+ );
1504
+ } catch {
1652
1505
  }
1653
- state.__preprDecisionSheet.textContent = css;
1654
- }
1506
+ return answer;
1507
+ });
1508
+ state.__preprPending = { key, value };
1509
+ value.then((answer) => {
1510
+ if (location.pathname + location.search === key) state.__preprApply(answer);
1511
+ });
1655
1512
  }
1656
1513
 
1657
1514
  // src/nextjs/stack.ts
1658
1515
  var import_components = require("./components.cjs");
1659
- var AB_ATTRIBUTE = "data-prepr-ab";
1660
- var AB_BUCKETS = ["A", "B"];
1661
- var AB_BASE_CSS = `[${AB_ATTRIBUTE}]{display:contents}[${AB_ATTRIBUTE}]:not([${AB_ATTRIBUTE}="A"]){display:none}`;
1662
- var AB_SWITCH_SCRIPT = `(${applyPreprAbBucket.toString()})();(${applyPreprDecisions.toString()})()`;
1663
- var GROUP_ATTRIBUTE = "data-prepr-group";
1664
- var VARIANT_ATTRIBUTE = "data-prepr-variant";
1516
+ var VARIANT_ATTRIBUTE = "data-prepr-v";
1665
1517
  var DEFAULT_ATTRIBUTE = "data-prepr-default";
1666
- var GROUP_BASE_CSS = `[${GROUP_ATTRIBUTE}]{display:contents}[${GROUP_ATTRIBUTE}]:not([${DEFAULT_ATTRIBUTE}]){display:none}`;
1518
+ var NESTED_ATTRIBUTE = "data-prepr-nested";
1519
+ 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}`;
1520
+ var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1521
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};(${startPreprDecision.toString()})();`;
1522
+ var APPLY_SCRIPT = "__preprApply()";
1667
1523
  function variantKeysOf(items) {
1668
1524
  return items.flatMap((item) => {
1669
- const key = preprVariantProps(item)["data-prepr-variant-key"];
1525
+ const key = preprVariantProps(item)["data-prepr-variant-key"] ?? nestedKeyOf(item);
1670
1526
  return key ? [key] : [];
1671
1527
  });
1672
1528
  }
1673
- function renderBuckets(items, render, options) {
1674
- const {
1675
- bucket,
1676
- decisions,
1677
- abTest,
1678
- index,
1679
- track,
1680
- placeholder = false,
1681
- defaultVariant = true
1682
- } = options;
1683
- const renderVariant = (variant, key) => {
1684
- const resolved = resolveVariants(items, { decisions, bucket: variant });
1685
- const ab = abTest && !bucket ? variant : void 0;
1686
- const content = render(resolved, {
1687
- index,
1688
- placeholder,
1689
- defaultVariant: defaultVariant && (ab === void 0 || ab === "A")
1690
- });
1691
- const variantKeys = track ? variantKeysOf(resolved) : [];
1692
- if (variantKeys.length > 0) {
1693
- return (0, import_react.createElement)(
1694
- import_components.PreprVariantTracking,
1695
- { key, variantKeys, ab },
1696
- content
1697
- );
1529
+ function nestedKeyOf(value) {
1530
+ if (!value || typeof value !== "object") return void 0;
1531
+ for (const child of Object.values(value)) {
1532
+ const items = Array.isArray(child) ? child : [child];
1533
+ for (const item of items) {
1534
+ const key = contextOf(item)?.variant_key ?? (item && nestedKeyOf(item));
1535
+ if (key) return key;
1698
1536
  }
1699
- if (ab) return (0, import_react.createElement)("div", { key, [AB_ATTRIBUTE]: ab }, content);
1700
- return content;
1537
+ }
1538
+ return void 0;
1539
+ }
1540
+ function renderItems(items, render, options) {
1541
+ const resolved = resolveVariants(items, {
1542
+ decisions: options.decisions,
1543
+ bucket: options.bucket
1544
+ });
1545
+ const content = render(resolved, {
1546
+ index: options.index,
1547
+ defaultVariant: options.defaultVariant
1548
+ });
1549
+ const variantKeys = options.track ? variantKeysOf(resolved) : [];
1550
+ return variantKeys.length > 0 ? (0, import_react.createElement)(import_components.PreprVariantTracking, { variantKeys }, content) : content;
1551
+ }
1552
+ function decidedGroups(items, bucket) {
1553
+ const groups = /* @__PURE__ */ new Map();
1554
+ const walk = (value) => {
1555
+ if (Array.isArray(value)) return value.forEach(walk);
1556
+ if (!value || typeof value !== "object") return;
1557
+ const ctx = contextOf(value);
1558
+ const decided = ctx?.kind === "PERSONALIZATION" || ctx?.kind === "AB_TEST" && !bucket;
1559
+ if (decided && ctx.variant_id) {
1560
+ const group = groups.get(ctx.group_id) ?? {
1561
+ variants: /* @__PURE__ */ new Set(),
1562
+ fallback: ctx.kind === "AB_TEST" ? "A" : PREPR_DEFAULT_VARIANT
1563
+ };
1564
+ group.variants.add(ctx.variant_id);
1565
+ groups.set(ctx.group_id, group);
1566
+ }
1567
+ Object.values(value).forEach(walk);
1701
1568
  };
1702
- if (!abTest || bucket) return renderVariant(bucket);
1703
- return AB_BUCKETS.map((variant) => renderVariant(variant, variant));
1704
- }
1705
- function renderAllVariants(block, render, options) {
1706
- const group = block.key.slice("group:".length);
1707
- const variants = [
1708
- ...new Set(
1709
- block.items.flatMap((item) => {
1710
- const ctx = item[PREPR_CONTEXT_ALIAS];
1711
- return ctx?.variant_id ? [ctx.variant_id] : [];
1712
- })
1713
- )
1714
- ];
1715
- return variants.map(
1716
- (variant) => (0, import_react.createElement)(
1569
+ walk(items);
1570
+ return groups;
1571
+ }
1572
+ function renderVariants(block, groups, render, options) {
1573
+ const top = block.key.startsWith("group:") ? block.key.slice("group:".length) : void 0;
1574
+ const nested = !(top !== void 0 && groups.size === 1);
1575
+ const choices = [...groups].map(([group, { variants, fallback }]) => {
1576
+ const ids = nested ? [fallback, ...variants] : [...variants];
1577
+ return [...new Set(ids)].map((variant) => [group, variant]);
1578
+ });
1579
+ const combinations = choices.reduce(
1580
+ (all, pairs) => all.flatMap((combo) => pairs.map((p) => [...combo, p])),
1581
+ [[]]
1582
+ );
1583
+ return combinations.map((combination) => {
1584
+ const pairs = combination.map(([group, variant]) => `${group}:${variant}`);
1585
+ const isDefault = combination.every(
1586
+ ([group, variant]) => variant === groups.get(group).fallback
1587
+ );
1588
+ return (0, import_react.createElement)(
1717
1589
  "div",
1718
1590
  {
1719
- key: variant,
1720
- [GROUP_ATTRIBUTE]: group,
1721
- [VARIANT_ATTRIBUTE]: variant,
1722
- ...variant === PREPR_DEFAULT_VARIANT ? { [DEFAULT_ATTRIBUTE]: "" } : {}
1591
+ key: pairs.join(" "),
1592
+ [VARIANT_ATTRIBUTE]: pairs.join(" "),
1593
+ ...isDefault ? { [DEFAULT_ATTRIBUTE]: "" } : {},
1594
+ ...nested ? { [NESTED_ATTRIBUTE]: "" } : {},
1595
+ // The decision script marks the shown wrapper before hydration.
1596
+ suppressHydrationWarning: true
1723
1597
  },
1724
- renderBuckets(block.items, render, {
1598
+ renderItems(block.items, render, {
1725
1599
  bucket: options.bucket,
1726
- decisions: /* @__PURE__ */ new Map([[group, [variant]]]),
1727
- abTest: false,
1600
+ decisions: new Map(combination.map(([g, v]) => [g, [v]])),
1728
1601
  index: block.index,
1729
1602
  track: options.track,
1730
- defaultVariant: variant === PREPR_DEFAULT_VARIANT
1603
+ defaultVariant: isDefault
1731
1604
  })
1732
- )
1733
- );
1734
- }
1735
- async function PersonalizedBlock({
1736
- items,
1737
- decisions,
1738
- bucket,
1739
- abTest,
1740
- index,
1741
- render,
1742
- track
1743
- }) {
1744
- const decided = await decisions.catch((error) => {
1745
- if (isFrameworkSignal(error)) throw error;
1746
- return void 0;
1747
- });
1748
- return renderBuckets(items, render, {
1749
- bucket,
1750
- decisions: decided,
1751
- abTest,
1752
- index,
1753
- track
1605
+ );
1754
1606
  });
1755
1607
  }
1756
1608
  function PreprStack({
1757
1609
  items,
1758
- decisions,
1759
1610
  render,
1760
1611
  bucket,
1761
1612
  nonce,
1762
- trackVariants = false,
1763
- aboveTheFold = 1
1613
+ trackVariants = false
1764
1614
  }) {
1765
- decisions.catch(() => void 0);
1766
- const blocks = splitStack(items);
1767
- const decidedInProxy = (block) => block.index < aboveTheFold && block.personalized && !block.abTest && block.key.startsWith("group:");
1768
- const hasDecided = blocks.some(decidedInProxy);
1769
- const needsSwitch = !bucket && blocks.some((block) => block.abTest) || hasDecided;
1615
+ const blocks = splitStack(items).map((block) => ({
1616
+ ...block,
1617
+ groups: decidedGroups(block.items, bucket)
1618
+ }));
1619
+ const varies = blocks.some((block) => block.groups.size > 0);
1770
1620
  return (0, import_react.createElement)(
1771
1621
  import_react.Fragment,
1772
1622
  null,
1773
- needsSwitch && (0, import_react.createElement)(
1623
+ varies && (0, import_react.createElement)(
1774
1624
  import_react.Fragment,
1775
- { key: "prepr-ab" },
1625
+ { key: "prepr-switch" },
1776
1626
  (0, import_react.createElement)("style", {
1777
1627
  nonce,
1778
- dangerouslySetInnerHTML: {
1779
- __html: AB_BASE_CSS + (hasDecided ? GROUP_BASE_CSS : "")
1780
- }
1628
+ dangerouslySetInnerHTML: { __html: GROUP_BASE_CSS }
1629
+ }),
1630
+ (0, import_react.createElement)("noscript", {
1631
+ dangerouslySetInnerHTML: { __html: NOSCRIPT_HTML }
1781
1632
  }),
1782
1633
  (0, import_react.createElement)("script", {
1783
1634
  nonce,
1784
- dangerouslySetInnerHTML: { __html: AB_SWITCH_SCRIPT }
1635
+ dangerouslySetInnerHTML: { __html: DECISION_SCRIPT }
1785
1636
  }),
1786
- (0, import_react.createElement)(import_components.PreprAbSwitch),
1787
- hasDecided && (0, import_react.createElement)(import_components.PreprPersonalizationSwitch, { refresh: false })
1637
+ (0, import_react.createElement)(import_components.PreprDecisionApplier),
1638
+ // Marked decided by the decision switch; never laid out.
1639
+ (0, import_react.createElement)("template", {
1640
+ "data-prepr-decision": "",
1641
+ suppressHydrationWarning: true
1642
+ })
1788
1643
  ),
1789
1644
  ...blocks.map((block) => {
1790
- if (decidedInProxy(block)) {
1645
+ if (block.groups.size === 0) {
1791
1646
  return (0, import_react.createElement)(
1792
1647
  import_react.Fragment,
1793
1648
  { key: block.key },
1794
- renderAllVariants(block, render, { bucket, track: trackVariants })
1649
+ renderItems(block.items, render, {
1650
+ bucket,
1651
+ index: block.index,
1652
+ track: trackVariants,
1653
+ defaultVariant: true
1654
+ })
1795
1655
  );
1796
1656
  }
1797
- const fallback = renderBuckets(block.items, render, {
1798
- bucket,
1799
- abTest: block.abTest,
1800
- index: block.index,
1801
- track: trackVariants && !block.personalized,
1802
- placeholder: block.personalized
1803
- });
1804
- if (!block.personalized) {
1805
- return (0, import_react.createElement)(import_react.Fragment, { key: block.key }, fallback);
1806
- }
1807
1657
  return (0, import_react.createElement)(
1808
- import_react.Suspense,
1809
- { key: block.key, fallback },
1810
- (0, import_react.createElement)(PersonalizedBlock, {
1811
- items: block.items,
1812
- decisions,
1658
+ import_react.Fragment,
1659
+ { key: block.key },
1660
+ renderVariants(block, block.groups, render, {
1813
1661
  bucket,
1814
- abTest: block.abTest,
1815
- index: block.index,
1816
- render,
1817
1662
  track: trackVariants
1663
+ }),
1664
+ (0, import_react.createElement)("script", {
1665
+ nonce,
1666
+ dangerouslySetInnerHTML: { __html: APPLY_SCRIPT }
1818
1667
  })
1819
1668
  );
1820
1669
  })
@@ -1826,33 +1675,59 @@ function createPreprClient2(options) {
1826
1675
  const { cacheTags = ["prepr"], revalidate, ...coreOptions } = options;
1827
1676
  const client = createPreprClient({
1828
1677
  // The schema introspection goes through the Data Cache: shared across
1829
- // server instances, refreshed hourly and by revalidateTag on publish.
1678
+ // server instances, refreshed hourly and by a `schema.changed` webhook.
1830
1679
  schemaInit: {
1831
- next: { tags: cacheTags, revalidate: 3600 }
1680
+ next: { tags: [...cacheTags, PREPR_SCHEMA_TAG], revalidate: 3600 }
1832
1681
  },
1833
1682
  ...coreOptions
1834
1683
  });
1835
- const seenGroups = /* @__PURE__ */ new Map();
1836
1684
  const nextClient = {
1837
1685
  async query(document2, variables, queryOptions = {}) {
1838
- const { personalize = false, ...rest } = queryOptions;
1839
- const init = rest.init;
1840
- const tagged = {
1686
+ const { personalize = false, tagItems = false, ...rest } = queryOptions;
1687
+ const { next, ...init } = rest.init ?? {};
1688
+ if (tagItems && typeof nextCache.cacheTag !== "function") {
1689
+ throw new Error("tagItems requires Next.js 16 or later.");
1690
+ }
1691
+ const options2 = {
1841
1692
  ...rest,
1842
- init: {
1693
+ // With tagItems, `next` is dropped too: tags or revalidate alongside
1694
+ // `no-store` are a conflict Next.js resolves by ignoring the store.
1695
+ // Otherwise tags are attached unconditionally; the core client drops
1696
+ // them again whenever the request ends up `no-store` (personalized,
1697
+ // per-request headers, or preview).
1698
+ init: tagItems ? { ...init, cache: "no-store" } : {
1843
1699
  ...init,
1844
1700
  next: {
1845
- tags: cacheTags,
1846
1701
  ...revalidate === void 0 ? {} : { revalidate },
1847
- ...init?.next
1702
+ ...next,
1703
+ // Merged, so a per-query tag never drops the client's own,
1704
+ // which the publish webhook relies on.
1705
+ tags: [...cacheTags, ...next?.tags ?? []]
1848
1706
  }
1849
1707
  }
1850
1708
  };
1851
- if (!personalize) return client.query(document2, variables, tagged);
1852
- return client.query(document2, variables, {
1853
- ...tagged,
1854
- headers: mergePreprHeaders(await getPreprHeaders(), rest.headers)
1855
- });
1709
+ if (personalize) {
1710
+ options2.headers = mergePreprHeaders(
1711
+ await getPreprHeaders(),
1712
+ rest.headers
1713
+ );
1714
+ }
1715
+ const result = await client.query(document2, variables, options2);
1716
+ if (tagItems) {
1717
+ let modelTags;
1718
+ try {
1719
+ modelTags = preprModelTagsFor(
1720
+ documentOf(document2),
1721
+ result.data,
1722
+ await clientSchemas.get(client)()
1723
+ );
1724
+ } catch {
1725
+ modelTags = [PREPR_ANY_ITEM_TAG];
1726
+ }
1727
+ nextCache.cacheTag(...cacheTags, ...next?.tags ?? [], ...modelTags);
1728
+ nextCache.cacheTag(...preprCacheTags(result.data));
1729
+ }
1730
+ return result;
1856
1731
  },
1857
1732
  async decide(document2, variables, queryOptions = {}) {
1858
1733
  return client.decide(document2, variables, {
@@ -1863,27 +1738,14 @@ function createPreprClient2(options) {
1863
1738
  )
1864
1739
  });
1865
1740
  },
1866
- hasAbTests: (document2, variables) => client.hasAbTests(document2, variables),
1867
- resolveDecisions: (request, response, document2, variables, options2) => resolveDecisionsWith(
1868
- client,
1869
- seenGroups,
1870
- request,
1871
- response,
1872
- document2,
1873
- variables,
1874
- options2
1875
- )
1741
+ hasAbTests: (document2, variables) => client.hasAbTests(document2, variables)
1876
1742
  };
1877
- preprClientInternals.set(nextClient, {
1878
- client,
1879
- seenGroups,
1880
- url: coreOptions.url
1881
- });
1743
+ preprClientInternals.set(nextClient, { client });
1882
1744
  return nextClient;
1883
1745
  }
1884
1746
  // Annotate the CommonJS export names for ESM import in node:
1885
1747
  0 && (module.exports = {
1886
- PREPR_DECISIONS_COOKIE,
1748
+ PREPR_ANY_ITEM_TAG,
1887
1749
  PREPR_DEFAULT_VARIANT,
1888
1750
  PreprError,
1889
1751
  PreprStack,
@@ -1892,6 +1754,8 @@ function createPreprClient2(options) {
1892
1754
  decisionsFrom,
1893
1755
  isPersonalizedResponse,
1894
1756
  loadPreprSchema,
1757
+ preprCacheTags,
1758
+ preprModelTag,
1895
1759
  preprVariantProps,
1896
1760
  resolveVariants,
1897
1761
  splitStack,