@preprio/toolkit 0.5.0-rc.1 → 0.5.0-rc.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 (61) hide show
  1. package/README.md +416 -23
  2. package/dist/astro/index.cjs +1290 -1
  3. package/dist/astro/index.d.cts +47 -1
  4. package/dist/astro/index.d.ts +47 -1
  5. package/dist/astro/index.js +1297 -1
  6. package/dist/chunk-F46GJS6Y.js +26 -0
  7. package/dist/{chunk-4JNKYIEV.js → chunk-IDEC77LZ.js} +1423 -642
  8. package/dist/chunk-IPMB32AZ.js +246 -0
  9. package/dist/chunk-KD3GK6TO.js +1221 -0
  10. package/dist/chunk-S3VT4UWM.js +75 -0
  11. package/dist/chunk-TBBU3LDD.js +184 -0
  12. package/dist/chunk-URXSDJOU.js +356 -0
  13. package/dist/codegen/index.cjs +82 -0
  14. package/dist/codegen/index.d.cts +70 -0
  15. package/dist/codegen/index.d.ts +70 -0
  16. package/dist/codegen/index.js +64 -0
  17. package/dist/index.cjs +3342 -417
  18. package/dist/index.d.cts +276 -5
  19. package/dist/index.d.ts +276 -5
  20. package/dist/index.js +307 -144
  21. package/dist/live-preview-DLXGXRE5.js +18 -0
  22. package/dist/nextjs/components.cjs +1582 -386
  23. package/dist/nextjs/components.d.cts +1 -1
  24. package/dist/nextjs/components.d.ts +1 -1
  25. package/dist/nextjs/components.js +4 -1
  26. package/dist/nextjs/index.cjs +1181 -6
  27. package/dist/nextjs/index.d.cts +83 -3
  28. package/dist/nextjs/index.d.ts +83 -3
  29. package/dist/nextjs/index.js +1179 -7
  30. package/dist/nextjs/live-action.cjs +285 -0
  31. package/dist/nextjs/live-action.d.cts +12 -0
  32. package/dist/nextjs/live-action.d.ts +12 -0
  33. package/dist/nextjs/live-action.js +68 -0
  34. package/dist/nextjs/live.cjs +1899 -0
  35. package/dist/nextjs/live.d.cts +54 -0
  36. package/dist/nextjs/live.d.ts +54 -0
  37. package/dist/nextjs/live.js +268 -0
  38. package/dist/nuxt/chunk-BWPGO25E.js +340 -0
  39. package/dist/nuxt/index.cjs +1 -1
  40. package/dist/nuxt/index.js +1 -1
  41. package/dist/nuxt/live-preview-2RCU35W5.js +1189 -0
  42. package/dist/nuxt/live.cjs +1816 -0
  43. package/dist/nuxt/live.d.cts +43 -0
  44. package/dist/nuxt/live.d.ts +43 -0
  45. package/dist/nuxt/live.js +198 -0
  46. package/dist/{pixel-e1fUWl2Y.d.cts → pixel-CX422dCO.d.cts} +18 -3
  47. package/dist/{pixel-e1fUWl2Y.d.ts → pixel-CX422dCO.d.ts} +18 -3
  48. package/dist/react/components.cjs +1582 -386
  49. package/dist/react/components.d.cts +17 -2
  50. package/dist/react/components.d.ts +17 -2
  51. package/dist/react/components.js +1582 -386
  52. package/dist/sveltekit/chunk-BWPGO25E.js +340 -0
  53. package/dist/sveltekit/index.cjs +1 -1
  54. package/dist/sveltekit/index.js +1 -1
  55. package/dist/sveltekit/live-preview-2RCU35W5.js +1189 -0
  56. package/dist/sveltekit/live.cjs +1830 -0
  57. package/dist/sveltekit/live.d.cts +59 -0
  58. package/dist/sveltekit/live.d.ts +59 -0
  59. package/dist/sveltekit/live.js +204 -0
  60. package/package.json +37 -11
  61. package/src/astro/PreprToolbar.astro +7 -1
package/dist/index.js CHANGED
@@ -4,185 +4,341 @@ import {
4
4
  COOKIE_SEGMENT,
5
5
  COOKIE_UID,
6
6
  COOKIE_VARIANT,
7
+ HEADER_LIVE_RENDER,
7
8
  ONE_YEAR_SECONDS,
8
9
  PARAM_HIDE_BAR,
9
10
  PARAM_SEGMENT,
10
11
  PARAM_VARIANT,
11
12
  PreprToolbarElement,
12
- VERSION,
13
13
  createPreprPreview,
14
14
  createToolbarStore,
15
+ decodeStega,
15
16
  definePreprToolbar,
16
17
  loadTrackingPixel,
17
- resolveFeatures,
18
18
  setTrackingParam,
19
19
  stegaClean,
20
20
  trackEvent
21
- } from "./chunk-4JNKYIEV.js";
21
+ } from "./chunk-IDEC77LZ.js";
22
+ import {
23
+ PreprError,
24
+ extractAccessToken,
25
+ getActiveSegmentFromHeaders,
26
+ getActiveVariantFromHeaders,
27
+ getPreprEnvironmentSegments,
28
+ getPreprHeadersFromHeaders,
29
+ getPreprUUIDFromHeaders,
30
+ getToolbarPropsFromHeaders,
31
+ validatePreprToken
32
+ } from "./chunk-TBBU3LDD.js";
33
+ import {
34
+ VERSION,
35
+ resolveFeatures
36
+ } from "./chunk-F46GJS6Y.js";
37
+ import {
38
+ createLiveBridge,
39
+ createLiveStore
40
+ } from "./chunk-IPMB32AZ.js";
41
+ import "./chunk-URXSDJOU.js";
42
+ import {
43
+ collectFields,
44
+ normalizeQuery,
45
+ parsePatchLog,
46
+ readQueryDocument
47
+ } from "./chunk-KD3GK6TO.js";
48
+ import {
49
+ createScopedLogger
50
+ } from "./chunk-S3VT4UWM.js";
22
51
 
23
- // src/core/server.ts
24
- var PreprError = class extends Error {
25
- constructor(message, code, context, originalError) {
26
- super(message);
27
- this.code = code;
28
- this.context = context;
29
- this.originalError = originalError;
30
- this.name = "PreprError";
31
- }
32
- };
33
- function getPreprUUIDFromHeaders(headers) {
34
- return headers.get("prepr-customer-id");
35
- }
36
- function getActiveSegmentFromHeaders(headers) {
37
- return headers.get("Prepr-Segments");
38
- }
39
- function getActiveVariantFromHeaders(headers) {
40
- return headers.get("Prepr-ABtesting");
41
- }
42
- var PREPR_HEADER_KEYS = [
43
- "prepr-customer-id",
44
- "Prepr-Segments",
45
- "Prepr-ABtesting",
46
- "Prepr-Preview-Bar",
47
- "Prepr-Context-utm_source",
48
- "Prepr-Context-utm_medium",
49
- "Prepr-Context-utm_term",
50
- "Prepr-Context-utm_content",
51
- "Prepr-Context-utm_campaign",
52
- "Prepr-Context-initial_referral",
53
- "Prepr-Visitor-IP",
54
- "Prepr-Hubspot-Id",
55
- "Prepr-Customer-Id-Created",
56
- "User-Agent",
57
- "Prepr-Package"
58
- ];
59
- function getPreprHeadersFromHeaders(headers) {
60
- const preprHeaders = {};
61
- const copy = (key) => {
62
- const value = headers.get(key);
63
- if (value !== null) {
64
- preprHeaders[key] = value;
65
- }
52
+ // src/core/preview-attrs.ts
53
+ function previewAttrs(target, options) {
54
+ if (!options.enabled || !target?.id || !target.field) return {};
55
+ const attrs = {
56
+ "data-prepr-encoded": "",
57
+ "data-prepr-id": target.id,
58
+ "data-prepr-field": target.field
66
59
  };
67
- for (const key of PREPR_HEADER_KEYS) {
68
- copy(key);
69
- }
70
- return preprHeaders;
60
+ if (target.locale) attrs["data-prepr-locale"] = target.locale;
61
+ if (target.entryId) attrs["data-prepr-entry-id"] = target.entryId;
62
+ return attrs;
71
63
  }
72
- function validatePreprToken(token) {
73
- if (!token) {
74
- throw new PreprError(
75
- "Token is required",
76
- "MISSING_TOKEN",
77
- "validatePreprToken"
78
- );
64
+ var URL_BASE = "https://prepr.invalid";
65
+ function stegaTarget(value, options = {}) {
66
+ const decoded = value ? decodeStega(value) : null;
67
+ if (!decoded) return null;
68
+ let url;
69
+ try {
70
+ url = new URL(decoded.href, URL_BASE);
71
+ } catch {
72
+ return null;
79
73
  }
80
- if (!token.startsWith("https://")) {
81
- throw new PreprError(
82
- "Token must be a valid HTTPS URL",
83
- "INVALID_TOKEN",
84
- "validatePreprToken"
85
- );
74
+ const id = decodeURIComponent(url.pathname.split("/").pop() ?? "");
75
+ const field = url.searchParams.get("field");
76
+ if (!id || !field) return null;
77
+ return {
78
+ id,
79
+ field: options.field ? `${field.slice(0, field.lastIndexOf(".") + 1)}${options.field}` : field,
80
+ locale: url.searchParams.get("locale")
81
+ };
82
+ }
83
+
84
+ // src/core/live-preview/edit-links.ts
85
+ import {
86
+ vercelStegaCombine,
87
+ vercelStegaDecode,
88
+ vercelStegaSplit
89
+ } from "@vercel/stega";
90
+ var ENTRY_PARAM = "entry";
91
+ var TOP_LEVEL_ENTRY = /^[A-Za-z0-9_]+\.\d+$/;
92
+ function entryIdOf(element) {
93
+ if (!isRecord(element)) return void 0;
94
+ if (typeof element._id === "string") return element._id;
95
+ if (element.__typename !== "Assets" || !Array.isArray(element.items)) {
96
+ return void 0;
86
97
  }
98
+ const first = element.items[0];
99
+ return isRecord(first) && typeof first._id === "string" ? first._id : void 0;
87
100
  }
88
- function extractAccessToken(graphqlUrl) {
89
- let url;
101
+ function isRecord(value) {
102
+ return typeof value === "object" && value !== null && !Array.isArray(value);
103
+ }
104
+ function annotate(value, ctx) {
105
+ if (!ctx.entryId) return value;
106
+ const { cleaned, encoded } = vercelStegaSplit(value);
107
+ if (!encoded) return value;
108
+ let payload;
90
109
  try {
91
- url = new URL(graphqlUrl);
92
- } catch (error) {
93
- throw new PreprError(
94
- "Token must be a valid URL",
95
- "INVALID_TOKEN",
96
- "extractAccessToken",
97
- error instanceof Error ? error : new Error(String(error))
98
- );
110
+ payload = vercelStegaDecode(encoded);
111
+ } catch {
112
+ return value;
99
113
  }
100
- if (url.hostname !== "graphql.prepr.io") {
101
- throw new PreprError(
102
- "Token must be a graphql.prepr.io URL",
103
- "INVALID_TOKEN",
104
- "extractAccessToken"
105
- );
114
+ if (typeof payload?.href !== "string") return value;
115
+ let url;
116
+ try {
117
+ url = new URL(payload.href);
118
+ } catch {
119
+ return value;
106
120
  }
107
- const pathParts = url.pathname.split("/");
108
- const token = pathParts[pathParts.length - 1];
109
- if (!token) {
110
- throw new PreprError(
111
- "Token URL is missing an access token segment",
112
- "INVALID_TOKEN",
113
- "extractAccessToken"
114
- );
121
+ if (url.searchParams.has(ENTRY_PARAM)) return value;
122
+ url.searchParams.set(ENTRY_PARAM, ctx.entryId);
123
+ const itemId = decodeURIComponent(url.pathname.split("/").pop() ?? "");
124
+ if (!url.searchParams.has("field") && ctx.entryField && TOP_LEVEL_ENTRY.test(ctx.entryField) && itemId === ctx.entryOwnerId) {
125
+ url.searchParams.set("field", ctx.entryField);
115
126
  }
116
- return token;
127
+ return vercelStegaCombine(
128
+ cleaned,
129
+ { ...payload, href: url.toString() },
130
+ false
131
+ );
117
132
  }
118
- async function getPreprEnvironmentSegments(token) {
119
- validatePreprToken(token);
120
- try {
121
- const response = await fetch(token, {
122
- headers: {
123
- "Prepr-Package": `@preprio/toolkit@${VERSION}`,
124
- // Duplicates the version in Prepr-Package. It keeps the User-Agent
125
- // format used by `@preprio/prepr-nextjs`, for Prepr API consumers that
126
- // still identify the toolbar by that string.
127
- "User-Agent": `Prepr-Preview-Bar/${VERSION}`,
128
- "Content-Type": "application/json"
129
- },
130
- method: "POST",
131
- body: JSON.stringify({
132
- query: `{
133
- _Segments {
134
- _id
135
- name
136
- }
137
- }`
138
- })
133
+ function walk(value, selections, ctx) {
134
+ if (typeof value === "string") return annotate(value, ctx);
135
+ if (Array.isArray(value)) {
136
+ let changed2 = false;
137
+ const next2 = value.map((element, index) => {
138
+ const path = [...ctx.path, String(index)];
139
+ const entryId = ctx.inAssets ? void 0 : entryIdOf(element);
140
+ const entry = entryId ? {
141
+ entryId,
142
+ entryField: ctx.ownerId ? path.join(".") : void 0,
143
+ entryOwnerId: ctx.ownerId,
144
+ inAssets: isRecord(element) && element._id === void 0
145
+ } : {};
146
+ const out = walk(element, selections, { ...ctx, ...entry, path });
147
+ if (out !== element) changed2 = true;
148
+ return out;
149
+ });
150
+ return changed2 ? next2 : value;
151
+ }
152
+ if (!isRecord(value)) return value;
153
+ const id = typeof value._id === "string" ? value._id : void 0;
154
+ const typename = typeof value.__typename === "string" ? value.__typename : void 0;
155
+ const fields = ctx.doc && selections ? collectFields(selections, typename, ctx.doc) : null;
156
+ let changed = false;
157
+ const next = {};
158
+ for (const [key, child] of Object.entries(value)) {
159
+ const field = fields?.get(key);
160
+ const name = field?.name ?? key;
161
+ const out = walk(child, field?.selections ?? null, {
162
+ ...ctx,
163
+ ownerId: id ?? ctx.ownerId,
164
+ path: id ? [name] : [...ctx.path, name]
139
165
  });
140
- if (!response.ok) {
141
- throw new PreprError(
142
- `HTTP ${response.status}: ${response.statusText}`,
143
- "HTTP_ERROR",
144
- "getPreprEnvironmentSegments"
145
- );
166
+ if (out !== child) changed = true;
167
+ next[key] = out;
168
+ }
169
+ return changed ? next : value;
170
+ }
171
+ function annotateEditLinks(result, query, operationName) {
172
+ let doc = null;
173
+ if (query !== void 0) {
174
+ try {
175
+ doc = readQueryDocument(query, operationName);
176
+ } catch {
177
+ doc = null;
146
178
  }
147
- const json = await response.json();
148
- if (!json || !json.data || !json.data._Segments) {
149
- throw new PreprError(
150
- "Invalid response format from Prepr API",
151
- "INVALID_RESPONSE",
152
- "getPreprEnvironmentSegments"
153
- );
179
+ }
180
+ return walk(result, doc?.operation ?? null, { doc, path: [] });
181
+ }
182
+
183
+ // src/core/live-preview/morph.ts
184
+ function morphElement(current, next) {
185
+ syncAttributes(current, next);
186
+ morphChildren(current, next);
187
+ }
188
+ var SKIP_ATTR = "data-prepr-morph-skip";
189
+ var RUNTIME_ATTR_PREFIXES = ["data-prepr-"];
190
+ function isRuntimeAttribute(name) {
191
+ return RUNTIME_ATTR_PREFIXES.some((prefix) => name.startsWith(prefix));
192
+ }
193
+ function syncAttributes(current, next) {
194
+ for (const { name } of Array.from(current.attributes)) {
195
+ if (!next.hasAttribute(name) && !isRuntimeAttribute(name)) {
196
+ current.removeAttribute(name);
154
197
  }
155
- return json.data._Segments;
156
- } catch (error) {
157
- if (error instanceof PreprError) {
158
- throw error;
198
+ }
199
+ for (const { name, value } of Array.from(next.attributes)) {
200
+ if (current.getAttribute(name) !== value) current.setAttribute(name, value);
201
+ }
202
+ }
203
+ function isSkipped(node) {
204
+ return node.nodeType === Node.ELEMENT_NODE && (node.hasAttribute(SKIP_ATTR) || node.localName === "prepr-toolbar");
205
+ }
206
+ function sameKind(a, b) {
207
+ if (a.nodeType !== b.nodeType) return false;
208
+ if (a.nodeType !== Node.ELEMENT_NODE) return true;
209
+ const ea = a;
210
+ const eb = b;
211
+ return ea.localName === eb.localName && ea.id === eb.id;
212
+ }
213
+ function morphChildren(current, next) {
214
+ const wanted = Array.from(next.childNodes).filter((n) => !isSkipped(n));
215
+ const existing = Array.from(current.childNodes).filter((n) => !isSkipped(n));
216
+ wanted.forEach((target, i) => {
217
+ const node = existing[i];
218
+ if (node && sameKind(node, target)) {
219
+ if (node.nodeType === Node.ELEMENT_NODE) {
220
+ const element = node;
221
+ if (element.localName === "script") return;
222
+ if (element.localName === "astro-island" && !element.isEqualNode(target)) {
223
+ element.replaceWith(adopt(target, current));
224
+ return;
225
+ }
226
+ morphElement(element, target);
227
+ } else if (node.nodeValue !== target.nodeValue) {
228
+ node.nodeValue = target.nodeValue;
229
+ }
230
+ return;
159
231
  }
160
- throw new PreprError(
161
- "Failed to fetch segments from Prepr API",
162
- "FETCH_ERROR",
163
- "getPreprEnvironmentSegments",
164
- error instanceof Error ? error : new Error(String(error))
165
- );
232
+ const replacement = adopt(target, current);
233
+ if (node) node.replaceWith(replacement);
234
+ else current.appendChild(replacement);
235
+ });
236
+ for (const extra of existing.slice(wanted.length)) extra.remove();
237
+ }
238
+ function adopt(node, parent) {
239
+ const copy = parent.ownerDocument.importNode(node, true);
240
+ if (copy.nodeType === Node.ELEMENT_NODE) {
241
+ const element = copy;
242
+ const scripts = element.localName === "script" ? [element] : Array.from(element.querySelectorAll("script"));
243
+ scripts.forEach((s) => s.setAttribute("data-prepr-morphed", ""));
166
244
  }
245
+ return copy;
167
246
  }
168
- async function getToolbarPropsFromHeaders(headers, token, features) {
169
- const resolved = resolveFeatures(features);
170
- let segments = [];
171
- if (resolved.segments) {
247
+
248
+ // src/core/live-preview/html-live.ts
249
+ var debug = createScopedLogger("live-preview:html");
250
+ var COMPLETE_DOCUMENT = /<\/html>\s*$/i;
251
+ var SYNC_DEBOUNCE_MS = 150;
252
+ function startHtmlLivePreview(options = {}) {
253
+ if (typeof window === "undefined" || window.self === window.top) {
254
+ return { stop() {
255
+ } };
256
+ }
257
+ const store = createLiveStore();
258
+ const bridge = createLiveBridge(store, {
259
+ allowedEditorOrigins: () => options.allowedEditorOrigins,
260
+ strictMode: () => options.strictMode
261
+ });
262
+ bridge.start();
263
+ let timer = null;
264
+ let inFlight = false;
265
+ let pending = null;
266
+ let stopped = false;
267
+ const render = async (log) => {
268
+ inFlight = true;
172
269
  try {
173
- segments = await getPreprEnvironmentSegments(token);
174
- } catch {
175
- segments = [];
270
+ const response = await fetch(window.location.href, {
271
+ method: log.length ? "POST" : "GET",
272
+ headers: log.length ? { "Content-Type": "application/json", [HEADER_LIVE_RENDER]: "1" } : {},
273
+ body: log.length ? JSON.stringify({ log }) : void 0,
274
+ credentials: "same-origin",
275
+ cache: "no-store"
276
+ });
277
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
278
+ const html = await response.text();
279
+ if (stopped) return;
280
+ if (!COMPLETE_DOCUMENT.test(html)) {
281
+ throw new Error("incomplete page; a component failed to render");
282
+ }
283
+ const next = new DOMParser().parseFromString(html, "text/html");
284
+ if (next.title && next.title !== document.title)
285
+ document.title = next.title;
286
+ morphElement(document.body, next.body);
287
+ } catch (error) {
288
+ debug.log("re-render failed", error);
289
+ } finally {
290
+ inFlight = false;
176
291
  }
177
- }
292
+ if (pending && !stopped) {
293
+ const queued = pending;
294
+ pending = null;
295
+ void render(queued);
296
+ }
297
+ };
298
+ const schedule = (log) => {
299
+ if (timer) clearTimeout(timer);
300
+ timer = setTimeout(() => {
301
+ timer = null;
302
+ if (inFlight) pending = log;
303
+ else void render(log);
304
+ }, SYNC_DEBOUNCE_MS);
305
+ };
306
+ const offLog = store.onLogChange((log) => {
307
+ if (log.length) schedule(log);
308
+ });
309
+ const offInvalidate = store.onInvalidate(() => schedule([]));
178
310
  return {
179
- activeSegment: resolved.segments ? getActiveSegmentFromHeaders(headers) : null,
180
- activeVariant: resolved.abTesting ? getActiveVariantFromHeaders(headers) : null,
181
- segments,
182
- data: segments
311
+ stop() {
312
+ stopped = true;
313
+ if (timer) clearTimeout(timer);
314
+ offLog();
315
+ offInvalidate();
316
+ bridge.stop();
317
+ }
183
318
  };
184
319
  }
185
320
 
321
+ // src/core/live-preview/live-request.ts
322
+ var MAX_BODY_BYTES = 1e6;
323
+ async function readLiveLogFromRequest(request) {
324
+ try {
325
+ if (request.method !== "POST") return [];
326
+ if (request.headers.get(HEADER_LIVE_RENDER) !== "1") return [];
327
+ const origin = request.headers.get("origin");
328
+ if (!origin || origin !== new URL(request.url).origin) return [];
329
+ const type = request.headers.get("content-type") ?? "";
330
+ if (!type.toLowerCase().startsWith("application/json")) return [];
331
+ const length = Number(request.headers.get("content-length") ?? "0");
332
+ if (length > MAX_BODY_BYTES) return [];
333
+ const text = await request.clone().text();
334
+ if (text.length > MAX_BODY_BYTES) return [];
335
+ const body = JSON.parse(text);
336
+ return parsePatchLog(body?.log);
337
+ } catch {
338
+ return [];
339
+ }
340
+ }
341
+
186
342
  // src/core/middleware.ts
187
343
  var UTM_PARAMS = [
188
344
  "utm_source",
@@ -341,6 +497,7 @@ export {
341
497
  PreprError,
342
498
  PreprToolbarElement,
343
499
  VERSION,
500
+ annotateEditLinks,
344
501
  createPreprPreview,
345
502
  createToolbarStore,
346
503
  definePreprToolbar,
@@ -352,10 +509,16 @@ export {
352
509
  getPreprUUIDFromHeaders,
353
510
  getToolbarPropsFromHeaders,
354
511
  loadTrackingPixel,
512
+ normalizeQuery,
513
+ parsePatchLog,
514
+ previewAttrs,
355
515
  processPreprRequest,
516
+ readLiveLogFromRequest,
356
517
  resolveFeatures,
357
518
  setTrackingParam,
519
+ startHtmlLivePreview,
358
520
  stegaClean,
521
+ stegaTarget,
359
522
  trackEvent,
360
523
  validatePreprToken
361
524
  };
@@ -0,0 +1,18 @@
1
+ import {
2
+ createLiveBridge,
3
+ createLiveStore
4
+ } from "./chunk-IPMB32AZ.js";
5
+ import "./chunk-URXSDJOU.js";
6
+ import {
7
+ applyPatchLog,
8
+ normalizeQuery,
9
+ parsePatchLog
10
+ } from "./chunk-KD3GK6TO.js";
11
+ import "./chunk-S3VT4UWM.js";
12
+ export {
13
+ applyPatchLog,
14
+ createLiveBridge,
15
+ createLiveStore,
16
+ normalizeQuery,
17
+ parsePatchLog
18
+ };