@preprio/toolkit 0.1.0-beta.5 → 0.2.0-beta.2

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 (36) hide show
  1. package/README.md +99 -14
  2. package/dist/astro/index.cjs +37 -15
  3. package/dist/astro/index.d.cts +38 -8
  4. package/dist/astro/index.d.ts +38 -8
  5. package/dist/astro/index.js +37 -15
  6. package/dist/{chunk-NLQIRN5O.js → chunk-6OOGNVJ4.js} +83 -41
  7. package/dist/index.cjs +102 -55
  8. package/dist/index.d.cts +59 -16
  9. package/dist/index.d.ts +59 -16
  10. package/dist/index.js +22 -16
  11. package/dist/nextjs/components.cjs +87 -42
  12. package/dist/nextjs/components.d.cts +3 -3
  13. package/dist/nextjs/components.d.ts +3 -3
  14. package/dist/nextjs/components.js +8 -4
  15. package/dist/nextjs/image-loader.cjs +17 -94
  16. package/dist/nextjs/image-loader.d.cts +8 -4
  17. package/dist/nextjs/image-loader.d.ts +8 -4
  18. package/dist/nextjs/image-loader.js +17 -94
  19. package/dist/nextjs/index.cjs +37 -15
  20. package/dist/nextjs/index.d.cts +46 -16
  21. package/dist/nextjs/index.d.ts +46 -16
  22. package/dist/nextjs/index.js +37 -15
  23. package/dist/nuxt/index.cjs +37 -15
  24. package/dist/nuxt/index.d.cts +62 -32
  25. package/dist/nuxt/index.d.ts +62 -32
  26. package/dist/nuxt/index.js +37 -15
  27. package/dist/{pixel-DVSJQC0K.d.cts → pixel-763p7m6A.d.cts} +64 -1
  28. package/dist/{pixel-DVSJQC0K.d.ts → pixel-763p7m6A.d.ts} +64 -1
  29. package/dist/sveltekit/index.cjs +37 -15
  30. package/dist/sveltekit/index.d.cts +62 -32
  31. package/dist/sveltekit/index.d.ts +62 -32
  32. package/dist/sveltekit/index.js +37 -15
  33. package/package.json +1 -1
  34. package/src/astro/PreprToolbar.astro +16 -10
  35. package/src/nuxt/PreprToolbar.vue +10 -8
  36. package/src/sveltekit/PreprToolbar.svelte +11 -4
@@ -23,100 +23,23 @@ __export(image_loader_exports, {
23
23
  default: () => preprImageLoader
24
24
  });
25
25
  module.exports = __toCommonJS(image_loader_exports);
26
- var TRANSFORMABLE_HOST_SUFFIXES = [".stream.prepr.io", ".b-cdn.net"];
27
- var KNOWN_OPTION_KEYS = /* @__PURE__ */ new Set([
28
- "w",
29
- // width
30
- "h",
31
- // height
32
- "q",
33
- // quality
34
- "c",
35
- // crop position (north, centre, ...)
36
- "ex",
37
- // crop extract: x offset
38
- "ey",
39
- // crop extract: y offset
40
- "ew",
41
- // crop extract: width
42
- "eh"
43
- // crop extract: height
44
- ]);
45
- function isTransformSegment(segment) {
46
- if (!segment) return false;
47
- const parts = segment.split(",");
48
- let sawKnownOption = false;
49
- for (const part of parts) {
50
- const underscore = part.indexOf("_");
51
- if (underscore <= 0) return false;
52
- const key = part.slice(0, underscore);
53
- const value = part.slice(underscore + 1);
54
- if (!value) return false;
55
- if (KNOWN_OPTION_KEYS.has(key)) {
56
- sawKnownOption = true;
26
+ var TRANSFORM_SEGMENT = /\/([^/]*\bw_\d+[^/]*)\//;
27
+ var isPositiveInteger = (value) => /^\d+$/.test(value);
28
+ function preprImageLoader({ src, width }) {
29
+ return src.replace(TRANSFORM_SEGMENT, (match, segment) => {
30
+ const options = segment.split(",").map((pair) => {
31
+ const underscore = pair.indexOf("_");
32
+ return [pair.slice(0, underscore), pair.slice(underscore + 1)];
33
+ });
34
+ const emittedWidth = options.find(([key]) => key === "w");
35
+ if (!emittedWidth || !isPositiveInteger(emittedWidth[1])) return match;
36
+ const emittedHeight = options.find(([key]) => key === "h");
37
+ if (emittedHeight && isPositiveInteger(emittedHeight[1])) {
38
+ emittedHeight[1] = String(
39
+ Math.round(width * Number(emittedHeight[1]) / Number(emittedWidth[1]))
40
+ );
57
41
  }
58
- }
59
- return sawKnownOption;
60
- }
61
- function parseTransform(segment) {
62
- return segment.split(",").map((part) => {
63
- const underscore = part.indexOf("_");
64
- return [part.slice(0, underscore), part.slice(underscore + 1)];
42
+ emittedWidth[1] = String(width);
43
+ return `/${options.map(([key, value]) => `${key}_${value}`).join(",")}/`;
65
44
  });
66
45
  }
67
- function parsePreprUrl(rawUrl) {
68
- let url;
69
- try {
70
- url = new URL(rawUrl);
71
- } catch {
72
- return null;
73
- }
74
- const isTransformableHost = TRANSFORMABLE_HOST_SUFFIXES.some(
75
- (suffix) => url.hostname.endsWith(suffix)
76
- );
77
- if (!isTransformableHost) return null;
78
- const rawSegments = url.pathname.split("/");
79
- rawSegments.shift();
80
- if (rawSegments.length === 0) return null;
81
- const filename = rawSegments[rawSegments.length - 1];
82
- const leading = rawSegments.slice(0, -1);
83
- let transform = [];
84
- const prefixSegments = [];
85
- for (const segment of leading) {
86
- if (transform.length === 0 && isTransformSegment(segment)) {
87
- transform = parseTransform(segment);
88
- } else if (segment === "") {
89
- continue;
90
- } else {
91
- prefixSegments.push(segment);
92
- }
93
- }
94
- const prefix = url.origin + "/" + (prefixSegments.length > 0 ? prefixSegments.join("/") + "/" : "");
95
- return { prefix, transform, filename: filename + url.search };
96
- }
97
- function buildTransformSegment(pairs) {
98
- return pairs.map(([key, value]) => `${key}_${value}`).join(",");
99
- }
100
- function preprImageLoader({ src, width, quality }) {
101
- const parsed = parsePreprUrl(src);
102
- if (!parsed) return src;
103
- const { prefix, transform, filename } = parsed;
104
- const hasCropExtract = transform.some(([key]) => ["ex", "ey", "ew", "eh"].includes(key));
105
- const next = [];
106
- let setWidth = false;
107
- for (const [key, value] of transform) {
108
- if (key === "w") {
109
- next.push(["w", String(width)]);
110
- setWidth = true;
111
- } else if (key === "h" && !hasCropExtract) {
112
- continue;
113
- } else if (key === "q") {
114
- continue;
115
- } else {
116
- next.push([key, value]);
117
- }
118
- }
119
- if (!setWidth) next.push(["w", String(width)]);
120
- if (quality) next.push(["q", String(quality)]);
121
- return `${prefix}${buildTransformSegment(next)}/${filename}`;
122
- }
@@ -1,10 +1,14 @@
1
1
  import { ImageLoaderProps } from 'next/image';
2
2
 
3
3
  /**
4
- * Rewrites Prepr stream URLs to the width `next/image` asks for, preserving any
5
- * crop/preset options already on the URL. Non-Prepr URLs (other hosts, relative
6
- * paths) come back unchanged — set `unoptimized` on those `<Image>`s.
4
+ * Rewrites Prepr stream URLs to the width `next/image` asks for. URLs with no
5
+ * transform segment (other hosts, relative paths) come back unchanged — set
6
+ * `unoptimized` on those `<Image>`s.
7
+ *
8
+ * No upper clamp: the emitted width is the asset's own size or its crop box,
9
+ * neither of which is a ceiling on what the CDN can serve, so capping there
10
+ * would rule out retina.
7
11
  */
8
- declare function preprImageLoader({ src, width, quality }: ImageLoaderProps): string;
12
+ declare function preprImageLoader({ src, width }: ImageLoaderProps): string;
9
13
 
10
14
  export { preprImageLoader as default };
@@ -1,10 +1,14 @@
1
1
  import { ImageLoaderProps } from 'next/image';
2
2
 
3
3
  /**
4
- * Rewrites Prepr stream URLs to the width `next/image` asks for, preserving any
5
- * crop/preset options already on the URL. Non-Prepr URLs (other hosts, relative
6
- * paths) come back unchanged — set `unoptimized` on those `<Image>`s.
4
+ * Rewrites Prepr stream URLs to the width `next/image` asks for. URLs with no
5
+ * transform segment (other hosts, relative paths) come back unchanged — set
6
+ * `unoptimized` on those `<Image>`s.
7
+ *
8
+ * No upper clamp: the emitted width is the asset's own size or its crop box,
9
+ * neither of which is a ceiling on what the CDN can serve, so capping there
10
+ * would rule out retina.
7
11
  */
8
- declare function preprImageLoader({ src, width, quality }: ImageLoaderProps): string;
12
+ declare function preprImageLoader({ src, width }: ImageLoaderProps): string;
9
13
 
10
14
  export { preprImageLoader as default };
@@ -1,101 +1,24 @@
1
1
  // src/nextjs/image-loader.ts
2
- var TRANSFORMABLE_HOST_SUFFIXES = [".stream.prepr.io", ".b-cdn.net"];
3
- var KNOWN_OPTION_KEYS = /* @__PURE__ */ new Set([
4
- "w",
5
- // width
6
- "h",
7
- // height
8
- "q",
9
- // quality
10
- "c",
11
- // crop position (north, centre, ...)
12
- "ex",
13
- // crop extract: x offset
14
- "ey",
15
- // crop extract: y offset
16
- "ew",
17
- // crop extract: width
18
- "eh"
19
- // crop extract: height
20
- ]);
21
- function isTransformSegment(segment) {
22
- if (!segment) return false;
23
- const parts = segment.split(",");
24
- let sawKnownOption = false;
25
- for (const part of parts) {
26
- const underscore = part.indexOf("_");
27
- if (underscore <= 0) return false;
28
- const key = part.slice(0, underscore);
29
- const value = part.slice(underscore + 1);
30
- if (!value) return false;
31
- if (KNOWN_OPTION_KEYS.has(key)) {
32
- sawKnownOption = true;
2
+ var TRANSFORM_SEGMENT = /\/([^/]*\bw_\d+[^/]*)\//;
3
+ var isPositiveInteger = (value) => /^\d+$/.test(value);
4
+ function preprImageLoader({ src, width }) {
5
+ return src.replace(TRANSFORM_SEGMENT, (match, segment) => {
6
+ const options = segment.split(",").map((pair) => {
7
+ const underscore = pair.indexOf("_");
8
+ return [pair.slice(0, underscore), pair.slice(underscore + 1)];
9
+ });
10
+ const emittedWidth = options.find(([key]) => key === "w");
11
+ if (!emittedWidth || !isPositiveInteger(emittedWidth[1])) return match;
12
+ const emittedHeight = options.find(([key]) => key === "h");
13
+ if (emittedHeight && isPositiveInteger(emittedHeight[1])) {
14
+ emittedHeight[1] = String(
15
+ Math.round(width * Number(emittedHeight[1]) / Number(emittedWidth[1]))
16
+ );
33
17
  }
34
- }
35
- return sawKnownOption;
36
- }
37
- function parseTransform(segment) {
38
- return segment.split(",").map((part) => {
39
- const underscore = part.indexOf("_");
40
- return [part.slice(0, underscore), part.slice(underscore + 1)];
18
+ emittedWidth[1] = String(width);
19
+ return `/${options.map(([key, value]) => `${key}_${value}`).join(",")}/`;
41
20
  });
42
21
  }
43
- function parsePreprUrl(rawUrl) {
44
- let url;
45
- try {
46
- url = new URL(rawUrl);
47
- } catch {
48
- return null;
49
- }
50
- const isTransformableHost = TRANSFORMABLE_HOST_SUFFIXES.some(
51
- (suffix) => url.hostname.endsWith(suffix)
52
- );
53
- if (!isTransformableHost) return null;
54
- const rawSegments = url.pathname.split("/");
55
- rawSegments.shift();
56
- if (rawSegments.length === 0) return null;
57
- const filename = rawSegments[rawSegments.length - 1];
58
- const leading = rawSegments.slice(0, -1);
59
- let transform = [];
60
- const prefixSegments = [];
61
- for (const segment of leading) {
62
- if (transform.length === 0 && isTransformSegment(segment)) {
63
- transform = parseTransform(segment);
64
- } else if (segment === "") {
65
- continue;
66
- } else {
67
- prefixSegments.push(segment);
68
- }
69
- }
70
- const prefix = url.origin + "/" + (prefixSegments.length > 0 ? prefixSegments.join("/") + "/" : "");
71
- return { prefix, transform, filename: filename + url.search };
72
- }
73
- function buildTransformSegment(pairs) {
74
- return pairs.map(([key, value]) => `${key}_${value}`).join(",");
75
- }
76
- function preprImageLoader({ src, width, quality }) {
77
- const parsed = parsePreprUrl(src);
78
- if (!parsed) return src;
79
- const { prefix, transform, filename } = parsed;
80
- const hasCropExtract = transform.some(([key]) => ["ex", "ey", "ew", "eh"].includes(key));
81
- const next = [];
82
- let setWidth = false;
83
- for (const [key, value] of transform) {
84
- if (key === "w") {
85
- next.push(["w", String(width)]);
86
- setWidth = true;
87
- } else if (key === "h" && !hasCropExtract) {
88
- continue;
89
- } else if (key === "q") {
90
- continue;
91
- } else {
92
- next.push([key, value]);
93
- }
94
- }
95
- if (!setWidth) next.push(["w", String(width)]);
96
- if (quality) next.push(["q", String(quality)]);
97
- return `${prefix}${buildTransformSegment(next)}/${filename}`;
98
- }
99
22
  export {
100
23
  preprImageLoader as default
101
24
  };
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(nextjs_exports);
38
38
  var import_server = require("next/server");
39
39
 
40
40
  // src/version.ts
41
- var VERSION = "0.1.0-beta.5";
41
+ var VERSION = "0.2.0-beta.2";
42
42
 
43
43
  // src/core/constants.ts
44
44
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -51,6 +51,24 @@ var PARAM_VARIANT = "prepr_preview_ab";
51
51
  var PARAM_HIDE_BAR = "prepr_hide_bar";
52
52
  var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
53
53
 
54
+ // src/core/features.ts
55
+ function isEnabled(config) {
56
+ if (typeof config === "boolean") return config;
57
+ return config?.enabled ?? true;
58
+ }
59
+ function resolveFeatures(features) {
60
+ return {
61
+ segments: isEnabled(features?.segments),
62
+ abTesting: isEnabled(features?.abTesting),
63
+ editMode: isEnabled(features?.editMode)
64
+ };
65
+ }
66
+ var ALL_FEATURES_ENABLED = Object.freeze({
67
+ segments: true,
68
+ abTesting: true,
69
+ editMode: true
70
+ });
71
+
54
72
  // src/core/middleware.ts
55
73
  var UTM_PARAMS = [
56
74
  "utm_source",
@@ -120,7 +138,8 @@ function processPreprRequest(request, opts) {
120
138
  if (!opts?.preview) {
121
139
  return { requestHeaders, responseCookies };
122
140
  }
123
- const hasPreviewParams = searchParams.has(PARAM_SEGMENT) || searchParams.has(PARAM_VARIANT);
141
+ const features = resolveFeatures(opts?.features);
142
+ const hasPreviewParams = features.segments && searchParams.has(PARAM_SEGMENT) || features.abTesting && searchParams.has(PARAM_VARIANT);
124
143
  if (!hasPreviewParams) {
125
144
  const previewModeCookie = requestCookies.get(COOKIE_PREVIEW_MODE);
126
145
  const previewModeEnabled = previewModeCookie !== "false";
@@ -140,16 +159,16 @@ function processPreprRequest(request, opts) {
140
159
  });
141
160
  }
142
161
  if (!isLivePreview) {
143
- const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
162
+ const segmentCookie = features.segments ? requestCookies.get(COOKIE_SEGMENT) : void 0;
144
163
  if (segmentCookie) {
145
164
  requestHeaders.set("Prepr-Segments", segmentCookie);
146
165
  }
147
- const abCookie = requestCookies.get(COOKIE_VARIANT);
166
+ const abCookie = features.abTesting ? requestCookies.get(COOKIE_VARIANT) : void 0;
148
167
  if (abCookie) {
149
168
  requestHeaders.set("Prepr-ABtesting", abCookie);
150
169
  }
151
170
  }
152
- const previewAb = searchParams.get(PARAM_VARIANT);
171
+ const previewAb = features.abTesting ? searchParams.get(PARAM_VARIANT) : null;
153
172
  if (previewAb !== null) {
154
173
  requestHeaders.set("Prepr-ABtesting", previewAb);
155
174
  if (!isLivePreview) {
@@ -162,7 +181,7 @@ function processPreprRequest(request, opts) {
162
181
  });
163
182
  }
164
183
  }
165
- const previewSegment = searchParams.get(PARAM_SEGMENT);
184
+ const previewSegment = features.segments ? searchParams.get(PARAM_SEGMENT) : null;
166
185
  if (previewSegment !== null) {
167
186
  requestHeaders.set("Prepr-Segments", previewSegment);
168
187
  if (!isLivePreview) {
@@ -346,16 +365,19 @@ async function getPreprEnvironmentSegments(token) {
346
365
  );
347
366
  }
348
367
  }
349
- async function getToolbarPropsFromHeaders(headers2, token) {
368
+ async function getToolbarPropsFromHeaders(headers2, token, features) {
369
+ const resolved = resolveFeatures(features);
350
370
  let segments = [];
351
- try {
352
- segments = await getPreprEnvironmentSegments(token);
353
- } catch {
354
- segments = [];
371
+ if (resolved.segments) {
372
+ try {
373
+ segments = await getPreprEnvironmentSegments(token);
374
+ } catch {
375
+ segments = [];
376
+ }
355
377
  }
356
378
  return {
357
- activeSegment: getActiveSegmentFromHeaders(headers2),
358
- activeVariant: getActiveVariantFromHeaders(headers2),
379
+ activeSegment: resolved.segments ? getActiveSegmentFromHeaders(headers2) : null,
380
+ activeVariant: resolved.abTesting ? getActiveVariantFromHeaders(headers2) : null,
359
381
  segments,
360
382
  data: segments
361
383
  };
@@ -381,9 +403,9 @@ async function getPreprHeaders() {
381
403
  const h = await readHeaders();
382
404
  return getPreprHeadersFromHeaders(h);
383
405
  }
384
- async function getToolbarProps(token) {
406
+ async function getToolbarProps(token, features) {
385
407
  const h = await readHeaders();
386
- return getToolbarPropsFromHeaders(h, token);
408
+ return getToolbarPropsFromHeaders(h, token, features);
387
409
  }
388
410
 
389
411
  // src/nextjs/index.ts
@@ -1,25 +1,34 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  export { PreprToolbar, PreprTrackingPixel, PreprTrackingPixelProps } from './components';
3
3
 
4
- interface PreprMiddlewareOptions {
5
- /** Enable preview-mode headers (segment/AB overrides, preview bar). */
6
- preview?: boolean;
7
- /** Override the version reported in Prepr-Package (mainly for tests). */
8
- version?: string;
9
- }
10
-
11
- /**
12
- * Next.js middleware entry point. Despite the `create` prefix this runs
13
- * per-request — it does not return a handler. Pass an existing `NextResponse`
14
- * as the second argument to chain with other middleware.
15
- */
16
- declare function createPreprMiddleware(request: NextRequest, options?: PreprMiddlewareOptions): NextResponse;
17
- declare function createPreprMiddleware(request: NextRequest, response: NextResponse, options?: PreprMiddlewareOptions): NextResponse;
18
-
19
4
  interface PreprSegment {
20
5
  readonly _id: string;
21
6
  readonly name: string;
22
7
  }
8
+ /**
9
+ * Per-feature config. `false` is shorthand for `{ enabled: false }`; the object
10
+ * form leaves room for per-feature options later without a breaking change.
11
+ */
12
+ type PreprFeatureConfig = boolean | {
13
+ enabled?: boolean;
14
+ };
15
+ /** Toolkit features a consumer can turn off. Omitted keys stay enabled. */
16
+ interface PreprFeatures {
17
+ /** Segment picker, `Prepr-Segments` header and its cookie/query param. */
18
+ segments?: PreprFeatureConfig;
19
+ /** A/B variant switch, `Prepr-ABtesting` header and its cookie/query param. */
20
+ abTesting?: PreprFeatureConfig;
21
+ /**
22
+ * The site's own click-to-edit: the toolbar's Edit mode control, the stega
23
+ * tooltip/overlay and the close-edit pill.
24
+ *
25
+ * NOT a security control, and NOT the Prepr editor's live preview. When the
26
+ * site is framed by the editor, `prepr:initVE` still activates edit mode —
27
+ * that is the CMS operating inside its own iframe, not an affordance offered
28
+ * to visitors. See iframe-bridge.ts.
29
+ */
30
+ editMode?: PreprFeatureConfig;
31
+ }
23
32
  interface PreprToolbarProps {
24
33
  readonly activeSegment: string | null;
25
34
  readonly activeVariant: string | null;
@@ -47,6 +56,27 @@ interface PreprHeaders {
47
56
  }
48
57
  type PreprErrorCode = 'INVALID_TOKEN' | 'MISSING_TOKEN' | 'HTTP_ERROR' | 'FETCH_ERROR' | 'INVALID_RESPONSE' | 'CONTEXT_ERROR';
49
58
 
59
+ interface PreprMiddlewareOptions {
60
+ /** Enable preview-mode headers (segment/AB overrides, preview bar). */
61
+ preview?: boolean;
62
+ /**
63
+ * Disable features app-wide. Pass the same object you give the toolbar: a
64
+ * disabled feature injects no header and persists no cookie, whatever the
65
+ * request carries.
66
+ */
67
+ features?: PreprFeatures;
68
+ /** Override the version reported in Prepr-Package (mainly for tests). */
69
+ version?: string;
70
+ }
71
+
72
+ /**
73
+ * Next.js middleware entry point. Despite the `create` prefix this runs
74
+ * per-request — it does not return a handler. Pass an existing `NextResponse`
75
+ * as the second argument to chain with other middleware.
76
+ */
77
+ declare function createPreprMiddleware(request: NextRequest, options?: PreprMiddlewareOptions): NextResponse;
78
+ declare function createPreprMiddleware(request: NextRequest, response: NextResponse, options?: PreprMiddlewareOptions): NextResponse;
79
+
50
80
  /** Prepr Customer ID for the current request. Server-only. */
51
81
  declare function getPreprUUID(): Promise<string | null>;
52
82
  /** Active segment for the current request. Server-only. */
@@ -66,7 +96,7 @@ declare function getPreprHeaders(): Promise<PreprHeaders>;
66
96
  * Never throws: segment-fetch failures degrade to an empty segment list, so a
67
97
  * bad or missing token can't crash the host app.
68
98
  */
69
- declare function getToolbarProps(token: string): Promise<PreprToolbarProps>;
99
+ declare function getToolbarProps(token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
70
100
 
71
101
  /** Error thrown by the token helpers and the segments fetch. */
72
102
  declare class PreprError extends Error {
@@ -1,25 +1,34 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  export { PreprToolbar, PreprTrackingPixel, PreprTrackingPixelProps } from './components';
3
3
 
4
- interface PreprMiddlewareOptions {
5
- /** Enable preview-mode headers (segment/AB overrides, preview bar). */
6
- preview?: boolean;
7
- /** Override the version reported in Prepr-Package (mainly for tests). */
8
- version?: string;
9
- }
10
-
11
- /**
12
- * Next.js middleware entry point. Despite the `create` prefix this runs
13
- * per-request — it does not return a handler. Pass an existing `NextResponse`
14
- * as the second argument to chain with other middleware.
15
- */
16
- declare function createPreprMiddleware(request: NextRequest, options?: PreprMiddlewareOptions): NextResponse;
17
- declare function createPreprMiddleware(request: NextRequest, response: NextResponse, options?: PreprMiddlewareOptions): NextResponse;
18
-
19
4
  interface PreprSegment {
20
5
  readonly _id: string;
21
6
  readonly name: string;
22
7
  }
8
+ /**
9
+ * Per-feature config. `false` is shorthand for `{ enabled: false }`; the object
10
+ * form leaves room for per-feature options later without a breaking change.
11
+ */
12
+ type PreprFeatureConfig = boolean | {
13
+ enabled?: boolean;
14
+ };
15
+ /** Toolkit features a consumer can turn off. Omitted keys stay enabled. */
16
+ interface PreprFeatures {
17
+ /** Segment picker, `Prepr-Segments` header and its cookie/query param. */
18
+ segments?: PreprFeatureConfig;
19
+ /** A/B variant switch, `Prepr-ABtesting` header and its cookie/query param. */
20
+ abTesting?: PreprFeatureConfig;
21
+ /**
22
+ * The site's own click-to-edit: the toolbar's Edit mode control, the stega
23
+ * tooltip/overlay and the close-edit pill.
24
+ *
25
+ * NOT a security control, and NOT the Prepr editor's live preview. When the
26
+ * site is framed by the editor, `prepr:initVE` still activates edit mode —
27
+ * that is the CMS operating inside its own iframe, not an affordance offered
28
+ * to visitors. See iframe-bridge.ts.
29
+ */
30
+ editMode?: PreprFeatureConfig;
31
+ }
23
32
  interface PreprToolbarProps {
24
33
  readonly activeSegment: string | null;
25
34
  readonly activeVariant: string | null;
@@ -47,6 +56,27 @@ interface PreprHeaders {
47
56
  }
48
57
  type PreprErrorCode = 'INVALID_TOKEN' | 'MISSING_TOKEN' | 'HTTP_ERROR' | 'FETCH_ERROR' | 'INVALID_RESPONSE' | 'CONTEXT_ERROR';
49
58
 
59
+ interface PreprMiddlewareOptions {
60
+ /** Enable preview-mode headers (segment/AB overrides, preview bar). */
61
+ preview?: boolean;
62
+ /**
63
+ * Disable features app-wide. Pass the same object you give the toolbar: a
64
+ * disabled feature injects no header and persists no cookie, whatever the
65
+ * request carries.
66
+ */
67
+ features?: PreprFeatures;
68
+ /** Override the version reported in Prepr-Package (mainly for tests). */
69
+ version?: string;
70
+ }
71
+
72
+ /**
73
+ * Next.js middleware entry point. Despite the `create` prefix this runs
74
+ * per-request — it does not return a handler. Pass an existing `NextResponse`
75
+ * as the second argument to chain with other middleware.
76
+ */
77
+ declare function createPreprMiddleware(request: NextRequest, options?: PreprMiddlewareOptions): NextResponse;
78
+ declare function createPreprMiddleware(request: NextRequest, response: NextResponse, options?: PreprMiddlewareOptions): NextResponse;
79
+
50
80
  /** Prepr Customer ID for the current request. Server-only. */
51
81
  declare function getPreprUUID(): Promise<string | null>;
52
82
  /** Active segment for the current request. Server-only. */
@@ -66,7 +96,7 @@ declare function getPreprHeaders(): Promise<PreprHeaders>;
66
96
  * Never throws: segment-fetch failures degrade to an empty segment list, so a
67
97
  * bad or missing token can't crash the host app.
68
98
  */
69
- declare function getToolbarProps(token: string): Promise<PreprToolbarProps>;
99
+ declare function getToolbarProps(token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
70
100
 
71
101
  /** Error thrown by the token helpers and the segments fetch. */
72
102
  declare class PreprError extends Error {
@@ -2,7 +2,7 @@
2
2
  import { NextResponse } from "next/server";
3
3
 
4
4
  // src/version.ts
5
- var VERSION = "0.1.0-beta.5";
5
+ var VERSION = "0.2.0-beta.2";
6
6
 
7
7
  // src/core/constants.ts
8
8
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -15,6 +15,24 @@ var PARAM_VARIANT = "prepr_preview_ab";
15
15
  var PARAM_HIDE_BAR = "prepr_hide_bar";
16
16
  var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
17
17
 
18
+ // src/core/features.ts
19
+ function isEnabled(config) {
20
+ if (typeof config === "boolean") return config;
21
+ return config?.enabled ?? true;
22
+ }
23
+ function resolveFeatures(features) {
24
+ return {
25
+ segments: isEnabled(features?.segments),
26
+ abTesting: isEnabled(features?.abTesting),
27
+ editMode: isEnabled(features?.editMode)
28
+ };
29
+ }
30
+ var ALL_FEATURES_ENABLED = Object.freeze({
31
+ segments: true,
32
+ abTesting: true,
33
+ editMode: true
34
+ });
35
+
18
36
  // src/core/middleware.ts
19
37
  var UTM_PARAMS = [
20
38
  "utm_source",
@@ -84,7 +102,8 @@ function processPreprRequest(request, opts) {
84
102
  if (!opts?.preview) {
85
103
  return { requestHeaders, responseCookies };
86
104
  }
87
- const hasPreviewParams = searchParams.has(PARAM_SEGMENT) || searchParams.has(PARAM_VARIANT);
105
+ const features = resolveFeatures(opts?.features);
106
+ const hasPreviewParams = features.segments && searchParams.has(PARAM_SEGMENT) || features.abTesting && searchParams.has(PARAM_VARIANT);
88
107
  if (!hasPreviewParams) {
89
108
  const previewModeCookie = requestCookies.get(COOKIE_PREVIEW_MODE);
90
109
  const previewModeEnabled = previewModeCookie !== "false";
@@ -104,16 +123,16 @@ function processPreprRequest(request, opts) {
104
123
  });
105
124
  }
106
125
  if (!isLivePreview) {
107
- const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
126
+ const segmentCookie = features.segments ? requestCookies.get(COOKIE_SEGMENT) : void 0;
108
127
  if (segmentCookie) {
109
128
  requestHeaders.set("Prepr-Segments", segmentCookie);
110
129
  }
111
- const abCookie = requestCookies.get(COOKIE_VARIANT);
130
+ const abCookie = features.abTesting ? requestCookies.get(COOKIE_VARIANT) : void 0;
112
131
  if (abCookie) {
113
132
  requestHeaders.set("Prepr-ABtesting", abCookie);
114
133
  }
115
134
  }
116
- const previewAb = searchParams.get(PARAM_VARIANT);
135
+ const previewAb = features.abTesting ? searchParams.get(PARAM_VARIANT) : null;
117
136
  if (previewAb !== null) {
118
137
  requestHeaders.set("Prepr-ABtesting", previewAb);
119
138
  if (!isLivePreview) {
@@ -126,7 +145,7 @@ function processPreprRequest(request, opts) {
126
145
  });
127
146
  }
128
147
  }
129
- const previewSegment = searchParams.get(PARAM_SEGMENT);
148
+ const previewSegment = features.segments ? searchParams.get(PARAM_SEGMENT) : null;
130
149
  if (previewSegment !== null) {
131
150
  requestHeaders.set("Prepr-Segments", previewSegment);
132
151
  if (!isLivePreview) {
@@ -310,16 +329,19 @@ async function getPreprEnvironmentSegments(token) {
310
329
  );
311
330
  }
312
331
  }
313
- async function getToolbarPropsFromHeaders(headers2, token) {
332
+ async function getToolbarPropsFromHeaders(headers2, token, features) {
333
+ const resolved = resolveFeatures(features);
314
334
  let segments = [];
315
- try {
316
- segments = await getPreprEnvironmentSegments(token);
317
- } catch {
318
- segments = [];
335
+ if (resolved.segments) {
336
+ try {
337
+ segments = await getPreprEnvironmentSegments(token);
338
+ } catch {
339
+ segments = [];
340
+ }
319
341
  }
320
342
  return {
321
- activeSegment: getActiveSegmentFromHeaders(headers2),
322
- activeVariant: getActiveVariantFromHeaders(headers2),
343
+ activeSegment: resolved.segments ? getActiveSegmentFromHeaders(headers2) : null,
344
+ activeVariant: resolved.abTesting ? getActiveVariantFromHeaders(headers2) : null,
323
345
  segments,
324
346
  data: segments
325
347
  };
@@ -345,9 +367,9 @@ async function getPreprHeaders() {
345
367
  const h = await readHeaders();
346
368
  return getPreprHeadersFromHeaders(h);
347
369
  }
348
- async function getToolbarProps(token) {
370
+ async function getToolbarProps(token, features) {
349
371
  const h = await readHeaders();
350
- return getToolbarPropsFromHeaders(h, token);
372
+ return getToolbarPropsFromHeaders(h, token, features);
351
373
  }
352
374
 
353
375
  // src/nextjs/index.ts