@preprio/toolkit 0.1.0-beta.5 → 0.1.0-beta.6

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.
package/README.md CHANGED
@@ -292,7 +292,11 @@ export default function RootLayout({ children }: { children: ReactNode }) {
292
292
 
293
293
  ### API Integration
294
294
 
295
- Use `getPreprHeaders()` to forward Prepr's personalization context to your own CMS or GraphQL fetches. Without these headers Prepr cannot resolve segments or A/B variants for the request.
295
+ Use `getPreprHeaders()` to forward the request's Prepr personalization context to your Prepr GraphQL fetches. Prepr resolves segments and A/B variants from these headers, so a query sent without them always returns the default, unpersonalized content.
296
+
297
+ The headers are set by the middleware, so they are only present on routes its matcher covers — off-matcher requests yield an empty object.
298
+
299
+ The signature differs per framework: `getPreprHeaders()` is async and zero-argument on Next.js (it reads `headers()` for you), while the Astro, SvelteKit, and Nuxt builds are synchronous and take the request's `Headers`.
296
300
 
297
301
  #### With the Fetch API
298
302
 
@@ -364,7 +368,15 @@ export default {
364
368
  };
365
369
  ```
366
370
 
367
- The loader only rewrites URLs on `*.stream.prepr.io` and `*.b-cdn.net`; anything else passes through untouched. Existing crop and quality transforms in the URL survive the resize.
371
+ The API emits each asset's real dimensions, so a Prepr URL renders as-is:
372
+
373
+ ```
374
+ https://393qibegr87z.b-cdn.net/w_800,h_600,fx_75,fy_67/b7ijwafd586-photo.jpg
375
+ ```
376
+
377
+ The loader rewrites `w_` to the width Next asks for and scales `h_` alongside it, holding the emitted ratio — for a cropped asset that ratio is the authored crop box, so the crop survives the resize. Focal points (`fx`/`fy`) are percentages and carry through unchanged, as does any other option in the segment. Quality is not a CDN option and is ignored.
378
+
379
+ URLs with no transform segment pass through untouched — set `unoptimized` on those `<Image>`s.
368
380
 
369
381
  See the runnable example at `examples/nextjs` for the full source of truth.
370
382
 
@@ -30,7 +30,7 @@ __export(astro_exports, {
30
30
  module.exports = __toCommonJS(astro_exports);
31
31
 
32
32
  // src/version.ts
33
- var VERSION = "0.1.0-beta.5";
33
+ var VERSION = "0.1.0-beta.6";
34
34
 
35
35
  // src/core/server.ts
36
36
  var PreprError = class extends Error {
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.1.0-beta.5";
2
+ var VERSION = "0.1.0-beta.6";
3
3
 
4
4
  // src/core/server.ts
5
5
  var PreprError = class extends Error {
package/dist/index.cjs CHANGED
@@ -43,7 +43,7 @@ __export(src_exports, {
43
43
  module.exports = __toCommonJS(src_exports);
44
44
 
45
45
  // src/version.ts
46
- var VERSION = "0.1.0-beta.5";
46
+ var VERSION = "0.1.0-beta.6";
47
47
 
48
48
  // src/core/constants.ts
49
49
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ export { f as PreprEnvironment, g as PreprEventType, h as PreprHeaderName, i as
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.1.0-beta.5";
8
+ declare const VERSION = "0.1.0-beta.6";
9
9
 
10
10
  interface ToolbarState {
11
11
  locale: Locale;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { f as PreprEnvironment, g as PreprEventType, h as PreprHeaderName, i as
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.1.0-beta.5";
8
+ declare const VERSION = "0.1.0-beta.6";
9
9
 
10
10
  interface ToolbarState {
11
11
  locale: Locale;
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  } from "./chunk-NLQIRN5O.js";
20
20
 
21
21
  // src/version.ts
22
- var VERSION = "0.1.0-beta.5";
22
+ var VERSION = "0.1.0-beta.6";
23
23
 
24
24
  // src/core/server.ts
25
25
  var PreprError = class extends Error {
@@ -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.1.0-beta.6";
42
42
 
43
43
  // src/core/constants.ts
44
44
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -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.1.0-beta.6";
6
6
 
7
7
  // src/core/constants.ts
8
8
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -35,7 +35,7 @@ __export(nuxt_exports, {
35
35
  module.exports = __toCommonJS(nuxt_exports);
36
36
 
37
37
  // src/version.ts
38
- var VERSION = "0.1.0-beta.5";
38
+ var VERSION = "0.1.0-beta.6";
39
39
 
40
40
  // src/core/constants.ts
41
41
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.1.0-beta.5";
2
+ var VERSION = "0.1.0-beta.6";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -35,7 +35,7 @@ __export(sveltekit_exports, {
35
35
  module.exports = __toCommonJS(sveltekit_exports);
36
36
 
37
37
  // src/version.ts
38
- var VERSION = "0.1.0-beta.5";
38
+ var VERSION = "0.1.0-beta.6";
39
39
 
40
40
  // src/core/constants.ts
41
41
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.1.0-beta.5";
2
+ var VERSION = "0.1.0-beta.6";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preprio/toolkit",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.6",
4
4
  "description": "Prepr personalization, A/B testing and preview toolkit for Next.js, Nuxt, Astro, SvelteKit and Express",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Prepr B.V. (https://prepr.io)",