@preprio/toolkit 0.1.0-beta.6 → 0.2.0-beta.3
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 +178 -13
- package/dist/astro/index.cjs +37 -15
- package/dist/astro/index.d.cts +38 -8
- package/dist/astro/index.d.ts +38 -8
- package/dist/astro/index.js +37 -15
- package/dist/{chunk-NLQIRN5O.js → chunk-6OOGNVJ4.js} +83 -41
- package/dist/index.cjs +102 -55
- package/dist/index.d.cts +59 -16
- package/dist/index.d.ts +59 -16
- package/dist/index.js +22 -16
- package/dist/nextjs/components.cjs +103 -50
- package/dist/nextjs/components.d.cts +16 -9
- package/dist/nextjs/components.d.ts +16 -9
- package/dist/nextjs/components.js +23 -11
- package/dist/nextjs/index.cjs +37 -15
- package/dist/nextjs/index.d.cts +46 -16
- package/dist/nextjs/index.d.ts +46 -16
- package/dist/nextjs/index.js +37 -15
- package/dist/nuxt/index.cjs +37 -15
- package/dist/nuxt/index.d.cts +62 -32
- package/dist/nuxt/index.d.ts +62 -32
- package/dist/nuxt/index.js +37 -15
- package/dist/{pixel-DVSJQC0K.d.cts → pixel-763p7m6A.d.cts} +64 -1
- package/dist/{pixel-DVSJQC0K.d.ts → pixel-763p7m6A.d.ts} +64 -1
- package/dist/react/components.cjs +2237 -0
- package/dist/react/components.d.cts +167 -0
- package/dist/react/components.d.ts +167 -0
- package/dist/react/components.js +2212 -0
- package/dist/react/index.cjs +32 -0
- package/dist/react/index.d.cts +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +9 -0
- package/dist/sveltekit/index.cjs +37 -15
- package/dist/sveltekit/index.d.cts +62 -32
- package/dist/sveltekit/index.d.ts +62 -32
- package/dist/sveltekit/index.js +37 -15
- package/package.json +13 -2
- package/src/astro/PreprToolbar.astro +16 -10
- package/src/nuxt/PreprToolbar.vue +10 -8
- package/src/sveltekit/PreprToolbar.svelte +11 -4
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ Per-framework requirements:
|
|
|
80
80
|
| Entry point | Peer dependencies |
|
|
81
81
|
| --- | --- |
|
|
82
82
|
| `@preprio/toolkit` (vanilla core) | none |
|
|
83
|
+
| `@preprio/toolkit/react` | `react` >= 17, `react-dom` >= 17 |
|
|
83
84
|
| `@preprio/toolkit/nextjs` | `next` >= 13, `react` >= 17, `react-dom` >= 17 |
|
|
84
85
|
| `@preprio/toolkit/astro` | none (`.astro` components compile in your own pipeline) |
|
|
85
86
|
| `@preprio/toolkit/sveltekit` | `svelte` (only for the `.svelte` components) |
|
|
@@ -535,6 +536,81 @@ const preprHeaders = getPreprHeadersFromEvent(event);
|
|
|
535
536
|
|
|
536
537
|
See the runnable example at `examples/nuxt` for the full source of truth.
|
|
537
538
|
|
|
539
|
+
## React (no framework)
|
|
540
|
+
|
|
541
|
+
`@preprio/toolkit/react` carries the two components that need nothing but React — for Vite + React Router, TanStack Router, Remix SPA mode, or Create React App. Next.js users want the [Next.js section](#nextjs) instead; its `PreprToolbar` binds the App Router for you.
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
import { PreprPreview, PreprTrackingPixel } from '@preprio/toolkit/react';
|
|
545
|
+
|
|
546
|
+
export function App() {
|
|
547
|
+
return (
|
|
548
|
+
<>
|
|
549
|
+
<YourRoutes />
|
|
550
|
+
{import.meta.env.DEV && <PreprPreview />}
|
|
551
|
+
<PreprTrackingPixel id={import.meta.env.VITE_PREPR_TRACKING_ID} />
|
|
552
|
+
</>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Mount `PreprPreview` once per page — two copies start two editor bridges.
|
|
558
|
+
|
|
559
|
+
### Without personalization
|
|
560
|
+
|
|
561
|
+
A client-rendered app with no segments and no A/B testing needs **no middleware and no server helpers at all**. The middleware exists to resolve segment and variant cookies into request headers; with both features off there are no headers to resolve. What remains is click-to-edit and the editor's scroll restore, which are pure client concerns:
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
<PreprPreview options={{ features: { segments: false, abTesting: false } }} />
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
No `navigation` prop is needed here. The default adapter drives `window.location`, and with both features off the only reachable path is the preview-mode refresh — a full reload either way. `PreprPreview` also needs no router context in this configuration, so it can mount anywhere in the tree.
|
|
568
|
+
|
|
569
|
+
### With personalization
|
|
570
|
+
|
|
571
|
+
Segments and A/B testing need request headers, which need a server. A static SPA has none — so this requires either a framework with a server (React Router in framework mode, Next.js) or your own backend using the [vanilla core](#any-other-framework).
|
|
572
|
+
|
|
573
|
+
Once there is a server, pass a `navigation` adapter so segment and variant switches route softly instead of reloading:
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
import { useNavigate, useLocation } from 'react-router';
|
|
577
|
+
|
|
578
|
+
const navigate = useNavigate();
|
|
579
|
+
const location = useLocation();
|
|
580
|
+
|
|
581
|
+
<PreprPreview
|
|
582
|
+
{...toolbarProps}
|
|
583
|
+
navigation={{
|
|
584
|
+
navigate: url => navigate(url),
|
|
585
|
+
currentPath: () => location.pathname + location.search,
|
|
586
|
+
}}
|
|
587
|
+
/>;
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`navigation.reload` is optional and defaults to `window.location.reload()`. React Router has no soft-refresh equivalent, so leaving it unset is usually right.
|
|
591
|
+
|
|
592
|
+
### Tracking pageviews
|
|
593
|
+
|
|
594
|
+
`loadTrackingPixel` queues one `pageload` event when it installs. Client-side route changes emit nothing on their own, so a SPA records a single pageview per session unless you fire them yourself:
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
import { useEffect } from 'react';
|
|
598
|
+
import { useLocation } from 'react-router';
|
|
599
|
+
import { trackEvent } from '@preprio/toolkit';
|
|
600
|
+
|
|
601
|
+
function usePreprPageviews() {
|
|
602
|
+
const { pathname } = useLocation();
|
|
603
|
+
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
trackEvent('pageload');
|
|
606
|
+
}, [pathname]);
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
This double-counts the first pageview, since the pixel's own install event fires too. Skip the initial run with a ref if the exact count matters.
|
|
611
|
+
|
|
612
|
+
The `id` is your access token, and in a client-rendered app it is public — bundled into JS and visible in devtools. Use a tracking-only token; do not derive it from a browser-exposed GraphQL URL, which would also expose content read access.
|
|
613
|
+
|
|
538
614
|
## Any other framework
|
|
539
615
|
|
|
540
616
|
The core is runtime-neutral: it works off a WHATWG `Request` and hands back the headers to forward to Prepr plus the cookies to persist. Adapting it to Express, Hono, Fastify, or a plain server is roughly twenty lines.
|
|
@@ -570,11 +646,11 @@ Forward the headers on your GraphQL fetch with `getPreprHeadersFromHeaders(reque
|
|
|
570
646
|
**Client** — mount the toolbar imperatively:
|
|
571
647
|
|
|
572
648
|
```javascript
|
|
573
|
-
import {
|
|
649
|
+
import { createPreprPreview, loadTrackingPixel } from '@preprio/toolkit';
|
|
574
650
|
|
|
575
651
|
loadTrackingPixel('YOUR_ACCESS_TOKEN');
|
|
576
652
|
|
|
577
|
-
const controller =
|
|
653
|
+
const controller = createPreprPreview({ props: toolbarProps });
|
|
578
654
|
// later: controller.destroy()
|
|
579
655
|
```
|
|
580
656
|
|
|
@@ -680,9 +756,25 @@ The toolbar. Takes the props returned by `getToolbarProps`, and renders nothing
|
|
|
680
756
|
|
|
681
757
|
There is no provider to wrap your tree in; state lives in the toolbar's own store.
|
|
682
758
|
|
|
759
|
+
#### `PreprPreview`
|
|
760
|
+
|
|
761
|
+
From `@preprio/toolkit/react`. The framework-free equivalent of `PreprToolbar`: same preview runtime, with the router binding left to you. Renders nothing.
|
|
762
|
+
|
|
763
|
+
```tsx
|
|
764
|
+
<PreprPreview {...toolbarProps} navigation={adapter} />
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
| Prop | Type | Description |
|
|
768
|
+
| --- | --- | --- |
|
|
769
|
+
| `activeSegment` / `activeVariant` / `segments` | — | Toolbar data, as returned by `getToolbarProps`. Optional for a headless preview. |
|
|
770
|
+
| `options` | `PreprPreviewOptions` | Feature flags, `ui`, locale, debug (see [Preview Options](#preview-options)). |
|
|
771
|
+
| `navigation` | `PreprNavigationAdapter` | Optional router binding. Defaults to `window.location`, which is correct for any router that keeps the URL bar in sync. |
|
|
772
|
+
|
|
773
|
+
Every prop is read once on mount; changing one afterwards has no effect, since a live update would have to tear down the editor bridge and re-announce the preview. Remount to change configuration.
|
|
774
|
+
|
|
683
775
|
#### `PreprTrackingPixel`
|
|
684
776
|
|
|
685
|
-
Loads Prepr's CDN tracking pixel on mount. Renders nothing.
|
|
777
|
+
Loads Prepr's CDN tracking pixel on mount. Renders nothing. Exported from both `@preprio/toolkit/react` and `@preprio/toolkit/nextjs` — the same component, with nothing Next-specific about it.
|
|
686
778
|
|
|
687
779
|
```tsx
|
|
688
780
|
<PreprTrackingPixel id={accessToken} config={{ destinations: { googleTagManager: true } }} />
|
|
@@ -707,12 +799,12 @@ const { requestHeaders, responseCookies } = processPreprRequest(request, { previ
|
|
|
707
799
|
// responseCookies: CookieSpec[] — { name, value, maxAge, path }
|
|
708
800
|
```
|
|
709
801
|
|
|
710
|
-
#### `
|
|
802
|
+
#### `createPreprPreview(options)`
|
|
711
803
|
|
|
712
|
-
|
|
804
|
+
Starts the preview runtime — optionally mounting the toolbar — and returns a controller.
|
|
713
805
|
|
|
714
806
|
```typescript
|
|
715
|
-
const controller =
|
|
807
|
+
const controller = createPreprPreview({
|
|
716
808
|
props: toolbarProps,
|
|
717
809
|
options: { debug: true, locale: 'nl' },
|
|
718
810
|
navigation: {
|
|
@@ -727,13 +819,15 @@ controller.destroy();
|
|
|
727
819
|
|
|
728
820
|
| Option | Type | Description |
|
|
729
821
|
| --- | --- | --- |
|
|
730
|
-
| `props` | `PreprToolbarProps` |
|
|
731
|
-
| `options` | `
|
|
822
|
+
| `props` | `PreprToolbarProps` | From `getToolbarProps`. Optional — a headless preview has no segment list to pass. |
|
|
823
|
+
| `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins? }`. |
|
|
732
824
|
| `navigation` | `PreprNavigationAdapter` | How segment/variant switches navigate. |
|
|
733
825
|
|
|
734
826
|
The navigation adapter is optional. Omit it and the toolbar uses `window.location.assign` — a full page load. Supply one to integrate with a client-side router; its `reload` member is optional too, and runs after a preview-mode toggle (defaulting to `window.location.reload()`). The Next.js and SvelteKit wrappers wire all of this up for you.
|
|
735
827
|
|
|
736
|
-
`
|
|
828
|
+
`createPreprPreview` is a no-op outside a browser (no `window`/`document`) and returns a controller whose `destroy()` does nothing, so it is safe to call during SSR.
|
|
829
|
+
|
|
830
|
+
Chrome is skipped — while every side effect stays wired — when any of these hold: `ui: false`, `?prepr_hide_bar=true` (how the Prepr live-preview iframe suppresses the bar), or running inside the editor's iframe. See [Headless preview](#headless-preview-no-toolbar-ui).
|
|
737
831
|
|
|
738
832
|
#### `loadTrackingPixel(id, config?)`
|
|
739
833
|
|
|
@@ -810,18 +904,89 @@ createPreprMiddleware(request, response, { preview: true });
|
|
|
810
904
|
| Option | Type | Description |
|
|
811
905
|
| --- | --- | --- |
|
|
812
906
|
| `preview` | `boolean` | Enables preview mode. The only gate — resolve it from your own environment signal. |
|
|
907
|
+
| `features` | `PreprFeatures` | Disable features app-wide. See [Feature flags](#feature-flags). |
|
|
813
908
|
| `version` | `string` | Override the version reported in the `Prepr-Package` header. Mainly for tests. |
|
|
814
909
|
|
|
815
|
-
###
|
|
910
|
+
### Preview Options
|
|
816
911
|
|
|
817
912
|
```typescript
|
|
818
|
-
|
|
913
|
+
createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
|
|
819
914
|
```
|
|
820
915
|
|
|
821
916
|
| Option | Type | Default | Description |
|
|
822
917
|
| --- | --- | --- | --- |
|
|
823
918
|
| `debug` | `boolean` | `false` | Enable debug logging. |
|
|
824
919
|
| `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
|
|
920
|
+
| `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
|
|
921
|
+
| `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
|
|
922
|
+
| `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted editors. Replaces the wildcard. |
|
|
923
|
+
|
|
924
|
+
`features` and `ui` are independent: `features` decides *what runs*, `ui` decides *whether the toolbar is visible*.
|
|
925
|
+
|
|
926
|
+
### Headless preview (no toolbar UI)
|
|
927
|
+
|
|
928
|
+
`ui: false` keeps every non-visual side effect wired — click-to-edit, the editor bridge, scroll restore, cookies and headers — while rendering no chrome of its own. That is how a site gets live editing, or editor scroll restore, alongside its own UI instead of the Prepr bar.
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
// Live editing, no bar:
|
|
932
|
+
const preview = createPreprPreview({
|
|
933
|
+
options: { ui: false, features: { editMode: true } },
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
preview.destroy();
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
`props` is optional here — a headless preview that only wants click-to-edit or scroll restore has no segment list to pass. For scroll restore and nothing else:
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
createPreprPreview({
|
|
943
|
+
options: {
|
|
944
|
+
ui: false,
|
|
945
|
+
features: { segments: false, abTesting: false, editMode: false },
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
`?prepr_hide_bar=true` and the editor's own iframe both imply `ui: false`. Outside an iframe the bridge is a no-op, so mounting unconditionally is safe. Call `createPreprPreview` **once per page** — two calls start two bridges and announce the preview twice.
|
|
951
|
+
|
|
952
|
+
### Feature flags
|
|
953
|
+
|
|
954
|
+
Preview mode is the master switch. Within it, each feature can be turned off with a `features` object. Everything is **on by default** — omit it and nothing changes.
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
import type { PreprFeatures } from '@preprio/toolkit';
|
|
958
|
+
|
|
959
|
+
export const preprFeatures: PreprFeatures = {
|
|
960
|
+
segments: false, // or { enabled: false }
|
|
961
|
+
abTesting: true,
|
|
962
|
+
editMode: { enabled: false },
|
|
963
|
+
};
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
| Feature | Off means |
|
|
967
|
+
| --- | --- |
|
|
968
|
+
| `segments` | No segment picker. No `Prepr-Segments` header, from cookie **or** `?prepr_preview_segment`. No segment cookie written. The `_Segments` API call is skipped, saving a round-trip per page. |
|
|
969
|
+
| `abTesting` | No A/B control or variant badge. No `Prepr-ABtesting` header, from cookie **or** `?prepr_preview_ab`. No variant cookie written. |
|
|
970
|
+
| `editMode` | No Edit mode control, no click-to-edit overlay, no close-edit pill. |
|
|
971
|
+
|
|
972
|
+
Pass the **same object to both sides**. Each enforces its own half, so a feature disabled in only one place is still half-on — the UI would hide a control the middleware still honours:
|
|
973
|
+
|
|
974
|
+
```typescript
|
|
975
|
+
// server
|
|
976
|
+
createPreprMiddleware(request, { preview: true, features: preprFeatures });
|
|
977
|
+
const props = await getToolbarProps(graphqlUrl, preprFeatures);
|
|
978
|
+
|
|
979
|
+
// client
|
|
980
|
+
<PreprToolbar {...props} options={{ features: preprFeatures }} />
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
Every framework's `getToolbarProps` takes `features` as its last argument, and every `<PreprToolbar>` accepts an `options` prop. Keeping the object in one module both sides import is the simplest way to stay in sync.
|
|
984
|
+
|
|
985
|
+
Disabling a feature also stops its `segment_changed` / `variant_changed` events, and the Reset button ignores it — state the user cannot see is never rewritten.
|
|
986
|
+
|
|
987
|
+
**`editMode` does not disable the Prepr visual editor.** It governs your site's own click-to-edit affordance. When the Prepr editor frames your site it drives edit mode over its own `postMessage` handshake, which keeps working regardless — that is the CMS operating inside its own iframe, not an affordance offered to your visitors. Treat `editMode: false` as a UI choice, never as a security control.
|
|
988
|
+
|
|
989
|
+
Feature flags govern the toolbar and the middleware. The toolbar-free scroll-sync entry point carries no personalization state, so there is nothing there to disable.
|
|
825
990
|
|
|
826
991
|
### Pixel Options
|
|
827
992
|
|
|
@@ -864,7 +1029,7 @@ Because the toolbar renders inside a shadow root, none of your own page CSS —
|
|
|
864
1029
|
The toolbar UI ships with English and Dutch. Pass `locale` explicitly, or omit it to auto-detect from the browser and fall back to `en`:
|
|
865
1030
|
|
|
866
1031
|
```typescript
|
|
867
|
-
|
|
1032
|
+
createPreprPreview({ props, options: { locale: 'nl' } });
|
|
868
1033
|
```
|
|
869
1034
|
|
|
870
1035
|
## Troubleshooting
|
|
@@ -916,7 +1081,7 @@ Codes: `INVALID_TOKEN`, `MISSING_TOKEN`, `HTTP_ERROR`, `FETCH_ERROR`, `INVALID_R
|
|
|
916
1081
|
### Debug Mode
|
|
917
1082
|
|
|
918
1083
|
```typescript
|
|
919
|
-
|
|
1084
|
+
createPreprPreview({ props, options: { debug: true } });
|
|
920
1085
|
```
|
|
921
1086
|
|
|
922
1087
|
## How It Works
|
package/dist/astro/index.cjs
CHANGED
|
@@ -30,7 +30,25 @@ __export(astro_exports, {
|
|
|
30
30
|
module.exports = __toCommonJS(astro_exports);
|
|
31
31
|
|
|
32
32
|
// src/version.ts
|
|
33
|
-
var VERSION = "0.
|
|
33
|
+
var VERSION = "0.2.0-beta.3";
|
|
34
|
+
|
|
35
|
+
// src/core/features.ts
|
|
36
|
+
function isEnabled(config) {
|
|
37
|
+
if (typeof config === "boolean") return config;
|
|
38
|
+
return config?.enabled ?? true;
|
|
39
|
+
}
|
|
40
|
+
function resolveFeatures(features) {
|
|
41
|
+
return {
|
|
42
|
+
segments: isEnabled(features?.segments),
|
|
43
|
+
abTesting: isEnabled(features?.abTesting),
|
|
44
|
+
editMode: isEnabled(features?.editMode)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
var ALL_FEATURES_ENABLED = Object.freeze({
|
|
48
|
+
segments: true,
|
|
49
|
+
abTesting: true,
|
|
50
|
+
editMode: true
|
|
51
|
+
});
|
|
34
52
|
|
|
35
53
|
// src/core/server.ts
|
|
36
54
|
var PreprError = class extends Error {
|
|
@@ -138,16 +156,19 @@ async function getPreprEnvironmentSegments(token) {
|
|
|
138
156
|
);
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
|
-
async function getToolbarPropsFromHeaders(headers, token) {
|
|
159
|
+
async function getToolbarPropsFromHeaders(headers, token, features) {
|
|
160
|
+
const resolved = resolveFeatures(features);
|
|
142
161
|
let segments = [];
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
if (resolved.segments) {
|
|
163
|
+
try {
|
|
164
|
+
segments = await getPreprEnvironmentSegments(token);
|
|
165
|
+
} catch {
|
|
166
|
+
segments = [];
|
|
167
|
+
}
|
|
147
168
|
}
|
|
148
169
|
return {
|
|
149
|
-
activeSegment: getActiveSegmentFromHeaders(headers),
|
|
150
|
-
activeVariant: getActiveVariantFromHeaders(headers),
|
|
170
|
+
activeSegment: resolved.segments ? getActiveSegmentFromHeaders(headers) : null,
|
|
171
|
+
activeVariant: resolved.abTesting ? getActiveVariantFromHeaders(headers) : null,
|
|
151
172
|
segments,
|
|
152
173
|
data: segments
|
|
153
174
|
};
|
|
@@ -247,7 +268,8 @@ function processPreprRequest(request, opts) {
|
|
|
247
268
|
if (!opts?.preview) {
|
|
248
269
|
return { requestHeaders, responseCookies };
|
|
249
270
|
}
|
|
250
|
-
const
|
|
271
|
+
const features = resolveFeatures(opts?.features);
|
|
272
|
+
const hasPreviewParams = features.segments && searchParams.has(PARAM_SEGMENT) || features.abTesting && searchParams.has(PARAM_VARIANT);
|
|
251
273
|
if (!hasPreviewParams) {
|
|
252
274
|
const previewModeCookie = requestCookies.get(COOKIE_PREVIEW_MODE);
|
|
253
275
|
const previewModeEnabled = previewModeCookie !== "false";
|
|
@@ -267,16 +289,16 @@ function processPreprRequest(request, opts) {
|
|
|
267
289
|
});
|
|
268
290
|
}
|
|
269
291
|
if (!isLivePreview) {
|
|
270
|
-
const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
|
|
292
|
+
const segmentCookie = features.segments ? requestCookies.get(COOKIE_SEGMENT) : void 0;
|
|
271
293
|
if (segmentCookie) {
|
|
272
294
|
requestHeaders.set("Prepr-Segments", segmentCookie);
|
|
273
295
|
}
|
|
274
|
-
const abCookie = requestCookies.get(COOKIE_VARIANT);
|
|
296
|
+
const abCookie = features.abTesting ? requestCookies.get(COOKIE_VARIANT) : void 0;
|
|
275
297
|
if (abCookie) {
|
|
276
298
|
requestHeaders.set("Prepr-ABtesting", abCookie);
|
|
277
299
|
}
|
|
278
300
|
}
|
|
279
|
-
const previewAb = searchParams.get(PARAM_VARIANT);
|
|
301
|
+
const previewAb = features.abTesting ? searchParams.get(PARAM_VARIANT) : null;
|
|
280
302
|
if (previewAb !== null) {
|
|
281
303
|
requestHeaders.set("Prepr-ABtesting", previewAb);
|
|
282
304
|
if (!isLivePreview) {
|
|
@@ -289,7 +311,7 @@ function processPreprRequest(request, opts) {
|
|
|
289
311
|
});
|
|
290
312
|
}
|
|
291
313
|
}
|
|
292
|
-
const previewSegment = searchParams.get(PARAM_SEGMENT);
|
|
314
|
+
const previewSegment = features.segments ? searchParams.get(PARAM_SEGMENT) : null;
|
|
293
315
|
if (previewSegment !== null) {
|
|
294
316
|
requestHeaders.set("Prepr-Segments", previewSegment);
|
|
295
317
|
if (!isLivePreview) {
|
|
@@ -329,8 +351,8 @@ function getActiveVariant(headers) {
|
|
|
329
351
|
function getPreprHeaders(headers) {
|
|
330
352
|
return getPreprHeadersFromHeaders(headers);
|
|
331
353
|
}
|
|
332
|
-
async function getToolbarProps(headers, token) {
|
|
333
|
-
return getToolbarPropsFromHeaders(headers, token);
|
|
354
|
+
async function getToolbarProps(headers, token, features) {
|
|
355
|
+
return getToolbarPropsFromHeaders(headers, token, features);
|
|
334
356
|
}
|
|
335
357
|
// Annotate the CommonJS export names for ESM import in node:
|
|
336
358
|
0 && (module.exports = {
|
package/dist/astro/index.d.cts
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
interface PreprMiddlewareOptions {
|
|
2
|
-
/** Enable preview-mode headers (segment/AB overrides, preview bar). */
|
|
3
|
-
preview?: boolean;
|
|
4
|
-
/** Override the version reported in Prepr-Package (mainly for tests). */
|
|
5
|
-
version?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
interface PreprSegment {
|
|
9
2
|
readonly _id: string;
|
|
10
3
|
readonly name: string;
|
|
11
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Per-feature config. `false` is shorthand for `{ enabled: false }`; the object
|
|
7
|
+
* form leaves room for per-feature options later without a breaking change.
|
|
8
|
+
*/
|
|
9
|
+
type PreprFeatureConfig = boolean | {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Toolkit features a consumer can turn off. Omitted keys stay enabled. */
|
|
13
|
+
interface PreprFeatures {
|
|
14
|
+
/** Segment picker, `Prepr-Segments` header and its cookie/query param. */
|
|
15
|
+
segments?: PreprFeatureConfig;
|
|
16
|
+
/** A/B variant switch, `Prepr-ABtesting` header and its cookie/query param. */
|
|
17
|
+
abTesting?: PreprFeatureConfig;
|
|
18
|
+
/**
|
|
19
|
+
* The site's own click-to-edit: the toolbar's Edit mode control, the stega
|
|
20
|
+
* tooltip/overlay and the close-edit pill.
|
|
21
|
+
*
|
|
22
|
+
* NOT a security control, and NOT the Prepr editor's live preview. When the
|
|
23
|
+
* site is framed by the editor, `prepr:initVE` still activates edit mode —
|
|
24
|
+
* that is the CMS operating inside its own iframe, not an affordance offered
|
|
25
|
+
* to visitors. See iframe-bridge.ts.
|
|
26
|
+
*/
|
|
27
|
+
editMode?: PreprFeatureConfig;
|
|
28
|
+
}
|
|
12
29
|
interface PreprToolbarProps {
|
|
13
30
|
readonly activeSegment: string | null;
|
|
14
31
|
readonly activeVariant: string | null;
|
|
@@ -35,6 +52,19 @@ interface PreprHeaders {
|
|
|
35
52
|
readonly 'Prepr-User-Agent'?: string;
|
|
36
53
|
}
|
|
37
54
|
|
|
55
|
+
interface PreprMiddlewareOptions {
|
|
56
|
+
/** Enable preview-mode headers (segment/AB overrides, preview bar). */
|
|
57
|
+
preview?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Disable features app-wide. Pass the same object you give the toolbar: a
|
|
60
|
+
* disabled feature injects no header and persists no cookie, whatever the
|
|
61
|
+
* request carries.
|
|
62
|
+
*/
|
|
63
|
+
features?: PreprFeatures;
|
|
64
|
+
/** Override the version reported in Prepr-Package (mainly for tests). */
|
|
65
|
+
version?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
/**
|
|
39
69
|
* Structural stand-in for Astro's `APIContext`, so this package needs no
|
|
40
70
|
* build-time dependency on `astro`. Only the fields `onPreprRequest` uses.
|
|
@@ -71,6 +101,6 @@ declare function getPreprHeaders(headers: Headers): PreprHeaders;
|
|
|
71
101
|
* your deployment already does — the toolkit reads no environment variables of
|
|
72
102
|
* its own.
|
|
73
103
|
*/
|
|
74
|
-
declare function getToolbarProps(headers: Headers, token: string): Promise<PreprToolbarProps>;
|
|
104
|
+
declare function getToolbarProps(headers: Headers, token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
|
|
75
105
|
|
|
76
106
|
export { type AstroLikeContext, type AstroNext, type PreprMiddlewareOptions, getActiveSegment, getActiveVariant, getPreprHeaders, getPreprUUID, getToolbarProps, onPreprRequest };
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
interface PreprMiddlewareOptions {
|
|
2
|
-
/** Enable preview-mode headers (segment/AB overrides, preview bar). */
|
|
3
|
-
preview?: boolean;
|
|
4
|
-
/** Override the version reported in Prepr-Package (mainly for tests). */
|
|
5
|
-
version?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
interface PreprSegment {
|
|
9
2
|
readonly _id: string;
|
|
10
3
|
readonly name: string;
|
|
11
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Per-feature config. `false` is shorthand for `{ enabled: false }`; the object
|
|
7
|
+
* form leaves room for per-feature options later without a breaking change.
|
|
8
|
+
*/
|
|
9
|
+
type PreprFeatureConfig = boolean | {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Toolkit features a consumer can turn off. Omitted keys stay enabled. */
|
|
13
|
+
interface PreprFeatures {
|
|
14
|
+
/** Segment picker, `Prepr-Segments` header and its cookie/query param. */
|
|
15
|
+
segments?: PreprFeatureConfig;
|
|
16
|
+
/** A/B variant switch, `Prepr-ABtesting` header and its cookie/query param. */
|
|
17
|
+
abTesting?: PreprFeatureConfig;
|
|
18
|
+
/**
|
|
19
|
+
* The site's own click-to-edit: the toolbar's Edit mode control, the stega
|
|
20
|
+
* tooltip/overlay and the close-edit pill.
|
|
21
|
+
*
|
|
22
|
+
* NOT a security control, and NOT the Prepr editor's live preview. When the
|
|
23
|
+
* site is framed by the editor, `prepr:initVE` still activates edit mode —
|
|
24
|
+
* that is the CMS operating inside its own iframe, not an affordance offered
|
|
25
|
+
* to visitors. See iframe-bridge.ts.
|
|
26
|
+
*/
|
|
27
|
+
editMode?: PreprFeatureConfig;
|
|
28
|
+
}
|
|
12
29
|
interface PreprToolbarProps {
|
|
13
30
|
readonly activeSegment: string | null;
|
|
14
31
|
readonly activeVariant: string | null;
|
|
@@ -35,6 +52,19 @@ interface PreprHeaders {
|
|
|
35
52
|
readonly 'Prepr-User-Agent'?: string;
|
|
36
53
|
}
|
|
37
54
|
|
|
55
|
+
interface PreprMiddlewareOptions {
|
|
56
|
+
/** Enable preview-mode headers (segment/AB overrides, preview bar). */
|
|
57
|
+
preview?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Disable features app-wide. Pass the same object you give the toolbar: a
|
|
60
|
+
* disabled feature injects no header and persists no cookie, whatever the
|
|
61
|
+
* request carries.
|
|
62
|
+
*/
|
|
63
|
+
features?: PreprFeatures;
|
|
64
|
+
/** Override the version reported in Prepr-Package (mainly for tests). */
|
|
65
|
+
version?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
/**
|
|
39
69
|
* Structural stand-in for Astro's `APIContext`, so this package needs no
|
|
40
70
|
* build-time dependency on `astro`. Only the fields `onPreprRequest` uses.
|
|
@@ -71,6 +101,6 @@ declare function getPreprHeaders(headers: Headers): PreprHeaders;
|
|
|
71
101
|
* your deployment already does — the toolkit reads no environment variables of
|
|
72
102
|
* its own.
|
|
73
103
|
*/
|
|
74
|
-
declare function getToolbarProps(headers: Headers, token: string): Promise<PreprToolbarProps>;
|
|
104
|
+
declare function getToolbarProps(headers: Headers, token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
|
|
75
105
|
|
|
76
106
|
export { type AstroLikeContext, type AstroNext, type PreprMiddlewareOptions, getActiveSegment, getActiveVariant, getPreprHeaders, getPreprUUID, getToolbarProps, onPreprRequest };
|
package/dist/astro/index.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var VERSION = "0.
|
|
2
|
+
var VERSION = "0.2.0-beta.3";
|
|
3
|
+
|
|
4
|
+
// src/core/features.ts
|
|
5
|
+
function isEnabled(config) {
|
|
6
|
+
if (typeof config === "boolean") return config;
|
|
7
|
+
return config?.enabled ?? true;
|
|
8
|
+
}
|
|
9
|
+
function resolveFeatures(features) {
|
|
10
|
+
return {
|
|
11
|
+
segments: isEnabled(features?.segments),
|
|
12
|
+
abTesting: isEnabled(features?.abTesting),
|
|
13
|
+
editMode: isEnabled(features?.editMode)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
var ALL_FEATURES_ENABLED = Object.freeze({
|
|
17
|
+
segments: true,
|
|
18
|
+
abTesting: true,
|
|
19
|
+
editMode: true
|
|
20
|
+
});
|
|
3
21
|
|
|
4
22
|
// src/core/server.ts
|
|
5
23
|
var PreprError = class extends Error {
|
|
@@ -107,16 +125,19 @@ async function getPreprEnvironmentSegments(token) {
|
|
|
107
125
|
);
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
|
-
async function getToolbarPropsFromHeaders(headers, token) {
|
|
128
|
+
async function getToolbarPropsFromHeaders(headers, token, features) {
|
|
129
|
+
const resolved = resolveFeatures(features);
|
|
111
130
|
let segments = [];
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
if (resolved.segments) {
|
|
132
|
+
try {
|
|
133
|
+
segments = await getPreprEnvironmentSegments(token);
|
|
134
|
+
} catch {
|
|
135
|
+
segments = [];
|
|
136
|
+
}
|
|
116
137
|
}
|
|
117
138
|
return {
|
|
118
|
-
activeSegment: getActiveSegmentFromHeaders(headers),
|
|
119
|
-
activeVariant: getActiveVariantFromHeaders(headers),
|
|
139
|
+
activeSegment: resolved.segments ? getActiveSegmentFromHeaders(headers) : null,
|
|
140
|
+
activeVariant: resolved.abTesting ? getActiveVariantFromHeaders(headers) : null,
|
|
120
141
|
segments,
|
|
121
142
|
data: segments
|
|
122
143
|
};
|
|
@@ -216,7 +237,8 @@ function processPreprRequest(request, opts) {
|
|
|
216
237
|
if (!opts?.preview) {
|
|
217
238
|
return { requestHeaders, responseCookies };
|
|
218
239
|
}
|
|
219
|
-
const
|
|
240
|
+
const features = resolveFeatures(opts?.features);
|
|
241
|
+
const hasPreviewParams = features.segments && searchParams.has(PARAM_SEGMENT) || features.abTesting && searchParams.has(PARAM_VARIANT);
|
|
220
242
|
if (!hasPreviewParams) {
|
|
221
243
|
const previewModeCookie = requestCookies.get(COOKIE_PREVIEW_MODE);
|
|
222
244
|
const previewModeEnabled = previewModeCookie !== "false";
|
|
@@ -236,16 +258,16 @@ function processPreprRequest(request, opts) {
|
|
|
236
258
|
});
|
|
237
259
|
}
|
|
238
260
|
if (!isLivePreview) {
|
|
239
|
-
const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
|
|
261
|
+
const segmentCookie = features.segments ? requestCookies.get(COOKIE_SEGMENT) : void 0;
|
|
240
262
|
if (segmentCookie) {
|
|
241
263
|
requestHeaders.set("Prepr-Segments", segmentCookie);
|
|
242
264
|
}
|
|
243
|
-
const abCookie = requestCookies.get(COOKIE_VARIANT);
|
|
265
|
+
const abCookie = features.abTesting ? requestCookies.get(COOKIE_VARIANT) : void 0;
|
|
244
266
|
if (abCookie) {
|
|
245
267
|
requestHeaders.set("Prepr-ABtesting", abCookie);
|
|
246
268
|
}
|
|
247
269
|
}
|
|
248
|
-
const previewAb = searchParams.get(PARAM_VARIANT);
|
|
270
|
+
const previewAb = features.abTesting ? searchParams.get(PARAM_VARIANT) : null;
|
|
249
271
|
if (previewAb !== null) {
|
|
250
272
|
requestHeaders.set("Prepr-ABtesting", previewAb);
|
|
251
273
|
if (!isLivePreview) {
|
|
@@ -258,7 +280,7 @@ function processPreprRequest(request, opts) {
|
|
|
258
280
|
});
|
|
259
281
|
}
|
|
260
282
|
}
|
|
261
|
-
const previewSegment = searchParams.get(PARAM_SEGMENT);
|
|
283
|
+
const previewSegment = features.segments ? searchParams.get(PARAM_SEGMENT) : null;
|
|
262
284
|
if (previewSegment !== null) {
|
|
263
285
|
requestHeaders.set("Prepr-Segments", previewSegment);
|
|
264
286
|
if (!isLivePreview) {
|
|
@@ -298,8 +320,8 @@ function getActiveVariant(headers) {
|
|
|
298
320
|
function getPreprHeaders(headers) {
|
|
299
321
|
return getPreprHeadersFromHeaders(headers);
|
|
300
322
|
}
|
|
301
|
-
async function getToolbarProps(headers, token) {
|
|
302
|
-
return getToolbarPropsFromHeaders(headers, token);
|
|
323
|
+
async function getToolbarProps(headers, token, features) {
|
|
324
|
+
return getToolbarPropsFromHeaders(headers, token, features);
|
|
303
325
|
}
|
|
304
326
|
export {
|
|
305
327
|
getActiveSegment,
|