@preprio/toolkit 0.6.0-alpha.6 → 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-MWRA3732.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-OQGEL4TV.js → chunk-3QGWCYTL.js} +1 -1
  10. package/dist/{chunk-W6AVX6FI.js → chunk-PFJO4TYL.js} +52 -5
  11. package/dist/{chunk-Y7DWVOOY.js → chunk-U7TRFVK7.js} +1 -1
  12. package/dist/{chunk-NSQOGI5K.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-PIQF5ZI5.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 +372 -509
  29. package/dist/nextjs/graphql.d.cts +131 -135
  30. package/dist/nextjs/graphql.d.ts +131 -135
  31. package/dist/nextjs/graphql.js +307 -456
  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-PF6GVWHV.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-5K7HWMIF.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,13 +1,20 @@
1
1
  import {
2
+ PREPR_ANY_ITEM_TAG,
3
+ PREPR_SCHEMA_TAG,
2
4
  PreprError,
3
5
  createPreprMiddleware,
4
6
  getPreprHeaders,
5
7
  getPreprHeadersFromHeaders,
6
8
  isPersonalizedResponse,
9
+ preprCacheTags,
7
10
  preprDraftRedirect,
8
- resolvePreprBucket,
11
+ preprModelTag,
12
+ preprModelTagsFor,
9
13
  validatePreprToken
10
- } from "./chunk-PIQF5ZI5.js";
14
+ } from "./chunk-CZK7SMBD.js";
15
+
16
+ // src/nextjs/graphql.ts
17
+ import * as nextCache from "next/cache";
11
18
 
12
19
  // src/core/variants/constants.ts
13
20
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -96,7 +103,10 @@ function decisionSelectionSet(set, typeName, path, ctx) {
96
103
  path,
97
104
  ctx
98
105
  );
99
- if (pruned) {
106
+ if (!pruned) continue;
107
+ if (fragment.typeCondition.name.value === typeName) {
108
+ selections.push(...pruned.selections);
109
+ } else {
100
110
  selections.push(
101
111
  inlineFragment(fragment.typeCondition.name.value, [
102
112
  ...pruned.selections
@@ -106,13 +116,19 @@ function decisionSelectionSet(set, typeName, path, ctx) {
106
116
  continue;
107
117
  }
108
118
  if (selection.kind === "InlineFragment") {
119
+ const condition = selection.typeCondition?.name.value ?? typeName;
109
120
  const pruned = decisionSelectionSet(
110
121
  selection.selectionSet,
111
- selection.typeCondition?.name.value ?? typeName,
122
+ condition,
112
123
  path,
113
124
  ctx
114
125
  );
115
- if (pruned) selections.push({ ...selection, selectionSet: pruned });
126
+ if (!pruned) continue;
127
+ if (condition === typeName && !selection.directives?.length) {
128
+ selections.push(...pruned.selections);
129
+ } else {
130
+ selections.push({ ...selection, selectionSet: pruned });
131
+ }
116
132
  continue;
117
133
  }
118
134
  if (!selection.selectionSet) continue;
@@ -186,20 +202,58 @@ function toDecisionDocument(document2, schema, groupPaths, options) {
186
202
  const used = /* @__PURE__ */ new Set();
187
203
  collectVariables(pruned, used);
188
204
  collectVariables(operation.directives, used);
205
+ const shared = sharedContextFragment(schema);
189
206
  return {
190
207
  kind: "Document",
191
208
  definitions: [
209
+ ...shared ? [shared.definition] : [],
192
210
  {
193
211
  ...operation,
194
212
  name: operation.name && name(`${operation.name.value}Decision`),
195
213
  variableDefinitions: (operation.variableDefinitions ?? []).filter(
196
214
  (definition) => used.has(definition.variable.name.value)
197
215
  ),
198
- selectionSet: pruned
216
+ selectionSet: shared ? shared.apply(pruned) : pruned
199
217
  }
200
218
  ]
201
219
  };
202
220
  }
221
+ function sharedContextFragment(schema) {
222
+ const contextTypes = new Set(
223
+ Object.values(schema.types).flatMap(
224
+ (type) => type.hasContext ? [type.fields._context.type] : []
225
+ )
226
+ );
227
+ if (contextTypes.size !== 1) return null;
228
+ const [contextType] = contextTypes;
229
+ const fragmentName = "c";
230
+ const definition = {
231
+ kind: "FragmentDefinition",
232
+ name: name(fragmentName),
233
+ typeCondition: { kind: "NamedType", name: name(contextType) },
234
+ selectionSet: selectionSet(
235
+ ["kind", "group_id", "variant_id"].map(
236
+ (field) => ({ kind: "Field", name: name(field) })
237
+ )
238
+ )
239
+ };
240
+ const spread = {
241
+ kind: "FragmentSpread",
242
+ name: name(fragmentName)
243
+ };
244
+ const apply = (set) => selectionSet(
245
+ set.selections.map((selection) => {
246
+ if (selection.kind === "Field" && selection.alias?.value === PREPR_CONTEXT_ALIAS) {
247
+ return { ...selection, selectionSet: selectionSet([spread]) };
248
+ }
249
+ return "selectionSet" in selection && selection.selectionSet ? {
250
+ ...selection,
251
+ selectionSet: apply(selection.selectionSet)
252
+ } : selection;
253
+ })
254
+ );
255
+ return { definition, apply };
256
+ }
203
257
 
204
258
  // src/core/variants/probe-document.ts
205
259
  function probeSelectionSet(set, fragments) {
@@ -773,6 +827,7 @@ function createVaryProbe(run, options) {
773
827
  }
774
828
 
775
829
  // src/core/graphql/client.ts
830
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
776
831
  function isParseableUrl(url) {
777
832
  try {
778
833
  new URL(url);
@@ -809,7 +864,7 @@ function createPreprClient(options) {
809
864
  ));
810
865
  const variantsDocuments = /* @__PURE__ */ new Map();
811
866
  const decisionDocuments = /* @__PURE__ */ new Map();
812
- return {
867
+ const client = {
813
868
  async query(document2, variables, queryOptions = {}) {
814
869
  const { variants, ...rest } = queryOptions;
815
870
  if (!variants) return run(document2, variables, rest);
@@ -821,10 +876,10 @@ function createPreprClient(options) {
821
876
  return run(derived, variables, rest);
822
877
  },
823
878
  async decide(document2, variables, queryOptions = {}) {
824
- const { from, topLevelOnly, ...rest } = queryOptions;
879
+ const { from, ...rest } = queryOptions;
825
880
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
826
881
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
827
- const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : topLevelOnly ? "top" : "*";
882
+ const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : "*";
828
883
  let byPaths = decisionDocuments.get(document2);
829
884
  if (!byPaths) {
830
885
  byPaths = /* @__PURE__ */ new Map();
@@ -832,9 +887,7 @@ function createPreprClient(options) {
832
887
  }
833
888
  let derived = byPaths.get(signature);
834
889
  if (derived === void 0) {
835
- derived = toDecisionDocument(document2, await loadSchema(), paths, {
836
- topLevelOnly: !paths && topLevelOnly
837
- });
890
+ derived = toDecisionDocument(document2, await loadSchema(), paths);
838
891
  byPaths.set(signature, derived);
839
892
  }
840
893
  if (!derived) return /* @__PURE__ */ new Map();
@@ -848,6 +901,8 @@ function createPreprClient(options) {
848
901
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
849
902
  )
850
903
  };
904
+ clientSchemas.set(client, loadSchema);
905
+ return client;
851
906
  }
852
907
 
853
908
  // src/core/graphql/schema-loader.ts
@@ -874,69 +929,15 @@ async function loadPreprSchema(url, options = {}) {
874
929
  return schema;
875
930
  }
876
931
 
877
- // src/core/refresh.ts
878
- var PREPR_REFRESH_COOKIE = "prepr_refresh";
879
- var PREPR_REFRESH_HEADER = "Prepr-Refresh";
880
- var REFRESH_MAX_PATHS = 5;
881
- var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
882
- function safePath(path) {
883
- return SAFE_PATH.test(path) ? path : void 0;
884
- }
885
- function isFresh(at, freshFor, now = Date.now()) {
886
- const age = now - at;
887
- return at > 0 && age >= 0 && age < freshFor;
888
- }
889
- function cleanEntries(entries) {
890
- const out = [];
891
- for (const entry of entries) {
892
- if (out.length === REFRESH_MAX_PATHS) break;
893
- if (safePath(entry.path) && !out.some((e) => e.path === entry.path)) {
894
- out.push(entry);
895
- }
896
- }
897
- return out;
898
- }
899
- function parseRefresh(raw) {
900
- return cleanEntries(
901
- (raw ?? "").split("|").map((pair) => {
902
- const split = pair.lastIndexOf("~");
903
- const at = Number(split < 0 ? "" : pair.slice(split + 1));
904
- return {
905
- path: split < 0 ? pair : pair.slice(0, split),
906
- at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
907
- };
908
- })
909
- );
910
- }
911
- function serializeRefresh(entries) {
912
- return cleanEntries(entries).map((entry) => `${entry.path}~${Math.floor(entry.at)}`).join("|");
913
- }
914
- function parseRefreshPaths(header) {
915
- return cleanEntries(
916
- (header ?? "").split("|").map((path) => ({ path, at: 0 }))
917
- ).map((entry) => entry.path);
918
- }
919
-
920
932
  // src/nextjs/decisions.ts
921
- var PREPR_DECISIONS_COOKIE = "prepr_decisions";
933
+ var PREPR_DECIDE_HEADER = "Prepr-Decide";
934
+ var DEFAULT_DECISION = "*";
922
935
  var DECISIONS_MAX_GROUPS = 40;
923
936
  var SAFE_ID = /^[\w-]+$/;
924
- var SEEN_MAX_PAGES = 1e3;
925
- function parseDecisions(raw) {
926
- const out = /* @__PURE__ */ new Map();
927
- for (const pair of (raw ?? "").split("~")) {
928
- const at = pair.indexOf(":");
929
- const group = pair.slice(0, at);
930
- if (at < 1 || !SAFE_ID.test(group)) continue;
931
- out.set(
932
- group,
933
- pair.slice(at + 1).split("|").filter((variant) => SAFE_ID.test(variant))
934
- );
935
- }
936
- return out;
937
- }
938
937
  function serializeDecisions(decisions) {
939
- return [...decisions].slice(0, DECISIONS_MAX_GROUPS).map(([group, variants]) => `${group}:${variants.join("|")}`).join("~");
938
+ return [...decisions].filter(([group]) => SAFE_ID.test(group)).slice(0, DECISIONS_MAX_GROUPS).map(
939
+ ([group, variants]) => `${group}:${variants.filter((v) => SAFE_ID.test(v)).join("|")}`
940
+ ).join("~");
940
941
  }
941
942
  function forwardedHeaders(request, response) {
942
943
  const overridden = response.headers.get("x-middleware-override-headers");
@@ -949,109 +950,16 @@ function forwardedHeaders(request, response) {
949
950
  return getPreprHeadersFromHeaders(headers);
950
951
  }
951
952
  var preprClientInternals = /* @__PURE__ */ new WeakMap();
952
- var DEFAULT_FRESH_FOR_MS = 1e4;
953
- async function decidePage(client, seenGroups, headers, document2, variables, timeoutMs = 1e3) {
954
- if (!await client.hasSegments(document2, variables)) return void 0;
955
- const decided = await client.decide(document2, variables, {
956
- topLevelOnly: true,
957
- headers,
958
- init: { signal: AbortSignal.timeout(timeoutMs) }
959
- });
960
- let byVariables = seenGroups.get(document2);
961
- if (!byVariables) {
962
- byVariables = /* @__PURE__ */ new Map();
963
- seenGroups.set(document2, byVariables);
964
- }
965
- const pageKey = JSON.stringify(variables ?? null);
966
- let groups = byVariables.get(pageKey);
967
- if (!groups) {
968
- if (byVariables.size >= SEEN_MAX_PAGES) {
969
- byVariables.delete(byVariables.keys().next().value);
970
- }
971
- groups = /* @__PURE__ */ new Set();
972
- byVariables.set(pageKey, groups);
973
- }
974
- for (const group of decided.keys()) groups.add(group);
975
- return decided;
976
- }
977
- function writeDecisions(request, response, decided, paths) {
978
- const next = /* @__PURE__ */ new Map();
979
- for (const decisions of decided) {
980
- for (const [group, variants] of decisions) {
981
- if (SAFE_ID.test(group) && !next.has(group)) {
982
- next.set(
983
- group,
984
- variants.filter((v) => SAFE_ID.test(v))
985
- );
986
- }
987
- }
988
- }
989
- for (const [group, variants] of parseDecisions(
990
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
991
- )) {
992
- if (!next.has(group)) next.set(group, variants);
993
- }
994
- const options = {
995
- path: "/",
996
- sameSite: "lax",
997
- httpOnly: false,
998
- secure: request.nextUrl.protocol === "https:"
999
- };
1000
- if (next.size > 0) {
1001
- response.cookies.set(
1002
- PREPR_DECISIONS_COOKIE,
1003
- serializeDecisions(next),
1004
- options
1005
- );
1006
- }
1007
- if (paths.length > 0) {
1008
- const at = Date.now();
1009
- response.cookies.set(
1010
- PREPR_REFRESH_COOKIE,
1011
- serializeRefresh([
1012
- ...paths.map((path) => ({ path, at })),
1013
- ...parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value)
1014
- ]),
1015
- options
1016
- );
1017
- }
1018
- }
1019
- function pagePath(request) {
1020
- return request.nextUrl.basePath + request.nextUrl.pathname;
1021
- }
1022
- function contextChanged(request, headers) {
1023
- return headers.get("Prepr-Customer-Id-Created") === "true" || headers.has("Prepr-Segments") || [...request.nextUrl.searchParams.keys()].some(
1024
- (key) => key.startsWith("utm_")
1025
- );
1026
- }
1027
- async function resolveDecisionsWith(client, seenGroups, request, response, document2, variables, options) {
953
+ async function decideView(client, headers, page, timeoutMs = 3e3) {
954
+ const init = { signal: AbortSignal.timeout(timeoutMs) };
1028
955
  try {
1029
- if (options?.onlyIf && !await options.onlyIf()) return;
1030
- const known = seenGroups.get(document2)?.get(JSON.stringify(variables ?? null));
1031
- const path = pagePath(request);
1032
- const headers = mergePreprHeaders(forwardedHeaders(request, response));
1033
- const stored = parseDecisions(
1034
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1035
- );
1036
- const decidedAt = parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value).find(
1037
- (entry) => entry.path === path
1038
- )?.at ?? 0;
1039
- const fresh = isFresh(decidedAt, options?.freshFor ?? DEFAULT_FRESH_FOR_MS);
1040
- if (fresh && known && !contextChanged(request, headers) && [...known].every((group) => stored.has(group))) {
1041
- return;
1042
- }
1043
- const decided = await decidePage(
1044
- client,
1045
- seenGroups,
1046
- headers,
1047
- document2,
1048
- variables,
1049
- options?.timeoutMs
1050
- );
1051
- if (!decided || decided.size === 0) return;
1052
- writeDecisions(request, response, [decided], [path]);
956
+ const decided = page.decision ? decisionsFrom(
957
+ (await client.query(page.decision, page.variables, { headers, init })).data
958
+ ) : await client.decide(page.document, page.variables, { headers, init });
959
+ return serializeDecisions(decided);
1053
960
  } catch (error) {
1054
961
  if (isFrameworkSignal(error)) throw error;
962
+ return DEFAULT_DECISION;
1055
963
  }
1056
964
  }
1057
965
 
@@ -1069,7 +977,7 @@ function createPreprProxy(options) {
1069
977
  "createPreprProxy"
1070
978
  );
1071
979
  }
1072
- const { client, seenGroups, url } = internals;
980
+ const { client } = internals;
1073
981
  const preview = options.preview ?? false;
1074
982
  const allowed = async (request) => {
1075
983
  if (!options.onlyIf) return true;
@@ -1086,66 +994,31 @@ function createPreprProxy(options) {
1086
994
  return null;
1087
995
  }
1088
996
  };
1089
- async function refresh(request, response, paths) {
1090
- const answer = new NextResponse(null, {
1091
- status: 204,
1092
- headers: { "Cache-Control": "no-store" }
1093
- });
1094
- for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
997
+ async function answerDecision(request, response, target) {
998
+ let decision = DEFAULT_DECISION;
1095
999
  const site = request.headers.get("sec-fetch-site");
1096
- if (request.method !== "GET" || site !== null && site !== "same-origin" || !await allowed(request)) {
1097
- return answer;
1000
+ if (target && request.method === "GET" && (site === null || site === "same-origin") && await allowed(request)) {
1001
+ decision = await decideView(
1002
+ client,
1003
+ mergePreprHeaders(forwardedHeaders(request, response)),
1004
+ target,
1005
+ options.timeoutMs
1006
+ );
1098
1007
  }
1099
- const own = new Set(
1100
- parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value).map(
1101
- (entry) => entry.path
1102
- )
1103
- );
1104
- const basePath = request.nextUrl.basePath;
1105
- const headers = mergePreprHeaders(forwardedHeaders(request, response));
1106
- const decided = await Promise.all(
1107
- paths.map(async (path) => {
1108
- if (!own.has(path) || !path.startsWith(basePath)) return void 0;
1109
- const pathname = path.slice(basePath.length) || "/";
1110
- if (!isPagePath(pathname)) return void 0;
1111
- const target = lookup(new URL(pathname, request.url));
1112
- if (!target) return void 0;
1113
- try {
1114
- const decisions = await decidePage(
1115
- client,
1116
- seenGroups,
1117
- headers,
1118
- target.document,
1119
- target.variables,
1120
- options.timeoutMs
1121
- );
1122
- return decisions?.size ? [path, decisions] : void 0;
1123
- } catch (error) {
1124
- if (isFrameworkSignal(error)) throw error;
1125
- return void 0;
1126
- }
1127
- })
1128
- );
1129
- const found = decided.filter((entry) => entry !== void 0);
1130
- writeDecisions(
1131
- request,
1132
- answer,
1133
- found.map(([, decisions]) => decisions),
1134
- found.map(([path]) => path)
1135
- );
1008
+ const answer = new NextResponse(decision, {
1009
+ headers: {
1010
+ "Cache-Control": "private, no-store",
1011
+ "Content-Type": "text/plain; charset=utf-8"
1012
+ }
1013
+ });
1014
+ for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1136
1015
  return answer;
1137
1016
  }
1138
1017
  return async (request) => {
1139
- if (request.headers.has(PREPR_REFRESH_HEADER)) {
1140
- return refresh(
1141
- request,
1142
- createPreprMiddleware(request, { preview, features: options.features }),
1143
- parseRefreshPaths(request.headers.get(PREPR_REFRESH_HEADER))
1144
- );
1145
- }
1146
1018
  const rendersPage = (request.method === "GET" || request.method === "HEAD") && isPagePath(request.nextUrl.pathname);
1147
1019
  const target = rendersPage ? lookup(request.nextUrl) : null;
1148
- if (target && preview) {
1020
+ const decisionRequest = request.headers.has(PREPR_DECIDE_HEADER);
1021
+ if (target && preview && !decisionRequest) {
1149
1022
  const draft = preprDraftRedirect(request, { preview });
1150
1023
  if (draft) return draft;
1151
1024
  }
@@ -1153,24 +1026,7 @@ function createPreprProxy(options) {
1153
1026
  preview,
1154
1027
  features: options.features
1155
1028
  });
1156
- if (target && await allowed(request)) {
1157
- await Promise.all([
1158
- resolvePreprBucket(request, response, {
1159
- url,
1160
- preview,
1161
- onlyIf: () => options.client.hasAbTests(target.document, target.variables)
1162
- }),
1163
- resolveDecisionsWith(
1164
- client,
1165
- seenGroups,
1166
- request,
1167
- response,
1168
- target.document,
1169
- target.variables,
1170
- { freshFor: options.freshFor, timeoutMs: options.timeoutMs }
1171
- )
1172
- ]);
1173
- }
1029
+ if (decisionRequest) return answerDecision(request, response, target);
1174
1030
  return response;
1175
1031
  };
1176
1032
  }
@@ -1178,243 +1034,223 @@ function createPreprProxy(options) {
1178
1034
  // src/nextjs/stack.ts
1179
1035
  import {
1180
1036
  createElement,
1181
- Fragment,
1182
- Suspense
1037
+ Fragment
1183
1038
  } from "react";
1184
1039
 
1185
- // src/nextjs/ab-switch.ts
1186
- function applyPreprAbBucket() {
1187
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
1188
- const bucket = match ? match[1] : "A";
1040
+ // src/nextjs/decision-switch.ts
1041
+ function applyPreprDecisions(decision) {
1189
1042
  const state = window;
1190
- if (state.__preprAbBucket === bucket) return;
1191
- state.__preprAbBucket = bucket;
1192
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
1193
- try {
1194
- if (!state.__preprAbSheet) {
1195
- const sheet = new CSSStyleSheet();
1196
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1197
- state.__preprAbSheet = sheet;
1198
- }
1199
- state.__preprAbSheet.replaceSync(css);
1200
- } catch {
1201
- if (!state.__preprAbSheet) {
1202
- state.__preprAbSheet = document.createElement("style");
1203
- document.head.appendChild(state.__preprAbSheet);
1204
- }
1205
- state.__preprAbSheet.textContent = css;
1206
- }
1207
- }
1208
- function applyPreprDecisions(view) {
1209
- const viewState = window;
1210
- const current = view === void 0 ? location.pathname + location.search : view;
1211
- if (viewState.__preprDecisionsView === current) return;
1212
- viewState.__preprDecisionsView = current;
1213
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
1214
- let raw;
1215
- try {
1216
- raw = match ? decodeURIComponent(match[1]) : "";
1217
- } catch {
1218
- raw = "";
1219
- }
1220
- const state = window;
1221
- if (state.__preprDecisions === raw) return;
1222
- state.__preprDecisions = raw;
1223
- const id = /^[\w-]+$/;
1224
- let css = "";
1225
- raw.split("~").forEach((pair) => {
1226
- const at = pair.indexOf(":");
1227
- const group = pair.slice(0, at);
1228
- if (at < 1 || !id.test(group)) return;
1229
- const selector = '[data-prepr-group="' + group + '"]';
1230
- css += selector + "{display:none!important}";
1231
- pair.slice(at + 1).split("|").forEach((variant) => {
1232
- if (id.test(variant)) {
1233
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
1234
- }
1043
+ const key = location.pathname + location.search;
1044
+ if (decision !== void 0) state.__preprDecision = { key, value: decision };
1045
+ const current = state.__preprDecision;
1046
+ if (!current || current.key !== key) return false;
1047
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
1048
+ marker.setAttribute("data-prepr-decided", "");
1049
+ });
1050
+ const defaults = current.value === "*";
1051
+ const chosen = {};
1052
+ current.value.split("~").forEach((pair) => {
1053
+ const colon = pair.indexOf(":");
1054
+ if (colon > 0)
1055
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
1056
+ });
1057
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
1058
+ const nested = element.hasAttribute("data-prepr-nested");
1059
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
1060
+ const colon = token.indexOf(":");
1061
+ const variant = token.slice(colon + 1);
1062
+ const variants = chosen[token.slice(0, colon)];
1063
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
1235
1064
  });
1065
+ if (show) element.setAttribute("data-prepr-show", "");
1066
+ else element.removeAttribute("data-prepr-show");
1236
1067
  });
1068
+ return true;
1069
+ }
1070
+ function startPreprDecision() {
1071
+ const state = window;
1072
+ const key = location.pathname + location.search;
1073
+ if (state.__preprPending && state.__preprPending.key === key) return;
1074
+ let last;
1237
1075
  try {
1238
- if (!state.__preprDecisionSheet) {
1239
- const sheet = new CSSStyleSheet();
1240
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1241
- state.__preprDecisionSheet = sheet;
1242
- }
1243
- state.__preprDecisionSheet.replaceSync(css);
1076
+ last = JSON.parse(sessionStorage.getItem("prepr_decision") || "{}");
1244
1077
  } catch {
1245
- if (!state.__preprDecisionSheet) {
1246
- state.__preprDecisionSheet = document.createElement("style");
1247
- document.head.appendChild(state.__preprDecisionSheet);
1248
- }
1249
- state.__preprDecisionSheet.textContent = css;
1078
+ last = {};
1250
1079
  }
1080
+ const age = Date.now() - (last.at || 0);
1081
+ const value = last.key === key && typeof last.value === "string" && age >= 0 && age < 1e4 ? Promise.resolve(last.value) : fetch(key, {
1082
+ headers: { "Prepr-Decide": "1" },
1083
+ cache: "no-store",
1084
+ credentials: "same-origin"
1085
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1086
+ try {
1087
+ sessionStorage.setItem(
1088
+ "prepr_decision",
1089
+ JSON.stringify({ key, value: answer, at: Date.now() })
1090
+ );
1091
+ } catch {
1092
+ }
1093
+ return answer;
1094
+ });
1095
+ state.__preprPending = { key, value };
1096
+ value.then((answer) => {
1097
+ if (location.pathname + location.search === key) state.__preprApply(answer);
1098
+ });
1251
1099
  }
1252
1100
 
1253
1101
  // src/nextjs/stack.ts
1254
- import {
1255
- PreprAbSwitch,
1256
- PreprPersonalizationSwitch,
1257
- PreprVariantTracking
1258
- } from "./components.js";
1259
- var AB_ATTRIBUTE = "data-prepr-ab";
1260
- var AB_BUCKETS = ["A", "B"];
1261
- var AB_BASE_CSS = `[${AB_ATTRIBUTE}]{display:contents}[${AB_ATTRIBUTE}]:not([${AB_ATTRIBUTE}="A"]){display:none}`;
1262
- var AB_SWITCH_SCRIPT = `(${applyPreprAbBucket.toString()})();(${applyPreprDecisions.toString()})()`;
1263
- var GROUP_ATTRIBUTE = "data-prepr-group";
1264
- var VARIANT_ATTRIBUTE = "data-prepr-variant";
1102
+ import { PreprDecisionApplier, PreprVariantTracking } from "./components.js";
1103
+ var VARIANT_ATTRIBUTE = "data-prepr-v";
1265
1104
  var DEFAULT_ATTRIBUTE = "data-prepr-default";
1266
- var GROUP_BASE_CSS = `[${GROUP_ATTRIBUTE}]{display:contents}[${GROUP_ATTRIBUTE}]:not([${DEFAULT_ATTRIBUTE}]){display:none}`;
1105
+ var NESTED_ATTRIBUTE = "data-prepr-nested";
1106
+ 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}`;
1107
+ var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1108
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};(${startPreprDecision.toString()})();`;
1109
+ var APPLY_SCRIPT = "__preprApply()";
1267
1110
  function variantKeysOf(items) {
1268
1111
  return items.flatMap((item) => {
1269
- const key = preprVariantProps(item)["data-prepr-variant-key"];
1112
+ const key = preprVariantProps(item)["data-prepr-variant-key"] ?? nestedKeyOf(item);
1270
1113
  return key ? [key] : [];
1271
1114
  });
1272
1115
  }
1273
- function renderBuckets(items, render, options) {
1274
- const {
1275
- bucket,
1276
- decisions,
1277
- abTest,
1278
- index,
1279
- track,
1280
- placeholder = false,
1281
- defaultVariant = true
1282
- } = options;
1283
- const renderVariant = (variant, key) => {
1284
- const resolved = resolveVariants(items, { decisions, bucket: variant });
1285
- const ab = abTest && !bucket ? variant : void 0;
1286
- const content = render(resolved, {
1287
- index,
1288
- placeholder,
1289
- defaultVariant: defaultVariant && (ab === void 0 || ab === "A")
1290
- });
1291
- const variantKeys = track ? variantKeysOf(resolved) : [];
1292
- if (variantKeys.length > 0) {
1293
- return createElement(
1294
- PreprVariantTracking,
1295
- { key, variantKeys, ab },
1296
- content
1297
- );
1116
+ function nestedKeyOf(value) {
1117
+ if (!value || typeof value !== "object") return void 0;
1118
+ for (const child of Object.values(value)) {
1119
+ const items = Array.isArray(child) ? child : [child];
1120
+ for (const item of items) {
1121
+ const key = contextOf(item)?.variant_key ?? (item && nestedKeyOf(item));
1122
+ if (key) return key;
1298
1123
  }
1299
- if (ab) return createElement("div", { key, [AB_ATTRIBUTE]: ab }, content);
1300
- return content;
1301
- };
1302
- if (!abTest || bucket) return renderVariant(bucket);
1303
- return AB_BUCKETS.map((variant) => renderVariant(variant, variant));
1124
+ }
1125
+ return void 0;
1304
1126
  }
1305
- function renderAllVariants(block, render, options) {
1306
- const group = block.key.slice("group:".length);
1307
- const variants = [
1308
- ...new Set(
1309
- block.items.flatMap((item) => {
1310
- const ctx = item[PREPR_CONTEXT_ALIAS];
1311
- return ctx?.variant_id ? [ctx.variant_id] : [];
1312
- })
1313
- )
1314
- ];
1315
- return variants.map(
1316
- (variant) => createElement(
1127
+ function renderItems(items, render, options) {
1128
+ const resolved = resolveVariants(items, {
1129
+ decisions: options.decisions,
1130
+ bucket: options.bucket
1131
+ });
1132
+ const content = render(resolved, {
1133
+ index: options.index,
1134
+ defaultVariant: options.defaultVariant
1135
+ });
1136
+ const variantKeys = options.track ? variantKeysOf(resolved) : [];
1137
+ return variantKeys.length > 0 ? createElement(PreprVariantTracking, { variantKeys }, content) : content;
1138
+ }
1139
+ function decidedGroups(items, bucket) {
1140
+ const groups = /* @__PURE__ */ new Map();
1141
+ const walk = (value) => {
1142
+ if (Array.isArray(value)) return value.forEach(walk);
1143
+ if (!value || typeof value !== "object") return;
1144
+ const ctx = contextOf(value);
1145
+ const decided = ctx?.kind === "PERSONALIZATION" || ctx?.kind === "AB_TEST" && !bucket;
1146
+ if (decided && ctx.variant_id) {
1147
+ const group = groups.get(ctx.group_id) ?? {
1148
+ variants: /* @__PURE__ */ new Set(),
1149
+ fallback: ctx.kind === "AB_TEST" ? "A" : PREPR_DEFAULT_VARIANT
1150
+ };
1151
+ group.variants.add(ctx.variant_id);
1152
+ groups.set(ctx.group_id, group);
1153
+ }
1154
+ Object.values(value).forEach(walk);
1155
+ };
1156
+ walk(items);
1157
+ return groups;
1158
+ }
1159
+ function renderVariants(block, groups, render, options) {
1160
+ const top = block.key.startsWith("group:") ? block.key.slice("group:".length) : void 0;
1161
+ const nested = !(top !== void 0 && groups.size === 1);
1162
+ const choices = [...groups].map(([group, { variants, fallback }]) => {
1163
+ const ids = nested ? [fallback, ...variants] : [...variants];
1164
+ return [...new Set(ids)].map((variant) => [group, variant]);
1165
+ });
1166
+ const combinations = choices.reduce(
1167
+ (all, pairs) => all.flatMap((combo) => pairs.map((p) => [...combo, p])),
1168
+ [[]]
1169
+ );
1170
+ return combinations.map((combination) => {
1171
+ const pairs = combination.map(([group, variant]) => `${group}:${variant}`);
1172
+ const isDefault = combination.every(
1173
+ ([group, variant]) => variant === groups.get(group).fallback
1174
+ );
1175
+ return createElement(
1317
1176
  "div",
1318
1177
  {
1319
- key: variant,
1320
- [GROUP_ATTRIBUTE]: group,
1321
- [VARIANT_ATTRIBUTE]: variant,
1322
- ...variant === PREPR_DEFAULT_VARIANT ? { [DEFAULT_ATTRIBUTE]: "" } : {}
1178
+ key: pairs.join(" "),
1179
+ [VARIANT_ATTRIBUTE]: pairs.join(" "),
1180
+ ...isDefault ? { [DEFAULT_ATTRIBUTE]: "" } : {},
1181
+ ...nested ? { [NESTED_ATTRIBUTE]: "" } : {},
1182
+ // The decision script marks the shown wrapper before hydration.
1183
+ suppressHydrationWarning: true
1323
1184
  },
1324
- renderBuckets(block.items, render, {
1185
+ renderItems(block.items, render, {
1325
1186
  bucket: options.bucket,
1326
- decisions: /* @__PURE__ */ new Map([[group, [variant]]]),
1327
- abTest: false,
1187
+ decisions: new Map(combination.map(([g, v]) => [g, [v]])),
1328
1188
  index: block.index,
1329
1189
  track: options.track,
1330
- defaultVariant: variant === PREPR_DEFAULT_VARIANT
1190
+ defaultVariant: isDefault
1331
1191
  })
1332
- )
1333
- );
1334
- }
1335
- async function PersonalizedBlock({
1336
- items,
1337
- decisions,
1338
- bucket,
1339
- abTest,
1340
- index,
1341
- render,
1342
- track
1343
- }) {
1344
- const decided = await decisions.catch((error) => {
1345
- if (isFrameworkSignal(error)) throw error;
1346
- return void 0;
1347
- });
1348
- return renderBuckets(items, render, {
1349
- bucket,
1350
- decisions: decided,
1351
- abTest,
1352
- index,
1353
- track
1192
+ );
1354
1193
  });
1355
1194
  }
1356
1195
  function PreprStack({
1357
1196
  items,
1358
- decisions,
1359
1197
  render,
1360
1198
  bucket,
1361
1199
  nonce,
1362
- trackVariants = false,
1363
- aboveTheFold = 1
1200
+ trackVariants = false
1364
1201
  }) {
1365
- decisions.catch(() => void 0);
1366
- const blocks = splitStack(items);
1367
- const decidedInProxy = (block) => block.index < aboveTheFold && block.personalized && !block.abTest && block.key.startsWith("group:");
1368
- const hasDecided = blocks.some(decidedInProxy);
1369
- const needsSwitch = !bucket && blocks.some((block) => block.abTest) || hasDecided;
1202
+ const blocks = splitStack(items).map((block) => ({
1203
+ ...block,
1204
+ groups: decidedGroups(block.items, bucket)
1205
+ }));
1206
+ const varies = blocks.some((block) => block.groups.size > 0);
1370
1207
  return createElement(
1371
1208
  Fragment,
1372
1209
  null,
1373
- needsSwitch && createElement(
1210
+ varies && createElement(
1374
1211
  Fragment,
1375
- { key: "prepr-ab" },
1212
+ { key: "prepr-switch" },
1376
1213
  createElement("style", {
1377
1214
  nonce,
1378
- dangerouslySetInnerHTML: {
1379
- __html: AB_BASE_CSS + (hasDecided ? GROUP_BASE_CSS : "")
1380
- }
1215
+ dangerouslySetInnerHTML: { __html: GROUP_BASE_CSS }
1216
+ }),
1217
+ createElement("noscript", {
1218
+ dangerouslySetInnerHTML: { __html: NOSCRIPT_HTML }
1381
1219
  }),
1382
1220
  createElement("script", {
1383
1221
  nonce,
1384
- dangerouslySetInnerHTML: { __html: AB_SWITCH_SCRIPT }
1222
+ dangerouslySetInnerHTML: { __html: DECISION_SCRIPT }
1385
1223
  }),
1386
- createElement(PreprAbSwitch),
1387
- hasDecided && createElement(PreprPersonalizationSwitch, { refresh: false })
1224
+ createElement(PreprDecisionApplier),
1225
+ // Marked decided by the decision switch; never laid out.
1226
+ createElement("template", {
1227
+ "data-prepr-decision": "",
1228
+ suppressHydrationWarning: true
1229
+ })
1388
1230
  ),
1389
1231
  ...blocks.map((block) => {
1390
- if (decidedInProxy(block)) {
1232
+ if (block.groups.size === 0) {
1391
1233
  return createElement(
1392
1234
  Fragment,
1393
1235
  { key: block.key },
1394
- renderAllVariants(block, render, { bucket, track: trackVariants })
1236
+ renderItems(block.items, render, {
1237
+ bucket,
1238
+ index: block.index,
1239
+ track: trackVariants,
1240
+ defaultVariant: true
1241
+ })
1395
1242
  );
1396
1243
  }
1397
- const fallback = renderBuckets(block.items, render, {
1398
- bucket,
1399
- abTest: block.abTest,
1400
- index: block.index,
1401
- track: trackVariants && !block.personalized,
1402
- placeholder: block.personalized
1403
- });
1404
- if (!block.personalized) {
1405
- return createElement(Fragment, { key: block.key }, fallback);
1406
- }
1407
1244
  return createElement(
1408
- Suspense,
1409
- { key: block.key, fallback },
1410
- createElement(PersonalizedBlock, {
1411
- items: block.items,
1412
- decisions,
1245
+ Fragment,
1246
+ { key: block.key },
1247
+ renderVariants(block, block.groups, render, {
1413
1248
  bucket,
1414
- abTest: block.abTest,
1415
- index: block.index,
1416
- render,
1417
1249
  track: trackVariants
1250
+ }),
1251
+ createElement("script", {
1252
+ nonce,
1253
+ dangerouslySetInnerHTML: { __html: APPLY_SCRIPT }
1418
1254
  })
1419
1255
  );
1420
1256
  })
@@ -1426,33 +1262,59 @@ function createPreprClient2(options) {
1426
1262
  const { cacheTags = ["prepr"], revalidate, ...coreOptions } = options;
1427
1263
  const client = createPreprClient({
1428
1264
  // The schema introspection goes through the Data Cache: shared across
1429
- // server instances, refreshed hourly and by revalidateTag on publish.
1265
+ // server instances, refreshed hourly and by a `schema.changed` webhook.
1430
1266
  schemaInit: {
1431
- next: { tags: cacheTags, revalidate: 3600 }
1267
+ next: { tags: [...cacheTags, PREPR_SCHEMA_TAG], revalidate: 3600 }
1432
1268
  },
1433
1269
  ...coreOptions
1434
1270
  });
1435
- const seenGroups = /* @__PURE__ */ new Map();
1436
1271
  const nextClient = {
1437
1272
  async query(document2, variables, queryOptions = {}) {
1438
- const { personalize = false, ...rest } = queryOptions;
1439
- const init = rest.init;
1440
- const tagged = {
1273
+ const { personalize = false, tagItems = false, ...rest } = queryOptions;
1274
+ const { next, ...init } = rest.init ?? {};
1275
+ if (tagItems && typeof nextCache.cacheTag !== "function") {
1276
+ throw new Error("tagItems requires Next.js 16 or later.");
1277
+ }
1278
+ const options2 = {
1441
1279
  ...rest,
1442
- init: {
1280
+ // With tagItems, `next` is dropped too: tags or revalidate alongside
1281
+ // `no-store` are a conflict Next.js resolves by ignoring the store.
1282
+ // Otherwise tags are attached unconditionally; the core client drops
1283
+ // them again whenever the request ends up `no-store` (personalized,
1284
+ // per-request headers, or preview).
1285
+ init: tagItems ? { ...init, cache: "no-store" } : {
1443
1286
  ...init,
1444
1287
  next: {
1445
- tags: cacheTags,
1446
1288
  ...revalidate === void 0 ? {} : { revalidate },
1447
- ...init?.next
1289
+ ...next,
1290
+ // Merged, so a per-query tag never drops the client's own,
1291
+ // which the publish webhook relies on.
1292
+ tags: [...cacheTags, ...next?.tags ?? []]
1448
1293
  }
1449
1294
  }
1450
1295
  };
1451
- if (!personalize) return client.query(document2, variables, tagged);
1452
- return client.query(document2, variables, {
1453
- ...tagged,
1454
- headers: mergePreprHeaders(await getPreprHeaders(), rest.headers)
1455
- });
1296
+ if (personalize) {
1297
+ options2.headers = mergePreprHeaders(
1298
+ await getPreprHeaders(),
1299
+ rest.headers
1300
+ );
1301
+ }
1302
+ const result = await client.query(document2, variables, options2);
1303
+ if (tagItems) {
1304
+ let modelTags;
1305
+ try {
1306
+ modelTags = preprModelTagsFor(
1307
+ documentOf(document2),
1308
+ result.data,
1309
+ await clientSchemas.get(client)()
1310
+ );
1311
+ } catch {
1312
+ modelTags = [PREPR_ANY_ITEM_TAG];
1313
+ }
1314
+ nextCache.cacheTag(...cacheTags, ...next?.tags ?? [], ...modelTags);
1315
+ nextCache.cacheTag(...preprCacheTags(result.data));
1316
+ }
1317
+ return result;
1456
1318
  },
1457
1319
  async decide(document2, variables, queryOptions = {}) {
1458
1320
  return client.decide(document2, variables, {
@@ -1463,26 +1325,13 @@ function createPreprClient2(options) {
1463
1325
  )
1464
1326
  });
1465
1327
  },
1466
- hasAbTests: (document2, variables) => client.hasAbTests(document2, variables),
1467
- resolveDecisions: (request, response, document2, variables, options2) => resolveDecisionsWith(
1468
- client,
1469
- seenGroups,
1470
- request,
1471
- response,
1472
- document2,
1473
- variables,
1474
- options2
1475
- )
1328
+ hasAbTests: (document2, variables) => client.hasAbTests(document2, variables)
1476
1329
  };
1477
- preprClientInternals.set(nextClient, {
1478
- client,
1479
- seenGroups,
1480
- url: coreOptions.url
1481
- });
1330
+ preprClientInternals.set(nextClient, { client });
1482
1331
  return nextClient;
1483
1332
  }
1484
1333
  export {
1485
- PREPR_DECISIONS_COOKIE,
1334
+ PREPR_ANY_ITEM_TAG,
1486
1335
  PREPR_DEFAULT_VARIANT,
1487
1336
  PreprError,
1488
1337
  PreprStack,
@@ -1491,6 +1340,8 @@ export {
1491
1340
  decisionsFrom,
1492
1341
  isPersonalizedResponse,
1493
1342
  loadPreprSchema,
1343
+ preprCacheTags,
1344
+ preprModelTag,
1494
1345
  preprVariantProps,
1495
1346
  resolveVariants,
1496
1347
  splitStack,