@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
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What `normalizeQuery` accepts as the query: GraphQL source text, a parsed
3
+ * `DocumentNode` (from `graphql-tag`, Apollo or GraphQL Code Generator), or a
4
+ * string-like object such as codegen's `TypedDocumentString`.
5
+ */
6
+ type QueryInput = string | {
7
+ kind: 'Document';
8
+ } | {
9
+ toString(): string;
10
+ };
11
+
12
+ interface NormalizedQueryOptions {
13
+ /**
14
+ * Pass your preview flag; never `true` on a production deployment.
15
+ * Defaults to false: the data is returned unchanged.
16
+ */
17
+ enabled?: boolean;
18
+ /** Called when the editor saves, e.g. `invalidateAll` from `$app/navigation`. */
19
+ onSave?: () => void;
20
+ /** Exact editor origins, replacing the `*.prepr.io` default. */
21
+ allowedEditorOrigins?: string[];
22
+ /** See the toolbar's `strictMode`. */
23
+ strictMode?: boolean;
24
+ }
25
+ /**
26
+ * Svelte's store contract, written out so this module needs no build-time
27
+ * dependency on `svelte`. Read it with `$store` in a component.
28
+ */
29
+ interface NormalizedQueryStore<T> {
30
+ subscribe(run: (value: T) => void): () => void;
31
+ }
32
+ /**
33
+ * Show the Prepr editor's unsaved changes in a SvelteKit page, applied in the
34
+ * browser while the editor types:
35
+ *
36
+ * ```svelte
37
+ * <script lang="ts">
38
+ * import { invalidateAll } from '$app/navigation';
39
+ * import { normalizedQuery } from '@preprio/toolkit/sveltekit/live';
40
+ *
41
+ * let { data } = $props();
42
+ * const result = $derived(
43
+ * normalizedQuery(GetPageDocument, data.result, {
44
+ * enabled: data.preview,
45
+ * onSave: invalidateAll,
46
+ * }),
47
+ * );
48
+ * </script>
49
+ *
50
+ * <h1>{$result.Page.title}</h1>
51
+ * ```
52
+ *
53
+ * Returns a store that holds the data unchanged on the server and outside
54
+ * the editor iframe. Select `_id` on every item and component that should
55
+ * update.
56
+ */
57
+ declare function normalizedQuery<T>(query: QueryInput, data: T, options?: NormalizedQueryOptions): NormalizedQueryStore<T>;
58
+
59
+ export { type NormalizedQueryOptions, type NormalizedQueryStore, normalizedQuery };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What `normalizeQuery` accepts as the query: GraphQL source text, a parsed
3
+ * `DocumentNode` (from `graphql-tag`, Apollo or GraphQL Code Generator), or a
4
+ * string-like object such as codegen's `TypedDocumentString`.
5
+ */
6
+ type QueryInput = string | {
7
+ kind: 'Document';
8
+ } | {
9
+ toString(): string;
10
+ };
11
+
12
+ interface NormalizedQueryOptions {
13
+ /**
14
+ * Pass your preview flag; never `true` on a production deployment.
15
+ * Defaults to false: the data is returned unchanged.
16
+ */
17
+ enabled?: boolean;
18
+ /** Called when the editor saves, e.g. `invalidateAll` from `$app/navigation`. */
19
+ onSave?: () => void;
20
+ /** Exact editor origins, replacing the `*.prepr.io` default. */
21
+ allowedEditorOrigins?: string[];
22
+ /** See the toolbar's `strictMode`. */
23
+ strictMode?: boolean;
24
+ }
25
+ /**
26
+ * Svelte's store contract, written out so this module needs no build-time
27
+ * dependency on `svelte`. Read it with `$store` in a component.
28
+ */
29
+ interface NormalizedQueryStore<T> {
30
+ subscribe(run: (value: T) => void): () => void;
31
+ }
32
+ /**
33
+ * Show the Prepr editor's unsaved changes in a SvelteKit page, applied in the
34
+ * browser while the editor types:
35
+ *
36
+ * ```svelte
37
+ * <script lang="ts">
38
+ * import { invalidateAll } from '$app/navigation';
39
+ * import { normalizedQuery } from '@preprio/toolkit/sveltekit/live';
40
+ *
41
+ * let { data } = $props();
42
+ * const result = $derived(
43
+ * normalizedQuery(GetPageDocument, data.result, {
44
+ * enabled: data.preview,
45
+ * onSave: invalidateAll,
46
+ * }),
47
+ * );
48
+ * </script>
49
+ *
50
+ * <h1>{$result.Page.title}</h1>
51
+ * ```
52
+ *
53
+ * Returns a store that holds the data unchanged on the server and outside
54
+ * the editor iframe. Select `_id` on every item and component that should
55
+ * update.
56
+ */
57
+ declare function normalizedQuery<T>(query: QueryInput, data: T, options?: NormalizedQueryOptions): NormalizedQueryStore<T>;
58
+
59
+ export { type NormalizedQueryOptions, type NormalizedQueryStore, normalizedQuery };
@@ -0,0 +1,204 @@
1
+ import {
2
+ collectFields,
3
+ readQueryDocument
4
+ } from "./chunk-BWPGO25E.js";
5
+
6
+ // src/core/live-preview/edit-links.ts
7
+ import {
8
+ vercelStegaCombine,
9
+ vercelStegaDecode,
10
+ vercelStegaSplit
11
+ } from "@vercel/stega";
12
+ var ENTRY_PARAM = "entry";
13
+ var TOP_LEVEL_ENTRY = /^[A-Za-z0-9_]+\.\d+$/;
14
+ function entryIdOf(element) {
15
+ if (!isRecord(element)) return void 0;
16
+ if (typeof element._id === "string") return element._id;
17
+ if (element.__typename !== "Assets" || !Array.isArray(element.items)) {
18
+ return void 0;
19
+ }
20
+ const first = element.items[0];
21
+ return isRecord(first) && typeof first._id === "string" ? first._id : void 0;
22
+ }
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function annotate(value, ctx) {
27
+ if (!ctx.entryId) return value;
28
+ const { cleaned, encoded } = vercelStegaSplit(value);
29
+ if (!encoded) return value;
30
+ let payload;
31
+ try {
32
+ payload = vercelStegaDecode(encoded);
33
+ } catch {
34
+ return value;
35
+ }
36
+ if (typeof payload?.href !== "string") return value;
37
+ let url;
38
+ try {
39
+ url = new URL(payload.href);
40
+ } catch {
41
+ return value;
42
+ }
43
+ if (url.searchParams.has(ENTRY_PARAM)) return value;
44
+ url.searchParams.set(ENTRY_PARAM, ctx.entryId);
45
+ const itemId = decodeURIComponent(url.pathname.split("/").pop() ?? "");
46
+ if (!url.searchParams.has("field") && ctx.entryField && TOP_LEVEL_ENTRY.test(ctx.entryField) && itemId === ctx.entryOwnerId) {
47
+ url.searchParams.set("field", ctx.entryField);
48
+ }
49
+ return vercelStegaCombine(
50
+ cleaned,
51
+ { ...payload, href: url.toString() },
52
+ false
53
+ );
54
+ }
55
+ function walk(value, selections, ctx) {
56
+ if (typeof value === "string") return annotate(value, ctx);
57
+ if (Array.isArray(value)) {
58
+ let changed2 = false;
59
+ const next2 = value.map((element, index) => {
60
+ const path = [...ctx.path, String(index)];
61
+ const entryId = ctx.inAssets ? void 0 : entryIdOf(element);
62
+ const entry = entryId ? {
63
+ entryId,
64
+ entryField: ctx.ownerId ? path.join(".") : void 0,
65
+ entryOwnerId: ctx.ownerId,
66
+ inAssets: isRecord(element) && element._id === void 0
67
+ } : {};
68
+ const out = walk(element, selections, { ...ctx, ...entry, path });
69
+ if (out !== element) changed2 = true;
70
+ return out;
71
+ });
72
+ return changed2 ? next2 : value;
73
+ }
74
+ if (!isRecord(value)) return value;
75
+ const id = typeof value._id === "string" ? value._id : void 0;
76
+ const typename = typeof value.__typename === "string" ? value.__typename : void 0;
77
+ const fields = ctx.doc && selections ? collectFields(selections, typename, ctx.doc) : null;
78
+ let changed = false;
79
+ const next = {};
80
+ for (const [key, child] of Object.entries(value)) {
81
+ const field = fields?.get(key);
82
+ const name = field?.name ?? key;
83
+ const out = walk(child, field?.selections ?? null, {
84
+ ...ctx,
85
+ ownerId: id ?? ctx.ownerId,
86
+ path: id ? [name] : [...ctx.path, name]
87
+ });
88
+ if (out !== child) changed = true;
89
+ next[key] = out;
90
+ }
91
+ return changed ? next : value;
92
+ }
93
+ function annotateEditLinks(result, query, operationName) {
94
+ let doc = null;
95
+ if (query !== void 0) {
96
+ try {
97
+ doc = readQueryDocument(query, operationName);
98
+ } catch {
99
+ doc = null;
100
+ }
101
+ }
102
+ return walk(result, doc?.operation ?? null, { doc, path: [] });
103
+ }
104
+
105
+ // src/core/live-preview/test-reset.ts
106
+ var resets = /* @__PURE__ */ new Set();
107
+ function onTestReset(reset) {
108
+ resets.add(reset);
109
+ }
110
+
111
+ // src/sveltekit/live.ts
112
+ var starting = null;
113
+ var store = null;
114
+ var bridge = null;
115
+ var applyLog = null;
116
+ var log = [];
117
+ var allowedEditorOrigins;
118
+ var strictMode;
119
+ var logListeners = /* @__PURE__ */ new Set();
120
+ var startListeners = /* @__PURE__ */ new Set();
121
+ function constant(value) {
122
+ return {
123
+ subscribe(run) {
124
+ run(value);
125
+ return () => {
126
+ };
127
+ }
128
+ };
129
+ }
130
+ function isFramed() {
131
+ return typeof window !== "undefined" && window.self !== window.top;
132
+ }
133
+ function startLive() {
134
+ starting ?? (starting = import("./live-preview-2RCU35W5.js").then(
135
+ ({ applyPatchLog, createLiveBridge, createLiveStore }) => {
136
+ applyLog = applyPatchLog;
137
+ const started = createLiveStore();
138
+ bridge = createLiveBridge(started, {
139
+ allowedEditorOrigins: () => allowedEditorOrigins,
140
+ strictMode: () => strictMode
141
+ });
142
+ bridge.start();
143
+ started.onLogChange((next) => {
144
+ log = next;
145
+ logListeners.forEach((listener) => listener());
146
+ });
147
+ store = started;
148
+ startListeners.forEach((listener) => listener(started));
149
+ startListeners.clear();
150
+ }
151
+ ));
152
+ }
153
+ function whenLive(listener) {
154
+ if (store) {
155
+ listener(store);
156
+ return () => {
157
+ };
158
+ }
159
+ startListeners.add(listener);
160
+ return () => startListeners.delete(listener);
161
+ }
162
+ function normalizedQuery(query, data, options = {}) {
163
+ if (!options.enabled) return constant(data);
164
+ const annotated = annotateEditLinks(data, query);
165
+ if (!isFramed()) return constant(annotated);
166
+ if (options.strictMode) strictMode = true;
167
+ if (options.allowedEditorOrigins) {
168
+ allowedEditorOrigins = [...options.allowedEditorOrigins];
169
+ }
170
+ startLive();
171
+ const current = () => log.length && applyLog ? applyLog(query, annotated, log) : annotated;
172
+ return {
173
+ subscribe(run) {
174
+ run(current());
175
+ const onLog = () => run(current());
176
+ logListeners.add(onLog);
177
+ let offInvalidate = null;
178
+ const stopWaiting = options.onSave ? whenLive((started) => {
179
+ offInvalidate = started.onInvalidate(() => options.onSave?.());
180
+ }) : () => {
181
+ };
182
+ return () => {
183
+ logListeners.delete(onLog);
184
+ stopWaiting();
185
+ offInvalidate?.();
186
+ };
187
+ }
188
+ };
189
+ }
190
+ onTestReset(() => {
191
+ bridge?.stop();
192
+ bridge = null;
193
+ store = null;
194
+ starting = null;
195
+ applyLog = null;
196
+ log = [];
197
+ logListeners.clear();
198
+ startListeners.clear();
199
+ allowedEditorOrigins = void 0;
200
+ strictMode = void 0;
201
+ });
202
+ export {
203
+ normalizedQuery
204
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preprio/toolkit",
3
- "version": "0.5.0-rc.1",
3
+ "version": "0.5.0-rc.10",
4
4
  "description": "Prepr personalization, A/B testing and preview toolkit for React, Next.js, Nuxt, Astro, SvelteKit and Express",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Prepr B.V. (https://prepr.io)",
@@ -69,23 +69,43 @@
69
69
  "import": "./dist/nextjs/image-loader.js",
70
70
  "require": "./dist/nextjs/image-loader.cjs"
71
71
  },
72
+ "./nextjs/live": {
73
+ "types": "./dist/nextjs/live.d.ts",
74
+ "import": "./dist/nextjs/live.js",
75
+ "require": "./dist/nextjs/live.cjs"
76
+ },
72
77
  "./astro": {
73
78
  "types": "./dist/astro/index.d.ts",
74
79
  "import": "./dist/astro/index.js",
75
80
  "require": "./dist/astro/index.cjs"
76
81
  },
77
82
  "./astro/components/*": "./src/astro/*.astro",
83
+ "./codegen": {
84
+ "types": "./dist/codegen/index.d.ts",
85
+ "import": "./dist/codegen/index.js",
86
+ "require": "./dist/codegen/index.cjs"
87
+ },
78
88
  "./sveltekit": {
79
89
  "types": "./dist/sveltekit/index.d.ts",
80
90
  "import": "./dist/sveltekit/index.js",
81
91
  "require": "./dist/sveltekit/index.cjs"
82
92
  },
93
+ "./sveltekit/live": {
94
+ "types": "./dist/sveltekit/live.d.ts",
95
+ "import": "./dist/sveltekit/live.js",
96
+ "require": "./dist/sveltekit/live.cjs"
97
+ },
83
98
  "./sveltekit/components/*": "./src/sveltekit/*.svelte",
84
99
  "./nuxt": {
85
100
  "types": "./dist/nuxt/index.d.ts",
86
101
  "import": "./dist/nuxt/index.js",
87
102
  "require": "./dist/nuxt/index.cjs"
88
103
  },
104
+ "./nuxt/live": {
105
+ "types": "./dist/nuxt/live.d.ts",
106
+ "import": "./dist/nuxt/live.js",
107
+ "require": "./dist/nuxt/live.cjs"
108
+ },
89
109
  "./nuxt/components/*": "./src/nuxt/*.vue"
90
110
  },
91
111
  "files": [
@@ -102,13 +122,15 @@
102
122
  "build": "tsup",
103
123
  "postbuild": "node scripts/check-exports.mjs",
104
124
  "test": "vitest run",
125
+ "test:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts",
105
126
  "typecheck": "tsc --noEmit"
106
127
  },
107
128
  "dependencies": {
108
129
  "@vercel/stega": "^1.1.0",
109
- "preact": "^10.29.7"
130
+ "preact": "^10.29.8"
110
131
  },
111
132
  "peerDependencies": {
133
+ "graphql": ">=16",
112
134
  "next": ">=13",
113
135
  "react": ">=17",
114
136
  "react-dom": ">=17",
@@ -116,6 +138,9 @@
116
138
  "vue": ">=3.3"
117
139
  },
118
140
  "peerDependenciesMeta": {
141
+ "graphql": {
142
+ "optional": true
143
+ },
119
144
  "next": {
120
145
  "optional": true
121
146
  },
@@ -133,17 +158,18 @@
133
158
  }
134
159
  },
135
160
  "devDependencies": {
136
- "@types/node": "^22.20.1",
137
- "@types/react": "^19.2.17",
138
- "@types/react-dom": "^19.2.3",
139
- "browserslist": "^4.28.7",
140
- "happy-dom": "^20.11.1",
161
+ "@types/node": "^22.20.5",
162
+ "@types/react": "^19.3.0",
163
+ "@types/react-dom": "^19.3.0",
164
+ "browserslist": "^4.29.5",
165
+ "graphql": "^16.14.2",
166
+ "happy-dom": "^21.0.4",
141
167
  "lightningcss": "^1.33.0",
142
- "next": "^16.2.12",
143
- "react": "^19.2.8",
144
- "react-dom": "^19.2.8",
168
+ "next": "^16.4.3",
169
+ "react": "^19.3.1",
170
+ "react-dom": "^19.3.1",
145
171
  "tsup": "^8.5.1",
146
172
  "typescript": "^5.9.3",
147
- "vitest": "^4.1.10"
173
+ "vitest": "^4.1.12"
148
174
  }
149
175
  }
@@ -50,11 +50,17 @@ const payload = {
50
50
  />
51
51
 
52
52
  <script>
53
- import { createPreprPreview } from '@preprio/toolkit';
53
+ import { createPreprPreview, startHtmlLivePreview } from '@preprio/toolkit';
54
54
 
55
55
  const el = document.querySelector('script[data-prepr-toolbar-props]');
56
56
  if (el?.textContent) {
57
57
  const { props, options } = JSON.parse(el.textContent);
58
58
  createPreprPreview({ props, options });
59
+ // Inside the Prepr editor: re-render the page with the editor's unsaved
60
+ // changes while they type (see `normalizeQuery`). A no-op elsewhere.
61
+ startHtmlLivePreview({
62
+ allowedEditorOrigins: options?.allowedEditorOrigins,
63
+ strictMode: options?.strictMode,
64
+ });
59
65
  }
60
66
  </script>