@preprio/toolkit 0.6.0-alpha.0 → 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 (59) hide show
  1. package/README.md +268 -59
  2. package/dist/astro/{chunk-3UVUDDNY.js → chunk-CG2QBHNU.js} +1 -1
  3. package/dist/astro/graphql.cjs +154 -59
  4. package/dist/astro/graphql.d.cts +75 -6
  5. package/dist/astro/graphql.d.ts +75 -6
  6. package/dist/astro/graphql.js +150 -59
  7. package/dist/astro/index.cjs +1 -1
  8. package/dist/astro/index.js +1 -1
  9. package/dist/chunk-3NCEVJJ3.js +10 -0
  10. package/dist/{chunk-BJ2LBLWO.js → chunk-LIDUJWWJ.js} +6 -4
  11. package/dist/chunk-M3RBYJ5J.js +499 -0
  12. package/dist/{chunk-DGZA7KX7.js → chunk-S5RWVMDJ.js} +0 -24
  13. package/dist/{chunk-YR7P4IWE.js → chunk-SF6TN4JR.js} +1 -9
  14. package/dist/chunk-TN6SNAR2.js +26 -0
  15. package/dist/graphql/codegen.cjs +557 -0
  16. package/dist/graphql/codegen.d.cts +23 -0
  17. package/dist/graphql/codegen.d.ts +23 -0
  18. package/dist/graphql/codegen.js +99 -0
  19. package/dist/graphql.cjs +185 -59
  20. package/dist/graphql.d.cts +110 -6
  21. package/dist/graphql.d.ts +110 -6
  22. package/dist/graphql.js +91 -430
  23. package/dist/index.cjs +1 -1
  24. package/dist/index.d.cts +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +10 -6
  27. package/dist/nextjs/chunk-B4GVZEOL.js +549 -0
  28. package/dist/nextjs/components.cjs +297 -81
  29. package/dist/nextjs/components.d.cts +26 -11
  30. package/dist/nextjs/components.d.ts +26 -11
  31. package/dist/nextjs/components.js +289 -73
  32. package/dist/nextjs/graphql.cjs +952 -315
  33. package/dist/nextjs/graphql.d.cts +225 -83
  34. package/dist/nextjs/graphql.d.ts +225 -83
  35. package/dist/nextjs/graphql.js +634 -314
  36. package/dist/nextjs/index.cjs +116 -54
  37. package/dist/nextjs/index.d.cts +61 -23
  38. package/dist/nextjs/index.d.ts +61 -23
  39. package/dist/nextjs/index.js +97 -299
  40. package/dist/nextjs/{server-CjpELYvq.d.ts → server-BL-sqZvu.d.cts} +1 -1
  41. package/dist/nextjs/{server-CjpELYvq.d.cts → server-BL-sqZvu.d.ts} +1 -1
  42. package/dist/nuxt/{chunk-P6ENNAEE.js → chunk-FMC3DP7N.js} +1 -1
  43. package/dist/nuxt/graphql.cjs +154 -59
  44. package/dist/nuxt/graphql.d.cts +75 -6
  45. package/dist/nuxt/graphql.d.ts +75 -6
  46. package/dist/nuxt/graphql.js +150 -59
  47. package/dist/nuxt/index.cjs +1 -1
  48. package/dist/nuxt/index.js +1 -1
  49. package/dist/react/components.cjs +1 -1
  50. package/dist/react/components.js +1 -1
  51. package/dist/sveltekit/{chunk-FSARVMG5.js → chunk-AJPKBAHO.js} +1 -1
  52. package/dist/sveltekit/graphql.cjs +154 -59
  53. package/dist/sveltekit/graphql.d.cts +75 -6
  54. package/dist/sveltekit/graphql.d.ts +75 -6
  55. package/dist/sveltekit/graphql.js +150 -59
  56. package/dist/sveltekit/index.cjs +1 -1
  57. package/dist/sveltekit/index.js +1 -1
  58. package/package.json +6 -1
  59. package/dist/nextjs/chunk-U4HXENF7.js +0 -231
@@ -1,10 +1,20 @@
1
1
  import {
2
+ PREPR_ANY_ITEM_TAG,
3
+ PREPR_SCHEMA_TAG,
2
4
  PreprError,
5
+ createPreprMiddleware,
3
6
  getPreprHeaders,
4
7
  getPreprHeadersFromHeaders,
5
8
  isPersonalizedResponse,
9
+ preprCacheTags,
10
+ preprDraftRedirect,
11
+ preprModelTag,
12
+ preprModelTagsFor,
6
13
  validatePreprToken
7
- } from "./chunk-U4HXENF7.js";
14
+ } from "./chunk-B4GVZEOL.js";
15
+
16
+ // src/nextjs/graphql.ts
17
+ import * as nextCache from "next/cache";
8
18
 
9
19
  // src/core/variants/constants.ts
10
20
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -93,7 +103,10 @@ function decisionSelectionSet(set, typeName, path, ctx) {
93
103
  path,
94
104
  ctx
95
105
  );
96
- if (pruned) {
106
+ if (!pruned) continue;
107
+ if (fragment.typeCondition.name.value === typeName) {
108
+ selections.push(...pruned.selections);
109
+ } else {
97
110
  selections.push(
98
111
  inlineFragment(fragment.typeCondition.name.value, [
99
112
  ...pruned.selections
@@ -103,13 +116,19 @@ function decisionSelectionSet(set, typeName, path, ctx) {
103
116
  continue;
104
117
  }
105
118
  if (selection.kind === "InlineFragment") {
119
+ const condition = selection.typeCondition?.name.value ?? typeName;
106
120
  const pruned = decisionSelectionSet(
107
121
  selection.selectionSet,
108
- selection.typeCondition?.name.value ?? typeName,
122
+ condition,
109
123
  path,
110
124
  ctx
111
125
  );
112
- 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
+ }
113
132
  continue;
114
133
  }
115
134
  if (!selection.selectionSet) continue;
@@ -183,20 +202,58 @@ function toDecisionDocument(document2, schema, groupPaths, options) {
183
202
  const used = /* @__PURE__ */ new Set();
184
203
  collectVariables(pruned, used);
185
204
  collectVariables(operation.directives, used);
205
+ const shared = sharedContextFragment(schema);
186
206
  return {
187
207
  kind: "Document",
188
208
  definitions: [
209
+ ...shared ? [shared.definition] : [],
189
210
  {
190
211
  ...operation,
191
212
  name: operation.name && name(`${operation.name.value}Decision`),
192
213
  variableDefinitions: (operation.variableDefinitions ?? []).filter(
193
214
  (definition) => used.has(definition.variable.name.value)
194
215
  ),
195
- selectionSet: pruned
216
+ selectionSet: shared ? shared.apply(pruned) : pruned
196
217
  }
197
218
  ]
198
219
  };
199
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
+ }
200
257
 
201
258
  // src/core/variants/probe-document.ts
202
259
  function probeSelectionSet(set, fragments) {
@@ -483,6 +540,63 @@ function toVariantsDocument(document2, schema) {
483
540
  };
484
541
  }
485
542
 
543
+ // src/core/graphql/document.ts
544
+ import { print } from "graphql/language/printer.js";
545
+ import { stripIgnoredCharacters } from "graphql/utilities/stripIgnoredCharacters.js";
546
+ var printed = /* @__PURE__ */ new WeakMap();
547
+ function isDocumentNode(value) {
548
+ return typeof value === "object" && value !== null && value.kind === "Document" && Array.isArray(value.definitions);
549
+ }
550
+ function invalidDocument(originalError) {
551
+ return new PreprError(
552
+ "Expected a GraphQL query string, TypedDocumentString or DocumentNode",
553
+ "INVALID_DOCUMENT",
554
+ "createPreprClient",
555
+ originalError
556
+ );
557
+ }
558
+ var minified = /* @__PURE__ */ new Map();
559
+ var MINIFIED_CACHE_LIMIT = 500;
560
+ function minify(query) {
561
+ const cached = minified.get(query);
562
+ if (cached !== void 0) return cached;
563
+ let result = query;
564
+ try {
565
+ result = stripIgnoredCharacters(query);
566
+ } catch {
567
+ }
568
+ if (minified.size < MINIFIED_CACHE_LIMIT) minified.set(query, result);
569
+ return result;
570
+ }
571
+ function resolveQuery(document2) {
572
+ if (typeof document2 === "string") return minify(document2);
573
+ if (document2 instanceof String) return minify(document2.toString());
574
+ if (!isDocumentNode(document2)) throw invalidDocument();
575
+ let query = printed.get(document2);
576
+ if (query === void 0) {
577
+ try {
578
+ query = minify(print(document2));
579
+ } catch (error) {
580
+ throw invalidDocument(error instanceof Error ? error : void 0);
581
+ }
582
+ printed.set(document2, query);
583
+ }
584
+ return query;
585
+ }
586
+
587
+ // src/core/variants/queries.ts
588
+ function toVariantsQuery(document2, schema) {
589
+ return resolveQuery(toVariantsDocument(document2, schema));
590
+ }
591
+ function toDecisionQuery(document2, schema, options = {}) {
592
+ const paths = options.from === void 0 ? void 0 : personalizationPaths(options.from);
593
+ if (paths && paths.size === 0) return null;
594
+ const decision = toDecisionDocument(document2, schema, paths, {
595
+ topLevelOnly: !paths && options.topLevelOnly
596
+ });
597
+ return decision ? resolveQuery(decision) : null;
598
+ }
599
+
486
600
  // src/core/graphql/transport.ts
487
601
  import { VERCEL_STEGA_REGEX } from "@vercel/stega";
488
602
 
@@ -535,50 +649,6 @@ function stegaClean(text) {
535
649
  return vercelStegaClean(text);
536
650
  }
537
651
 
538
- // src/core/graphql/document.ts
539
- import { print } from "graphql/language/printer.js";
540
- import { stripIgnoredCharacters } from "graphql/utilities/stripIgnoredCharacters.js";
541
- var printed = /* @__PURE__ */ new WeakMap();
542
- function isDocumentNode(value) {
543
- return typeof value === "object" && value !== null && value.kind === "Document" && Array.isArray(value.definitions);
544
- }
545
- function invalidDocument(originalError) {
546
- return new PreprError(
547
- "Expected a GraphQL query string, TypedDocumentString or DocumentNode",
548
- "INVALID_DOCUMENT",
549
- "createPreprClient",
550
- originalError
551
- );
552
- }
553
- var minified = /* @__PURE__ */ new Map();
554
- var MINIFIED_CACHE_LIMIT = 500;
555
- function minify(query) {
556
- const cached = minified.get(query);
557
- if (cached !== void 0) return cached;
558
- let result = query;
559
- try {
560
- result = stripIgnoredCharacters(query);
561
- } catch {
562
- }
563
- if (minified.size < MINIFIED_CACHE_LIMIT) minified.set(query, result);
564
- return result;
565
- }
566
- function resolveQuery(document2) {
567
- if (typeof document2 === "string") return minify(document2);
568
- if (document2 instanceof String) return minify(document2.toString());
569
- if (!isDocumentNode(document2)) throw invalidDocument();
570
- let query = printed.get(document2);
571
- if (query === void 0) {
572
- try {
573
- query = minify(print(document2));
574
- } catch (error) {
575
- throw invalidDocument(error instanceof Error ? error : void 0);
576
- }
577
- printed.set(document2, query);
578
- }
579
- return query;
580
- }
581
-
582
652
  // src/core/graphql/transport.ts
583
653
  var OPERATION_NAME = /\b(?:query|mutation|subscription)\s+([_A-Za-z][_0-9A-Za-z]*)/;
584
654
  var STEGA_RAW = new RegExp(
@@ -757,6 +827,7 @@ function createVaryProbe(run, options) {
757
827
  }
758
828
 
759
829
  // src/core/graphql/client.ts
830
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
760
831
  function isParseableUrl(url) {
761
832
  try {
762
833
  new URL(url);
@@ -765,16 +836,18 @@ function isParseableUrl(url) {
765
836
  return false;
766
837
  }
767
838
  }
768
- function createPreprClient(options) {
769
- const { url } = options;
839
+ function assertPreprUrl(url, source) {
770
840
  validatePreprToken(url);
771
841
  if (!isParseableUrl(url)) {
772
842
  throw new PreprError(
773
843
  "Prepr GraphQL URL is not a valid URL",
774
844
  "INVALID_TOKEN",
775
- "createPreprClient"
845
+ source
776
846
  );
777
847
  }
848
+ }
849
+ function createPreprClient(options) {
850
+ assertPreprUrl(options.url, "createPreprClient");
778
851
  const run = createTransport(options);
779
852
  const probeVary = createVaryProbe(run, options);
780
853
  let schema;
@@ -791,7 +864,7 @@ function createPreprClient(options) {
791
864
  ));
792
865
  const variantsDocuments = /* @__PURE__ */ new Map();
793
866
  const decisionDocuments = /* @__PURE__ */ new Map();
794
- return {
867
+ const client = {
795
868
  async query(document2, variables, queryOptions = {}) {
796
869
  const { variants, ...rest } = queryOptions;
797
870
  if (!variants) return run(document2, variables, rest);
@@ -803,10 +876,10 @@ function createPreprClient(options) {
803
876
  return run(derived, variables, rest);
804
877
  },
805
878
  async decide(document2, variables, queryOptions = {}) {
806
- const { from, topLevelOnly, ...rest } = queryOptions;
879
+ const { from, ...rest } = queryOptions;
807
880
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
808
881
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
809
- 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("|") : "*";
810
883
  let byPaths = decisionDocuments.get(document2);
811
884
  if (!byPaths) {
812
885
  byPaths = /* @__PURE__ */ new Map();
@@ -814,9 +887,7 @@ function createPreprClient(options) {
814
887
  }
815
888
  let derived = byPaths.get(signature);
816
889
  if (derived === void 0) {
817
- derived = toDecisionDocument(document2, await loadSchema(), paths, {
818
- topLevelOnly: !paths && topLevelOnly
819
- });
890
+ derived = toDecisionDocument(document2, await loadSchema(), paths);
820
891
  byPaths.set(signature, derived);
821
892
  }
822
893
  if (!derived) return /* @__PURE__ */ new Map();
@@ -830,28 +901,91 @@ function createPreprClient(options) {
830
901
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
831
902
  )
832
903
  };
904
+ clientSchemas.set(client, loadSchema);
905
+ return client;
906
+ }
907
+
908
+ // src/core/graphql/schema-loader.ts
909
+ var schemas = /* @__PURE__ */ new Map();
910
+ async function loadPreprSchema(url, options = {}) {
911
+ assertPreprUrl(url, "loadPreprSchema");
912
+ let schema = schemas.get(url);
913
+ if (!schema) {
914
+ const run = createTransport({ url, fetch: options.fetch, stega: "none" });
915
+ schema = run(
916
+ PREPR_SCHEMA_QUERY
917
+ ).then((result) => toSchemaInfo(result.data)).catch((error) => {
918
+ schemas.delete(url);
919
+ if (error instanceof PreprError) throw error;
920
+ throw new PreprError(
921
+ "Prepr schema response is not an introspection result",
922
+ "INVALID_RESPONSE",
923
+ "loadPreprSchema",
924
+ error instanceof Error ? error : void 0
925
+ );
926
+ });
927
+ schemas.set(url, schema);
928
+ }
929
+ return schema;
833
930
  }
834
931
 
835
932
  // src/nextjs/decisions.ts
933
+ var PREPR_DECIDE_HEADER = "Prepr-Decide";
934
+ var DEFAULT_DECISION = "*";
836
935
  var PREPR_DECISIONS_COOKIE = "prepr_decisions";
837
- var DECISIONS_MAX_GROUPS = 40;
838
- var SAFE_ID = /^[\w-]+$/;
839
- var SEEN_MAX_PAGES = 1e3;
840
- function parseDecisions(raw) {
841
- const out = /* @__PURE__ */ new Map();
842
- for (const pair of (raw ?? "").split("~")) {
843
- const at = pair.indexOf(":");
844
- const group = pair.slice(0, at);
845
- if (at < 1 || !SAFE_ID.test(group)) continue;
846
- out.set(
847
- group,
848
- pair.slice(at + 1).split("|").filter((variant) => SAFE_ID.test(variant))
849
- );
936
+ var DECISION_FRESH_MS = 1e4;
937
+ var DECISION_KEEP_MS = 18e5;
938
+ var DECISION_KEEP_STATIC_MS = 36e5;
939
+ var STORED_MAX_ENTRIES = 20;
940
+ var STORED_MAX_LENGTH = 3e3;
941
+ function readStoredDecisions(raw) {
942
+ const stored = /* @__PURE__ */ Object.create(null);
943
+ if (!raw) return stored;
944
+ try {
945
+ const parsed = JSON.parse(raw);
946
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
947
+ return stored;
948
+ for (const [key, entry] of Object.entries(parsed)) {
949
+ if (key.startsWith("/") && Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number") {
950
+ stored[key] = [entry[0], entry[1]];
951
+ }
952
+ }
953
+ } catch {
850
954
  }
851
- return out;
955
+ return stored;
852
956
  }
957
+ function usableDecision(stored, key, now) {
958
+ const entry = stored[key];
959
+ if (!entry) return void 0;
960
+ const [value, at] = entry;
961
+ const age = now - at;
962
+ const keep = value === DEFAULT_DECISION ? DECISION_FRESH_MS : value === "" ? DECISION_KEEP_STATIC_MS : DECISION_KEEP_MS;
963
+ return age >= 0 && age < keep ? value : void 0;
964
+ }
965
+ function writeStoredDecisions(stored, key, value, now) {
966
+ const kept = usableDecision(stored, key, now);
967
+ const entries = Object.entries(stored).filter(
968
+ ([stale]) => stale !== key && usableDecision(stored, stale, now) !== void 0
969
+ );
970
+ entries.push([
971
+ key,
972
+ value === DEFAULT_DECISION && kept !== void 0 && kept !== DEFAULT_DECISION ? stored[key] : [value, now]
973
+ ]);
974
+ entries.sort((a, b) => b[1][1] - a[1][1]);
975
+ entries.length = Math.min(entries.length, STORED_MAX_ENTRIES);
976
+ let json = JSON.stringify(Object.fromEntries(entries));
977
+ while (entries.length > 1 && encodeURIComponent(json).length > STORED_MAX_LENGTH) {
978
+ entries.pop();
979
+ json = JSON.stringify(Object.fromEntries(entries));
980
+ }
981
+ return json;
982
+ }
983
+ var DECISIONS_MAX_GROUPS = 40;
984
+ var SAFE_ID = /^[\w-]+$/;
853
985
  function serializeDecisions(decisions) {
854
- return [...decisions].slice(0, DECISIONS_MAX_GROUPS).map(([group, variants]) => `${group}:${variants.join("|")}`).join("~");
986
+ return [...decisions].filter(([group]) => SAFE_ID.test(group)).slice(0, DECISIONS_MAX_GROUPS).map(
987
+ ([group, variants]) => `${group}:${variants.filter((v) => SAFE_ID.test(v)).join("|")}`
988
+ ).join("~");
855
989
  }
856
990
  function forwardedHeaders(request, response) {
857
991
  const overridden = response.headers.get("x-middleware-override-headers");
@@ -863,288 +997,448 @@ function forwardedHeaders(request, response) {
863
997
  }
864
998
  return getPreprHeadersFromHeaders(headers);
865
999
  }
866
- async function resolveDecisionsWith(client, seenGroups, request, response, document2, variables, options) {
1000
+ var preprClientInternals = /* @__PURE__ */ new WeakMap();
1001
+ async function decideView(client, headers, page, timeoutMs = 1e3) {
1002
+ const init = { signal: AbortSignal.timeout(timeoutMs) };
867
1003
  try {
868
- if (options?.onlyIf && !await options.onlyIf()) return;
869
- if (!await client.hasSegments(document2, variables)) return;
870
- const headers = mergePreprHeaders(forwardedHeaders(request, response));
871
- const stored = parseDecisions(
872
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1004
+ const decided = page.decision ? decisionsFrom(
1005
+ (await client.query(page.decision, page.variables, { headers, init })).data
1006
+ ) : await client.decide(page.document, page.variables, { headers, init });
1007
+ return serializeDecisions(decided);
1008
+ } catch (error) {
1009
+ if (isFrameworkSignal(error)) throw error;
1010
+ return DEFAULT_DECISION;
1011
+ }
1012
+ }
1013
+
1014
+ // src/nextjs/proxy.ts
1015
+ import { NextResponse } from "next/server";
1016
+ function isRouterFetch(request) {
1017
+ return request.headers.has("rsc") || request.headers.has("next-url") || request.headers.get("sec-fetch-dest") === "empty";
1018
+ }
1019
+ var STATIC_PAGE_MS = 3e5;
1020
+ var STATIC_PAGES_MAX = 1e3;
1021
+ var staticPages = /* @__PURE__ */ new Map();
1022
+ function isPagePath(pathname) {
1023
+ return !pathname.startsWith("/_next/") && !pathname.startsWith("/api/") && !/\.[^/]*$/.test(pathname);
1024
+ }
1025
+ function createPreprProxy(options) {
1026
+ const internals = preprClientInternals.get(options.client);
1027
+ if (!internals) {
1028
+ throw new PreprError(
1029
+ "createPreprProxy needs a client made by createPreprClient from @preprio/toolkit/nextjs/graphql",
1030
+ "CONTEXT_ERROR",
1031
+ "createPreprProxy"
873
1032
  );
874
- const pageKey = `${JSON.stringify(variables ?? null)}`;
875
- let byVariables = seenGroups.get(document2);
876
- if (!byVariables) {
877
- byVariables = /* @__PURE__ */ new Map();
878
- seenGroups.set(document2, byVariables);
1033
+ }
1034
+ const { client } = internals;
1035
+ const preview = options.preview ?? false;
1036
+ const allowed = async (request) => {
1037
+ if (!options.onlyIf) return true;
1038
+ try {
1039
+ return await options.onlyIf(request);
1040
+ } catch {
1041
+ return false;
879
1042
  }
880
- const known = byVariables.get(pageKey);
881
- const isNewVisitor = headers.get("Prepr-Customer-Id-Created") === "true";
882
- const contextChanged = isNewVisitor || headers.has("Prepr-Segments") || [...request.nextUrl.searchParams.keys()].some(
883
- (key) => key.startsWith("utm_")
884
- );
885
- if (options?.reuse !== false && !contextChanged && known && [...known].every((group) => stored.has(group))) {
886
- return;
1043
+ };
1044
+ const lookup = (target) => {
1045
+ try {
1046
+ return options.page(target) ?? null;
1047
+ } catch {
1048
+ return null;
887
1049
  }
888
- const decided = await client.decide(document2, variables, {
889
- topLevelOnly: true,
890
- headers,
891
- init: { signal: AbortSignal.timeout(options?.timeoutMs ?? 1e3) }
892
- });
893
- if (!known && byVariables.size >= SEEN_MAX_PAGES) {
894
- byVariables.delete(byVariables.keys().next().value);
1050
+ };
1051
+ async function answerDecision(request, response, target) {
1052
+ let decision = DEFAULT_DECISION;
1053
+ const site = request.headers.get("sec-fetch-site");
1054
+ if (target && request.method === "GET" && (site === null || site === "same-origin") && await allowed(request)) {
1055
+ decision = await decideView(
1056
+ client,
1057
+ mergePreprHeaders(forwardedHeaders(request, response)),
1058
+ target,
1059
+ options.timeoutMs
1060
+ );
895
1061
  }
896
- const groups = known ?? /* @__PURE__ */ new Set();
897
- for (const group of decided.keys()) groups.add(group);
898
- byVariables.set(pageKey, groups);
899
- const next = /* @__PURE__ */ new Map();
900
- for (const [group, variants] of decided) {
901
- if (SAFE_ID.test(group)) {
902
- next.set(
903
- group,
904
- variants.filter((v) => SAFE_ID.test(v))
905
- );
1062
+ const answer = new NextResponse(decision, {
1063
+ headers: {
1064
+ "Cache-Control": "private, no-store",
1065
+ "Content-Type": "text/plain; charset=utf-8"
906
1066
  }
1067
+ });
1068
+ for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1069
+ return answer;
1070
+ }
1071
+ async function decidePageLoad(request, response, target) {
1072
+ const { basePath, pathname, search } = request.nextUrl;
1073
+ const key = basePath + pathname + search;
1074
+ const now = Date.now();
1075
+ if ((staticPages.get(key) ?? 0) > now) return response;
1076
+ const stored = readStoredDecisions(
1077
+ request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1078
+ );
1079
+ if (!preview && usableDecision(stored, key, now) !== void 0)
1080
+ return response;
1081
+ if (!await allowed(request)) return response;
1082
+ const decision = await decideView(
1083
+ client,
1084
+ mergePreprHeaders(forwardedHeaders(request, response)),
1085
+ target,
1086
+ options.timeoutMs
1087
+ );
1088
+ if (decision === "") {
1089
+ if (staticPages.size >= STATIC_PAGES_MAX) staticPages.clear();
1090
+ staticPages.set(key, now + STATIC_PAGE_MS);
1091
+ return response;
907
1092
  }
908
- for (const [group, variants] of stored) {
909
- if (!next.has(group)) next.set(group, variants);
1093
+ response.cookies.set(
1094
+ PREPR_DECISIONS_COOKIE,
1095
+ writeStoredDecisions(stored, key, decision, now),
1096
+ {
1097
+ path: "/",
1098
+ maxAge: 3600,
1099
+ sameSite: "lax",
1100
+ secure: request.nextUrl.protocol === "https:"
1101
+ }
1102
+ );
1103
+ return response;
1104
+ }
1105
+ return async (request) => {
1106
+ const rendersPage = (request.method === "GET" || request.method === "HEAD") && isPagePath(request.nextUrl.pathname);
1107
+ const target = rendersPage ? lookup(request.nextUrl) : null;
1108
+ const decisionRequest = request.headers.has(PREPR_DECIDE_HEADER);
1109
+ if (target && preview && !decisionRequest) {
1110
+ const draft = preprDraftRedirect(request, { preview });
1111
+ if (draft) return draft;
910
1112
  }
911
- if (next.size === 0) return;
912
- response.cookies.set(PREPR_DECISIONS_COOKIE, serializeDecisions(next), {
913
- path: "/",
914
- sameSite: "lax",
915
- // Read by the inline switch script before first paint.
916
- httpOnly: false
1113
+ const response = createPreprMiddleware(request, {
1114
+ preview,
1115
+ features: options.features
917
1116
  });
918
- } catch (error) {
919
- if (isFrameworkSignal(error)) throw error;
920
- }
1117
+ if (decisionRequest) return answerDecision(request, response, target);
1118
+ if (target && request.method === "GET" && !isRouterFetch(request))
1119
+ return decidePageLoad(request, response, target);
1120
+ return response;
1121
+ };
921
1122
  }
922
1123
 
923
1124
  // src/nextjs/stack.ts
924
1125
  import {
925
1126
  createElement,
926
- Fragment,
927
- Suspense
1127
+ Fragment
928
1128
  } from "react";
929
1129
 
930
- // src/nextjs/ab-switch.ts
931
- function applyPreprAbBucket() {
932
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
933
- const bucket = match ? match[1] : "A";
1130
+ // src/nextjs/decision-switch.ts
1131
+ function applyPreprDecisions(decision, options = {}) {
934
1132
  const state = window;
935
- if (state.__preprAbBucket === bucket) return;
936
- state.__preprAbBucket = bucket;
937
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
938
- try {
939
- if (!state.__preprAbSheet) {
940
- const sheet = new CSSStyleSheet();
941
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
942
- state.__preprAbSheet = sheet;
943
- }
944
- state.__preprAbSheet.replaceSync(css);
945
- } catch {
946
- if (!state.__preprAbSheet) {
947
- state.__preprAbSheet = document.createElement("style");
948
- document.head.appendChild(state.__preprAbSheet);
949
- }
950
- state.__preprAbSheet.textContent = css;
951
- }
952
- }
953
- function applyPreprDecisions() {
954
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
955
- let raw;
956
- try {
957
- raw = match ? decodeURIComponent(match[1]) : "";
958
- } catch {
959
- raw = "";
1133
+ const key = location.pathname + location.search;
1134
+ const previous = state.__preprDecision;
1135
+ let value = decision;
1136
+ if (value !== void 0 && value !== "*" && options.refresh && previous && previous.key === key && previous.value !== "*") {
1137
+ const pairs = (from) => {
1138
+ const out = {};
1139
+ from.split("~").forEach((pair) => {
1140
+ const colon = pair.indexOf(":");
1141
+ if (colon > 0) out[pair.slice(0, colon)] = pair;
1142
+ });
1143
+ return out;
1144
+ };
1145
+ const before = pairs(previous.value);
1146
+ const after = pairs(value);
1147
+ document.querySelectorAll("[data-prepr-v][data-prepr-show]").forEach((element) => {
1148
+ const range = document.createRange();
1149
+ range.selectNodeContents(element);
1150
+ if (range.getBoundingClientRect().top >= window.innerHeight) return;
1151
+ (element.getAttribute("data-prepr-v") || "").split(" ").forEach((token) => {
1152
+ const group = token.slice(0, token.indexOf(":"));
1153
+ if (before[group]) after[group] = before[group];
1154
+ });
1155
+ });
1156
+ value = Object.keys(after).map((group) => after[group]).join("~");
960
1157
  }
961
- const state = window;
962
- if (state.__preprDecisions === raw) return;
963
- state.__preprDecisions = raw;
964
- const id = /^[\w-]+$/;
965
- let css = "";
966
- raw.split("~").forEach((pair) => {
967
- const at = pair.indexOf(":");
968
- const group = pair.slice(0, at);
969
- if (at < 1 || !id.test(group)) return;
970
- const selector = '[data-prepr-group="' + group + '"]';
971
- css += selector + "{display:none!important}";
972
- pair.slice(at + 1).split("|").forEach((variant) => {
973
- if (id.test(variant)) {
974
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
975
- }
1158
+ if (value !== void 0) state.__preprDecision = { key, value };
1159
+ const current = state.__preprDecision;
1160
+ if (!current || current.key !== key) return false;
1161
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
1162
+ marker.setAttribute("data-prepr-decided", "");
1163
+ });
1164
+ const defaults = current.value === "*";
1165
+ const chosen = {};
1166
+ current.value.split("~").forEach((pair) => {
1167
+ const colon = pair.indexOf(":");
1168
+ if (colon > 0)
1169
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
1170
+ });
1171
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
1172
+ const nested = element.hasAttribute("data-prepr-nested");
1173
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
1174
+ const colon = token.indexOf(":");
1175
+ const variant = token.slice(colon + 1);
1176
+ const variants = chosen[token.slice(0, colon)];
1177
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
976
1178
  });
1179
+ if (show) element.setAttribute("data-prepr-show", "");
1180
+ else element.removeAttribute("data-prepr-show");
977
1181
  });
978
- try {
979
- if (!state.__preprDecisionSheet) {
980
- const sheet = new CSSStyleSheet();
981
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
982
- state.__preprDecisionSheet = sheet;
1182
+ return true;
1183
+ }
1184
+ function requestPreprDecision(key, options = {}) {
1185
+ const fresh = 1e4;
1186
+ const keep = 18e5;
1187
+ const keepStatic = 36e5;
1188
+ const state = window;
1189
+ const asking = state.__preprAsking = state.__preprAsking || {};
1190
+ const read = () => {
1191
+ const all = {};
1192
+ try {
1193
+ const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
1194
+ const parsed = match ? JSON.parse(decodeURIComponent(match[1])) : null;
1195
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
1196
+ Object.keys(parsed).forEach((stored) => {
1197
+ const entry = parsed[stored];
1198
+ if (stored.charAt(0) === "/" && Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number")
1199
+ all[stored] = [entry[0], entry[1]];
1200
+ });
1201
+ } catch {
983
1202
  }
984
- state.__preprDecisionSheet.replaceSync(css);
985
- } catch {
986
- if (!state.__preprDecisionSheet) {
987
- state.__preprDecisionSheet = document.createElement("style");
988
- document.head.appendChild(state.__preprDecisionSheet);
1203
+ return all;
1204
+ };
1205
+ const usable = (entry, now) => {
1206
+ if (!entry) return false;
1207
+ const age = now - entry[1];
1208
+ return age >= 0 && age < (entry[0] === "*" ? fresh : entry[0] === "" ? keepStatic : keep);
1209
+ };
1210
+ const store = (answer) => {
1211
+ const all = read();
1212
+ const now = Date.now();
1213
+ const kept = all[key];
1214
+ const entries = Object.keys(all).filter((stored) => stored !== key && usable(all[stored], now)).map((stored) => [stored, all[stored]]);
1215
+ entries.push([
1216
+ key,
1217
+ answer === "*" && kept && usable(kept, now) && kept[0] !== "*" ? kept : [answer, now]
1218
+ ]);
1219
+ entries.sort((a, b) => b[1][1] - a[1][1]);
1220
+ entries.length = Math.min(entries.length, 20);
1221
+ const encode = () => {
1222
+ const out = {};
1223
+ entries.forEach((entry) => {
1224
+ out[entry[0]] = entry[1];
1225
+ });
1226
+ return encodeURIComponent(JSON.stringify(out));
1227
+ };
1228
+ let value = encode();
1229
+ while (entries.length > 1 && value.length > 3e3) {
1230
+ entries.pop();
1231
+ value = encode();
1232
+ }
1233
+ document.cookie = "prepr_decisions=" + value + "; path=/; max-age=3600; samesite=lax" + (location.protocol === "https:" ? "; secure" : "");
1234
+ };
1235
+ const ask = () => {
1236
+ if (!asking[key]) {
1237
+ asking[key] = fetch(key, {
1238
+ headers: { "Prepr-Decide": "1" },
1239
+ cache: "no-store",
1240
+ credentials: "same-origin",
1241
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1242
+ // covers a proxy that never answers.
1243
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1244
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1245
+ delete asking[key];
1246
+ try {
1247
+ store(answer);
1248
+ } catch {
1249
+ }
1250
+ return answer;
1251
+ });
1252
+ }
1253
+ return asking[key];
1254
+ };
1255
+ const last = read()[key];
1256
+ if (last && !(last[0] === "" && options.personalized) && usable(last, Date.now())) {
1257
+ const value = last[0];
1258
+ if (value !== "" && Date.now() - last[1] >= fresh) {
1259
+ ask().then((answer) => {
1260
+ if (answer !== "*" && answer !== value && options.onChange)
1261
+ options.onChange(answer);
1262
+ });
989
1263
  }
990
- state.__preprDecisionSheet.textContent = css;
1264
+ return { result: value, value: Promise.resolve(value) };
991
1265
  }
1266
+ const pending = {
1267
+ value: ask()
1268
+ };
1269
+ pending.value.then((answer) => {
1270
+ pending.result = answer;
1271
+ });
1272
+ return pending;
1273
+ }
1274
+ function startPreprDecision() {
1275
+ const state = window;
1276
+ const key = location.pathname + location.search;
1277
+ const apply = (answer, refresh) => {
1278
+ if (location.pathname + location.search === key)
1279
+ state.__preprApply(answer, { refresh });
1280
+ };
1281
+ const pending = state.__preprRequest(key, {
1282
+ onChange: (answer) => apply(answer, true),
1283
+ personalized: true
1284
+ });
1285
+ if (pending.result !== void 0) apply(pending.result);
1286
+ else pending.value.then((answer) => apply(answer));
992
1287
  }
993
1288
 
994
1289
  // src/nextjs/stack.ts
995
- import { PreprAbSwitch, PreprVariantTracking } from "./components.js";
996
- var AB_ATTRIBUTE = "data-prepr-ab";
997
- var AB_BUCKETS = ["A", "B"];
998
- var AB_BASE_CSS = `[${AB_ATTRIBUTE}]{display:contents}[${AB_ATTRIBUTE}]:not([${AB_ATTRIBUTE}="A"]){display:none}`;
999
- var AB_SWITCH_SCRIPT = `(${applyPreprAbBucket.toString()})();(${applyPreprDecisions.toString()})()`;
1000
- var GROUP_ATTRIBUTE = "data-prepr-group";
1001
- var VARIANT_ATTRIBUTE = "data-prepr-variant";
1290
+ import { PreprDecisionApplier, PreprVariantTracking } from "./components.js";
1291
+ var VARIANT_ATTRIBUTE = "data-prepr-v";
1002
1292
  var DEFAULT_ATTRIBUTE = "data-prepr-default";
1003
- var GROUP_BASE_CSS = `[${GROUP_ATTRIBUTE}]{display:contents}[${GROUP_ATTRIBUTE}]:not([${DEFAULT_ATTRIBUTE}]){display:none}`;
1293
+ var NESTED_ATTRIBUTE = "data-prepr-nested";
1294
+ 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}`;
1295
+ var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1296
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1297
+ var APPLY_SCRIPT = "__preprApply()";
1004
1298
  function variantKeysOf(items) {
1005
1299
  return items.flatMap((item) => {
1006
- const key = preprVariantProps(item)["data-prepr-variant-key"];
1300
+ const key = preprVariantProps(item)["data-prepr-variant-key"] ?? nestedKeyOf(item);
1007
1301
  return key ? [key] : [];
1008
1302
  });
1009
1303
  }
1010
- function renderBuckets(items, render, options) {
1011
- const {
1012
- bucket,
1013
- decisions,
1014
- abTest,
1015
- index,
1016
- track,
1017
- placeholder = false
1018
- } = options;
1019
- const renderVariant = (variant, key) => {
1020
- const resolved = resolveVariants(items, { decisions, bucket: variant });
1021
- const content = render(resolved, { index, placeholder });
1022
- const variantKeys = track ? variantKeysOf(resolved) : [];
1023
- const ab = abTest && !bucket ? variant : void 0;
1024
- if (variantKeys.length > 0) {
1025
- return createElement(
1026
- PreprVariantTracking,
1027
- { key, variantKeys, ab },
1028
- content
1029
- );
1304
+ function nestedKeyOf(value) {
1305
+ if (!value || typeof value !== "object") return void 0;
1306
+ for (const child of Object.values(value)) {
1307
+ const items = Array.isArray(child) ? child : [child];
1308
+ for (const item of items) {
1309
+ const key = contextOf(item)?.variant_key ?? (item && nestedKeyOf(item));
1310
+ if (key) return key;
1311
+ }
1312
+ }
1313
+ return void 0;
1314
+ }
1315
+ function renderItems(items, render, options) {
1316
+ const resolved = resolveVariants(items, {
1317
+ decisions: options.decisions,
1318
+ bucket: options.bucket
1319
+ });
1320
+ const content = render(resolved, {
1321
+ index: options.index,
1322
+ defaultVariant: options.defaultVariant
1323
+ });
1324
+ const variantKeys = options.track ? variantKeysOf(resolved) : [];
1325
+ return variantKeys.length > 0 ? createElement(PreprVariantTracking, { variantKeys }, content) : content;
1326
+ }
1327
+ function decidedGroups(items, bucket) {
1328
+ const groups = /* @__PURE__ */ new Map();
1329
+ const walk = (value) => {
1330
+ if (Array.isArray(value)) return value.forEach(walk);
1331
+ if (!value || typeof value !== "object") return;
1332
+ const ctx = contextOf(value);
1333
+ const decided = ctx?.kind === "PERSONALIZATION" || ctx?.kind === "AB_TEST" && !bucket;
1334
+ if (decided && ctx.variant_id) {
1335
+ const group = groups.get(ctx.group_id) ?? {
1336
+ variants: /* @__PURE__ */ new Set(),
1337
+ fallback: ctx.kind === "AB_TEST" ? "A" : PREPR_DEFAULT_VARIANT
1338
+ };
1339
+ group.variants.add(ctx.variant_id);
1340
+ groups.set(ctx.group_id, group);
1030
1341
  }
1031
- if (ab) return createElement("div", { key, [AB_ATTRIBUTE]: ab }, content);
1032
- return content;
1342
+ Object.values(value).forEach(walk);
1033
1343
  };
1034
- if (!abTest || bucket) return renderVariant(bucket);
1035
- return AB_BUCKETS.map((variant) => renderVariant(variant, variant));
1036
- }
1037
- function renderAllVariants(block, render, options) {
1038
- const group = block.key.slice("group:".length);
1039
- const variants = [
1040
- ...new Set(
1041
- block.items.flatMap((item) => {
1042
- const ctx = item[PREPR_CONTEXT_ALIAS];
1043
- return ctx?.variant_id ? [ctx.variant_id] : [];
1044
- })
1045
- )
1046
- ];
1047
- return variants.map(
1048
- (variant) => createElement(
1344
+ walk(items);
1345
+ return groups;
1346
+ }
1347
+ function renderVariants(block, groups, render, options) {
1348
+ const top = block.key.startsWith("group:") ? block.key.slice("group:".length) : void 0;
1349
+ const nested = !(top !== void 0 && groups.size === 1);
1350
+ const choices = [...groups].map(([group, { variants, fallback }]) => {
1351
+ const ids = nested ? [fallback, ...variants] : [...variants];
1352
+ return [...new Set(ids)].map((variant) => [group, variant]);
1353
+ });
1354
+ const combinations = choices.reduce(
1355
+ (all, pairs) => all.flatMap((combo) => pairs.map((p) => [...combo, p])),
1356
+ [[]]
1357
+ );
1358
+ return combinations.map((combination) => {
1359
+ const pairs = combination.map(([group, variant]) => `${group}:${variant}`);
1360
+ const isDefault = combination.every(
1361
+ ([group, variant]) => variant === groups.get(group).fallback
1362
+ );
1363
+ return createElement(
1049
1364
  "div",
1050
1365
  {
1051
- key: variant,
1052
- [GROUP_ATTRIBUTE]: group,
1053
- [VARIANT_ATTRIBUTE]: variant,
1054
- ...variant === PREPR_DEFAULT_VARIANT ? { [DEFAULT_ATTRIBUTE]: "" } : {}
1366
+ key: pairs.join(" "),
1367
+ [VARIANT_ATTRIBUTE]: pairs.join(" "),
1368
+ ...isDefault ? { [DEFAULT_ATTRIBUTE]: "" } : {},
1369
+ ...nested ? { [NESTED_ATTRIBUTE]: "" } : {},
1370
+ // The decision script marks the shown wrapper before hydration.
1371
+ suppressHydrationWarning: true
1055
1372
  },
1056
- renderBuckets(block.items, render, {
1373
+ renderItems(block.items, render, {
1057
1374
  bucket: options.bucket,
1058
- decisions: /* @__PURE__ */ new Map([[group, [variant]]]),
1059
- abTest: false,
1375
+ decisions: new Map(combination.map(([g, v]) => [g, [v]])),
1060
1376
  index: block.index,
1061
- track: options.track
1377
+ track: options.track,
1378
+ defaultVariant: isDefault
1062
1379
  })
1063
- )
1064
- );
1065
- }
1066
- async function PersonalizedBlock({
1067
- items,
1068
- decisions,
1069
- bucket,
1070
- abTest,
1071
- index,
1072
- render,
1073
- track
1074
- }) {
1075
- const decided = await decisions.catch((error) => {
1076
- if (isFrameworkSignal(error)) throw error;
1077
- return void 0;
1078
- });
1079
- return renderBuckets(items, render, {
1080
- bucket,
1081
- decisions: decided,
1082
- abTest,
1083
- index,
1084
- track
1380
+ );
1085
1381
  });
1086
1382
  }
1087
1383
  function PreprStack({
1088
1384
  items,
1089
- decisions,
1090
1385
  render,
1091
1386
  bucket,
1092
1387
  nonce,
1093
- trackVariants = false,
1094
- aboveTheFold = 0
1388
+ trackVariants = false
1095
1389
  }) {
1096
- decisions.catch(() => void 0);
1097
- const blocks = splitStack(items);
1098
- const decidedInProxy = (block) => block.index < aboveTheFold && block.personalized && !block.abTest && block.key.startsWith("group:");
1099
- const hasDecided = blocks.some(decidedInProxy);
1100
- const needsSwitch = !bucket && blocks.some((block) => block.abTest) || hasDecided;
1390
+ const blocks = splitStack(items).map((block) => ({
1391
+ ...block,
1392
+ groups: decidedGroups(block.items, bucket)
1393
+ }));
1394
+ const varies = blocks.some((block) => block.groups.size > 0);
1101
1395
  return createElement(
1102
1396
  Fragment,
1103
1397
  null,
1104
- needsSwitch && createElement(
1398
+ varies && createElement(
1105
1399
  Fragment,
1106
- { key: "prepr-ab" },
1400
+ { key: "prepr-switch" },
1107
1401
  createElement("style", {
1108
1402
  nonce,
1109
- dangerouslySetInnerHTML: {
1110
- __html: AB_BASE_CSS + (hasDecided ? GROUP_BASE_CSS : "")
1111
- }
1403
+ dangerouslySetInnerHTML: { __html: GROUP_BASE_CSS }
1404
+ }),
1405
+ createElement("noscript", {
1406
+ dangerouslySetInnerHTML: { __html: NOSCRIPT_HTML }
1112
1407
  }),
1113
1408
  createElement("script", {
1114
1409
  nonce,
1115
- dangerouslySetInnerHTML: { __html: AB_SWITCH_SCRIPT }
1410
+ dangerouslySetInnerHTML: { __html: DECISION_SCRIPT }
1116
1411
  }),
1117
- createElement(PreprAbSwitch)
1412
+ createElement(PreprDecisionApplier),
1413
+ // Marked decided by the decision switch; never laid out.
1414
+ createElement("template", {
1415
+ "data-prepr-decision": "",
1416
+ suppressHydrationWarning: true
1417
+ })
1118
1418
  ),
1119
1419
  ...blocks.map((block) => {
1120
- if (decidedInProxy(block)) {
1420
+ if (block.groups.size === 0) {
1121
1421
  return createElement(
1122
1422
  Fragment,
1123
1423
  { key: block.key },
1124
- renderAllVariants(block, render, { bucket, track: trackVariants })
1424
+ renderItems(block.items, render, {
1425
+ bucket,
1426
+ index: block.index,
1427
+ track: trackVariants,
1428
+ defaultVariant: true
1429
+ })
1125
1430
  );
1126
1431
  }
1127
- const fallback = renderBuckets(block.items, render, {
1128
- bucket,
1129
- abTest: block.abTest,
1130
- index: block.index,
1131
- track: trackVariants && !block.personalized,
1132
- placeholder: block.personalized
1133
- });
1134
- if (!block.personalized) {
1135
- return createElement(Fragment, { key: block.key }, fallback);
1136
- }
1137
1432
  return createElement(
1138
- Suspense,
1139
- { key: block.key, fallback },
1140
- createElement(PersonalizedBlock, {
1141
- items: block.items,
1142
- decisions,
1433
+ Fragment,
1434
+ { key: block.key },
1435
+ renderVariants(block, block.groups, render, {
1143
1436
  bucket,
1144
- abTest: block.abTest,
1145
- index: block.index,
1146
- render,
1147
1437
  track: trackVariants
1438
+ }),
1439
+ createElement("script", {
1440
+ nonce,
1441
+ dangerouslySetInnerHTML: { __html: APPLY_SCRIPT }
1148
1442
  })
1149
1443
  );
1150
1444
  })
@@ -1156,33 +1450,59 @@ function createPreprClient2(options) {
1156
1450
  const { cacheTags = ["prepr"], revalidate, ...coreOptions } = options;
1157
1451
  const client = createPreprClient({
1158
1452
  // The schema introspection goes through the Data Cache: shared across
1159
- // server instances, refreshed hourly and by revalidateTag on publish.
1453
+ // server instances, refreshed hourly and by a `schema.changed` webhook.
1160
1454
  schemaInit: {
1161
- next: { tags: cacheTags, revalidate: 3600 }
1455
+ next: { tags: [...cacheTags, PREPR_SCHEMA_TAG], revalidate: 3600 }
1162
1456
  },
1163
1457
  ...coreOptions
1164
1458
  });
1165
- const seenGroups = /* @__PURE__ */ new Map();
1166
- return {
1459
+ const nextClient = {
1167
1460
  async query(document2, variables, queryOptions = {}) {
1168
- const { personalize = false, ...rest } = queryOptions;
1169
- const init = rest.init;
1170
- const tagged = {
1461
+ const { personalize = false, tagItems = false, ...rest } = queryOptions;
1462
+ const { next, ...init } = rest.init ?? {};
1463
+ if (tagItems && typeof nextCache.cacheTag !== "function") {
1464
+ throw new Error("tagItems requires Next.js 16 or later.");
1465
+ }
1466
+ const options2 = {
1171
1467
  ...rest,
1172
- init: {
1468
+ // With tagItems, `next` is dropped too: tags or revalidate alongside
1469
+ // `no-store` are a conflict Next.js resolves by ignoring the store.
1470
+ // Otherwise tags are attached unconditionally; the core client drops
1471
+ // them again whenever the request ends up `no-store` (personalized,
1472
+ // per-request headers, or preview).
1473
+ init: tagItems ? { ...init, cache: "no-store" } : {
1173
1474
  ...init,
1174
1475
  next: {
1175
- tags: cacheTags,
1176
1476
  ...revalidate === void 0 ? {} : { revalidate },
1177
- ...init?.next
1477
+ ...next,
1478
+ // Merged, so a per-query tag never drops the client's own,
1479
+ // which the publish webhook relies on.
1480
+ tags: [...cacheTags, ...next?.tags ?? []]
1178
1481
  }
1179
1482
  }
1180
1483
  };
1181
- if (!personalize) return client.query(document2, variables, tagged);
1182
- return client.query(document2, variables, {
1183
- ...tagged,
1184
- headers: mergePreprHeaders(await getPreprHeaders(), rest.headers)
1185
- });
1484
+ if (personalize) {
1485
+ options2.headers = mergePreprHeaders(
1486
+ await getPreprHeaders(),
1487
+ rest.headers
1488
+ );
1489
+ }
1490
+ const result = await client.query(document2, variables, options2);
1491
+ if (tagItems) {
1492
+ let modelTags;
1493
+ try {
1494
+ modelTags = preprModelTagsFor(
1495
+ documentOf(document2),
1496
+ result.data,
1497
+ await clientSchemas.get(client)()
1498
+ );
1499
+ } catch {
1500
+ modelTags = [PREPR_ANY_ITEM_TAG];
1501
+ }
1502
+ nextCache.cacheTag(...cacheTags, ...next?.tags ?? [], ...modelTags);
1503
+ nextCache.cacheTag(...preprCacheTags(result.data));
1504
+ }
1505
+ return result;
1186
1506
  },
1187
1507
  async decide(document2, variables, queryOptions = {}) {
1188
1508
  return client.decide(document2, variables, {
@@ -1193,26 +1513,26 @@ function createPreprClient2(options) {
1193
1513
  )
1194
1514
  });
1195
1515
  },
1196
- hasAbTests: (document2, variables) => client.hasAbTests(document2, variables),
1197
- resolveDecisions: (request, response, document2, variables, options2) => resolveDecisionsWith(
1198
- client,
1199
- seenGroups,
1200
- request,
1201
- response,
1202
- document2,
1203
- variables,
1204
- options2
1205
- )
1516
+ hasAbTests: (document2, variables) => client.hasAbTests(document2, variables)
1206
1517
  };
1518
+ preprClientInternals.set(nextClient, { client });
1519
+ return nextClient;
1207
1520
  }
1208
1521
  export {
1209
- PREPR_DECISIONS_COOKIE,
1522
+ PREPR_ANY_ITEM_TAG,
1210
1523
  PREPR_DEFAULT_VARIANT,
1211
1524
  PreprError,
1212
1525
  PreprStack,
1213
1526
  createPreprClient2 as createPreprClient,
1527
+ createPreprProxy,
1528
+ decisionsFrom,
1214
1529
  isPersonalizedResponse,
1530
+ loadPreprSchema,
1531
+ preprCacheTags,
1532
+ preprModelTag,
1215
1533
  preprVariantProps,
1216
1534
  resolveVariants,
1217
- splitStack
1535
+ splitStack,
1536
+ toDecisionQuery,
1537
+ toVariantsQuery
1218
1538
  };