@webiny/website-builder-sdk 6.6.0-alpha.0 → 6.6.0-alpha.1

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 (91) hide show
  1. package/ContentSdk.d.ts +13 -0
  2. package/ContentSdk.js +25 -0
  3. package/ContentSdk.js.map +1 -1
  4. package/ElementFactory.js +3 -1
  5. package/ElementFactory.js.map +1 -1
  6. package/Theme.js +2 -1
  7. package/Theme.js.map +1 -1
  8. package/asset/deliveryUrl.d.ts +41 -0
  9. package/asset/deliveryUrl.js +34 -0
  10. package/asset/deliveryUrl.js.map +1 -0
  11. package/asset/deliveryUrl.test.d.ts +1 -0
  12. package/asset/deliveryUrl.test.js +127 -0
  13. package/asset/deliveryUrl.test.js.map +1 -0
  14. package/asset/geometry.d.ts +7 -0
  15. package/asset/geometry.js +10 -0
  16. package/asset/geometry.js.map +1 -0
  17. package/asset/imageSrcSet.d.ts +63 -0
  18. package/asset/imageSrcSet.js +98 -0
  19. package/asset/imageSrcSet.js.map +1 -0
  20. package/asset/imageSrcSet.test.d.ts +1 -0
  21. package/asset/imageSrcSet.test.js +103 -0
  22. package/asset/imageSrcSet.test.js.map +1 -0
  23. package/asset/index.d.ts +5 -0
  24. package/asset/index.js +5 -0
  25. package/asset/normalize.d.ts +24 -0
  26. package/asset/normalize.js +102 -0
  27. package/asset/normalize.js.map +1 -0
  28. package/asset/normalize.test.d.ts +1 -0
  29. package/asset/normalize.test.js +256 -0
  30. package/asset/normalize.test.js.map +1 -0
  31. package/asset/types.d.ts +2 -0
  32. package/asset/types.js +0 -0
  33. package/createComponent.d.ts +5 -0
  34. package/createComponent.js +32 -0
  35. package/createComponent.js.map +1 -0
  36. package/dataProviders/ApiClient.d.ts +3 -1
  37. package/dataProviders/ApiClient.js +4 -2
  38. package/dataProviders/ApiClient.js.map +1 -1
  39. package/dataProviders/DefaultDataProvider.d.ts +4 -0
  40. package/dataProviders/DefaultDataProvider.js +35 -0
  41. package/dataProviders/DefaultDataProvider.js.map +1 -1
  42. package/dataProviders/GET_EXPERIMENT_PAUSED.d.ts +1 -0
  43. package/dataProviders/GET_EXPERIMENT_PAUSED.js +17 -0
  44. package/dataProviders/GET_EXPERIMENT_PAUSED.js.map +1 -0
  45. package/dataProviders/GET_PAGE_EXPERIMENT.d.ts +1 -0
  46. package/dataProviders/GET_PAGE_EXPERIMENT.js +32 -0
  47. package/dataProviders/GET_PAGE_EXPERIMENT.js.map +1 -0
  48. package/dataProviders/GET_VARIANT_CONTENT.d.ts +1 -0
  49. package/dataProviders/GET_VARIANT_CONTENT.js +24 -0
  50. package/dataProviders/GET_VARIANT_CONTENT.js.map +1 -0
  51. package/dataProviders/NullDataProvider.d.ts +4 -0
  52. package/dataProviders/NullDataProvider.js +9 -0
  53. package/dataProviders/NullDataProvider.js.map +1 -1
  54. package/experiments/PostHogAnalyticsProvider.d.ts +31 -0
  55. package/experiments/PostHogAnalyticsProvider.js +28 -0
  56. package/experiments/PostHogAnalyticsProvider.js.map +1 -0
  57. package/experiments/analyticsProvider.d.ts +4 -0
  58. package/experiments/analyticsProvider.js +11 -0
  59. package/experiments/analyticsProvider.js.map +1 -0
  60. package/experiments/bucketing.d.ts +27 -0
  61. package/experiments/bucketing.js +91 -0
  62. package/experiments/bucketing.js.map +1 -0
  63. package/experiments/bucketing.test.d.ts +1 -0
  64. package/experiments/bucketing.test.js +183 -0
  65. package/experiments/bucketing.test.js.map +1 -0
  66. package/experiments/index.d.ts +5 -0
  67. package/experiments/index.js +5 -0
  68. package/experiments/render.d.ts +79 -0
  69. package/experiments/render.js +149 -0
  70. package/experiments/render.js.map +1 -0
  71. package/experiments/types.d.ts +108 -0
  72. package/experiments/types.js +4 -0
  73. package/experiments/types.js.map +1 -0
  74. package/image/geometry.d.ts +54 -0
  75. package/image/geometry.js +112 -0
  76. package/image/geometry.js.map +1 -0
  77. package/image/geometry.test.d.ts +1 -0
  78. package/image/geometry.test.js +190 -0
  79. package/image/geometry.test.js.map +1 -0
  80. package/image/index.d.ts +3 -0
  81. package/image/index.js +3 -0
  82. package/image/presets.d.ts +6 -0
  83. package/image/presets.js +20 -0
  84. package/image/presets.js.map +1 -0
  85. package/image/types.d.ts +10 -0
  86. package/image/types.js +0 -0
  87. package/index.d.ts +5 -0
  88. package/index.js +5 -0
  89. package/package.json +6 -5
  90. package/types/WebsiteBuilderTheme.d.ts +5 -0
  91. package/types.d.ts +5 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset/normalize.test.js","sources":["../../src/asset/normalize.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { assetImageFromLegacyEdit, getAssetCategory, normalizeToAsset } from \"./normalize.js\";\nimport { resolveAssetImage } from \"../image/geometry.js\";\n\ndescribe(\"getAssetCategory\", () => {\n it(\"buckets by MIME prefix\", () => {\n expect(getAssetCategory(\"image/jpeg\")).toBe(\"image\");\n expect(getAssetCategory(\"image/svg+xml\")).toBe(\"image\");\n expect(getAssetCategory(\"video/mp4\")).toBe(\"video\");\n });\n\n it(\"treats everything else as a document\", () => {\n expect(getAssetCategory(\"application/pdf\")).toBe(\"document\");\n expect(getAssetCategory(\"application/zip\")).toBe(\"document\");\n expect(getAssetCategory(\"\")).toBe(\"document\");\n expect(getAssetCategory(undefined)).toBe(\"document\");\n expect(getAssetCategory(null)).toBe(\"document\");\n });\n});\n\ndescribe(\"assetImageFromLegacyEdit\", () => {\n it(\"maps hotspot -> focalPoint and keeps crop/alt/caption\", () => {\n const image = assetImageFromLegacyEdit(\n {\n crop: { top: 0.1, left: 0, bottom: 0.1, right: 0 },\n hotspot: { x: 0.3, y: 0.7, width: 0.5, height: 0.5 },\n alt: \"A cat\",\n caption: \"Meow\"\n },\n { width: 800, height: 600 }\n );\n expect(image).toEqual({\n width: 800,\n height: 600,\n crop: { top: 0.1, left: 0, bottom: 0.1, right: 0 },\n focalPoint: { x: 0.3, y: 0.7 },\n alt: \"A cat\",\n caption: \"Meow\"\n });\n });\n\n it(\"returns undefined when there is nothing to store\", () => {\n expect(assetImageFromLegacyEdit(undefined, {})).toBeUndefined();\n expect(assetImageFromLegacyEdit(null, { width: null, height: undefined })).toBeUndefined();\n });\n\n it(\"keeps dimensions even without an edit\", () => {\n expect(assetImageFromLegacyEdit(undefined, { width: 100, height: 50 })).toEqual({\n width: 100,\n height: 50\n });\n });\n});\n\ndescribe(\"normalizeToAsset — legacy Website Builder image value\", () => {\n const legacy = {\n id: \"file-1\",\n name: \"cat.jpg\",\n size: 1234,\n mimeType: \"image/jpeg\",\n src: \"https://cdn/x/cat.jpg\",\n width: 800,\n height: 600,\n edit: {\n crop: { top: 0.1, left: 0.1, bottom: 0.1, right: 0.1 },\n hotspot: { x: 0.25, y: 0.75, width: 1, height: 1 },\n alt: \"A cat\"\n }\n };\n\n it(\"upgrades mimeType -> type and edit -> image\", () => {\n const asset = normalizeToAsset(legacy);\n expect(asset).toEqual({\n id: \"file-1\",\n src: \"https://cdn/x/cat.jpg\",\n url: \"https://cdn/x/cat.jpg\",\n name: \"cat.jpg\",\n type: \"image/jpeg\",\n size: 1234,\n image: {\n width: 800,\n height: 600,\n crop: { top: 0.1, left: 0.1, bottom: 0.1, right: 0.1 },\n focalPoint: { x: 0.25, y: 0.75 },\n alt: \"A cat\"\n }\n });\n });\n\n it(\"handles a legacy value with no edit (dimensions only)\", () => {\n const asset = normalizeToAsset({ ...legacy, edit: undefined });\n expect(asset?.image).toEqual({ width: 800, height: 600 });\n });\n\n it(\"upgrades an original flat WB file value (type + top-level dims, no edit)\", () => {\n // The shape existing released pages store: flat, uses `type`, no sub-object.\n const asset = normalizeToAsset({\n id: \"f\",\n src: \"https://cdn/x.jpg\",\n name: \"x.jpg\",\n type: \"image/jpeg\",\n size: 10,\n width: 800,\n height: 600\n });\n expect(asset).toEqual({\n id: \"f\",\n src: \"https://cdn/x.jpg\",\n url: \"https://cdn/x.jpg\",\n name: \"x.jpg\",\n type: \"image/jpeg\",\n size: 10,\n image: { width: 800, height: 600 }\n });\n });\n\n it(\"does not attach image data for a non-image legacy value\", () => {\n const asset = normalizeToAsset({\n ...legacy,\n mimeType: \"application/pdf\",\n edit: undefined,\n width: undefined,\n height: undefined\n });\n expect(asset?.type).toBe(\"application/pdf\");\n expect(asset?.image).toBeUndefined();\n });\n});\n\ndescribe(\"normalizeToAsset — already-unified asset\", () => {\n it(\"is idempotent for a well-formed image asset\", () => {\n const asset = {\n id: \"f2\",\n src: \"https://cdn/y/pic.png\",\n url: \"https://cdn/y/pic.png\",\n name: \"pic.png\",\n type: \"image/png\",\n size: 9,\n image: {\n width: 400,\n height: 300,\n crop: { top: 0, left: 0.2, bottom: 0, right: 0.2 },\n focalPoint: { x: 0.5, y: 0.5 },\n alt: \"Pic\"\n }\n };\n expect(normalizeToAsset(asset)).toEqual(asset);\n });\n\n it(\"preserves video data\", () => {\n const asset = normalizeToAsset({\n id: \"v1\",\n src: \"https://cdn/v/clip.mp4\",\n name: \"clip.mp4\",\n type: \"video/mp4\",\n size: 42,\n video: { autoplay: true, poster: \"https://cdn/v/poster.jpg\" }\n });\n expect(asset?.video).toEqual({ autoplay: true, poster: \"https://cdn/v/poster.jpg\" });\n });\n\n it(\"returns null for invalid input\", () => {\n expect(normalizeToAsset(null)).toBeNull();\n expect(normalizeToAsset(undefined)).toBeNull();\n expect(normalizeToAsset(\"nope\")).toBeNull();\n expect(normalizeToAsset(42)).toBeNull();\n });\n});\n\ndescribe(\"resolveAssetImage\", () => {\n it(\"prefers override per property, falling back to base\", () => {\n const merged = resolveAssetImage(\n { crop: { top: 0.1, left: 0, bottom: 0, right: 0 } },\n {\n crop: { top: 0.5, left: 0, bottom: 0, right: 0 },\n focalPoint: { x: 0.2, y: 0.2 },\n alt: \"base\"\n }\n );\n expect(merged.crop).toEqual({ top: 0.1, left: 0, bottom: 0, right: 0 });\n expect(merged.focalPoint).toEqual({ x: 0.2, y: 0.2 });\n expect(merged.alt).toBe(\"base\");\n });\n});\n"],"names":["describe","it","expect","getAssetCategory","undefined","image","assetImageFromLegacyEdit","legacy","asset","normalizeToAsset","merged","resolveAssetImage"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export type { Asset, AssetImage, AssetCrop, AssetFocalPoint, AssetDocument, AssetVideo } from "@webiny/sdk";
2
+ export type AssetCategory = "image" | "document" | "video";
package/asset/types.js ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ import type { Component, ComponentManifestInput } from "./types.js";
2
+ export type ExtractInputs<T> = T extends {
3
+ inputs: infer I;
4
+ } ? I : never;
5
+ export declare function createComponent<TComponent extends (props: any) => any, TInputs extends ExtractInputs<Parameters<TComponent>[0]>>(component: TComponent, manifest: ComponentManifestInput<TInputs>): Component;
@@ -0,0 +1,32 @@
1
+ import { createSlotInput } from "./createInput.js";
2
+ function createComponent(component, manifest) {
3
+ const inputs = [];
4
+ if (Array.isArray(manifest.inputs)) inputs.push(...manifest.inputs);
5
+ else {
6
+ const inputsObject = manifest.inputs ?? {};
7
+ Object.keys(inputsObject).forEach((name)=>{
8
+ inputs.push({
9
+ ...inputsObject[name],
10
+ name
11
+ });
12
+ });
13
+ }
14
+ const acceptsChildren = manifest.acceptsChildren;
15
+ if (acceptsChildren) {
16
+ const hasChildren = inputs.some((input)=>"children" === input.name);
17
+ if (!hasChildren) inputs.push(createSlotInput({
18
+ name: "children"
19
+ }));
20
+ }
21
+ return {
22
+ component,
23
+ manifest: {
24
+ ...manifest,
25
+ tags: manifest.tags ?? [],
26
+ inputs
27
+ }
28
+ };
29
+ }
30
+ export { createComponent };
31
+
32
+ //# sourceMappingURL=createComponent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createComponent.js","sources":["../src/createComponent.ts"],"sourcesContent":["import type {\n Component,\n ComponentInput,\n ComponentManifest,\n ComponentManifestInput\n} from \"./types.js\";\nimport type { InputFactory } from \"./createInput.js\";\nimport { createSlotInput } from \"./createInput.js\";\n\nexport type ExtractInputs<T> = T extends { inputs: infer I } ? I : never;\n\nexport function createComponent<\n TComponent extends (props: any) => any,\n TInputs extends ExtractInputs<Parameters<TComponent>[0]>\n>(component: TComponent, manifest: ComponentManifestInput<TInputs>): Component {\n const inputs: ComponentInput[] = [];\n\n if (Array.isArray(manifest.inputs)) {\n inputs.push(...manifest.inputs);\n } else {\n const inputsObject: Record<string, InputFactory<any>> = manifest.inputs ?? {};\n Object.keys(inputsObject).forEach((name: string) => {\n inputs.push({ ...inputsObject[name], name });\n });\n }\n\n const acceptsChildren = manifest.acceptsChildren;\n\n if (acceptsChildren) {\n const hasChildren = inputs.some(input => input.name === \"children\");\n if (!hasChildren) {\n inputs.push(createSlotInput({ name: \"children\" }));\n }\n }\n\n return {\n component,\n manifest: { ...manifest, tags: manifest.tags ?? [], inputs } as ComponentManifest\n };\n}\n"],"names":["createComponent","component","manifest","inputs","Array","inputsObject","Object","name","acceptsChildren","hasChildren","input","createSlotInput"],"mappings":";AAWO,SAASA,gBAGdC,SAAqB,EAAEC,QAAyC;IAC9D,MAAMC,SAA2B,EAAE;IAEnC,IAAIC,MAAM,OAAO,CAACF,SAAS,MAAM,GAC7BC,OAAO,IAAI,IAAID,SAAS,MAAM;SAC3B;QACH,MAAMG,eAAkDH,SAAS,MAAM,IAAI,CAAC;QAC5EI,OAAO,IAAI,CAACD,cAAc,OAAO,CAAC,CAACE;YAC/BJ,OAAO,IAAI,CAAC;gBAAE,GAAGE,YAAY,CAACE,KAAK;gBAAEA;YAAK;QAC9C;IACJ;IAEA,MAAMC,kBAAkBN,SAAS,eAAe;IAEhD,IAAIM,iBAAiB;QACjB,MAAMC,cAAcN,OAAO,IAAI,CAACO,CAAAA,QAASA,AAAe,eAAfA,MAAM,IAAI;QACnD,IAAI,CAACD,aACDN,OAAO,IAAI,CAACQ,gBAAgB;YAAE,MAAM;QAAW;IAEvD;IAEA,OAAO;QACHV;QACA,UAAU;YAAE,GAAGC,QAAQ;YAAE,MAAMA,SAAS,IAAI,IAAI,EAAE;YAAEC;QAAO;IAC/D;AACJ"}
@@ -16,6 +16,8 @@ export declare class ApiClient {
16
16
  private readonly config;
17
17
  constructor(config: ApiClientConfig);
18
18
  fetch({ headers, path, ...params }: WithPath<RequestInit>): Promise<any>;
19
- query({ query, variables }: QueryParams): Promise<any>;
19
+ query({ query, variables }: QueryParams, options?: {
20
+ noStore?: boolean;
21
+ }): Promise<any>;
20
22
  }
21
23
  export {};
@@ -14,8 +14,10 @@ class ApiClient {
14
14
  ...params
15
15
  }).then((res)=>res.json());
16
16
  }
17
- async query({ query, variables }) {
18
- const fetchOptions = {
17
+ async query({ query, variables }, options) {
18
+ const fetchOptions = options?.noStore ? {
19
+ cache: "no-store"
20
+ } : {
19
21
  next: {
20
22
  revalidate: this.config.preview ? 0 : 60
21
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dataProviders/ApiClient.js","sources":["../../src/dataProviders/ApiClient.ts"],"sourcesContent":["interface QueryParams {\n query: string;\n variables: Record<string, any>;\n}\n\nexport interface ApiClientConfig {\n apiHost: string;\n apiKey: string;\n apiTenant: string;\n preview?: boolean;\n}\n\ntype WithPath<T> = T & {\n path?: string;\n next?: Record<string, any>;\n};\n\nexport class ApiClient {\n private readonly config: ApiClientConfig;\n\n constructor(config: ApiClientConfig) {\n this.config = config;\n }\n\n async fetch({ headers, path, ...params }: WithPath<RequestInit>): Promise<any> {\n return fetch(`${this.config.apiHost}${path}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Tenant\": this.config.apiTenant,\n Authorization: \"Bearer \" + this.config.apiKey,\n ...headers\n },\n ...params\n }).then(res => res.json());\n }\n\n async query({ query, variables }: QueryParams) {\n const fetchOptions = {\n next: {\n revalidate: this.config.preview ? 0 : 60\n }\n };\n\n const request: WithPath<RequestInit> = {\n ...fetchOptions,\n path: \"/graphql\",\n method: \"POST\",\n body: JSON.stringify({\n query,\n variables\n })\n };\n\n const json = await this.fetch(request);\n\n if (json.message) {\n throw new Error(json.message);\n }\n\n if (json.errors) {\n console.error(json.errors);\n throw new Error(\"Failed to fetch API\");\n }\n\n return json.data;\n }\n}\n"],"names":["ApiClient","config","headers","path","params","fetch","res","query","variables","fetchOptions","request","JSON","json","Error","console"],"mappings":"AAiBO,MAAMA;IAGT,YAAYC,MAAuB,CAAE;QACjC,IAAI,CAAC,MAAM,GAAGA;IAClB;IAEA,MAAM,MAAM,EAAEC,OAAO,EAAEC,IAAI,EAAE,GAAGC,QAA+B,EAAgB;QAC3E,OAAOC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,GAAGF,MAAM,EAAE;YAC1C,QAAQ;YACR,SAAS;gBACL,gBAAgB;gBAChB,YAAY,IAAI,CAAC,MAAM,CAAC,SAAS;gBACjC,eAAe,YAAY,IAAI,CAAC,MAAM,CAAC,MAAM;gBAC7C,GAAGD,OAAO;YACd;YACA,GAAGE,MAAM;QACb,GAAG,IAAI,CAACE,CAAAA,MAAOA,IAAI,IAAI;IAC3B;IAEA,MAAM,MAAM,EAAEC,KAAK,EAAEC,SAAS,EAAe,EAAE;QAC3C,MAAMC,eAAe;YACjB,MAAM;gBACF,YAAY,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI;YAC1C;QACJ;QAEA,MAAMC,UAAiC;YACnC,GAAGD,YAAY;YACf,MAAM;YACN,QAAQ;YACR,MAAME,KAAK,SAAS,CAAC;gBACjBJ;gBACAC;YACJ;QACJ;QAEA,MAAMI,OAAO,MAAM,IAAI,CAAC,KAAK,CAACF;QAE9B,IAAIE,KAAK,OAAO,EACZ,MAAM,IAAIC,MAAMD,KAAK,OAAO;QAGhC,IAAIA,KAAK,MAAM,EAAE;YACbE,QAAQ,KAAK,CAACF,KAAK,MAAM;YACzB,MAAM,IAAIC,MAAM;QACpB;QAEA,OAAOD,KAAK,IAAI;IACpB;AACJ"}
1
+ {"version":3,"file":"dataProviders/ApiClient.js","sources":["../../src/dataProviders/ApiClient.ts"],"sourcesContent":["interface QueryParams {\n query: string;\n variables: Record<string, any>;\n}\n\nexport interface ApiClientConfig {\n apiHost: string;\n apiKey: string;\n apiTenant: string;\n preview?: boolean;\n}\n\ntype WithPath<T> = T & {\n path?: string;\n next?: Record<string, any>;\n};\n\nexport class ApiClient {\n private readonly config: ApiClientConfig;\n\n constructor(config: ApiClientConfig) {\n this.config = config;\n }\n\n async fetch({ headers, path, ...params }: WithPath<RequestInit>): Promise<any> {\n return fetch(`${this.config.apiHost}${path}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Tenant\": this.config.apiTenant,\n Authorization: \"Bearer \" + this.config.apiKey,\n ...headers\n },\n ...params\n }).then(res => res.json());\n }\n\n async query({ query, variables }: QueryParams, options?: { noStore?: boolean }) {\n // Runtime reads (e.g. the experiment kill-switch) must bypass the cache so they take\n // effect immediately; everything else caches and revalidates on an interval.\n const fetchOptions: Pick<WithPath<RequestInit>, \"cache\" | \"next\"> = options?.noStore\n ? { cache: \"no-store\" }\n : { next: { revalidate: this.config.preview ? 0 : 60 } };\n\n const request: WithPath<RequestInit> = {\n ...fetchOptions,\n path: \"/graphql\",\n method: \"POST\",\n body: JSON.stringify({\n query,\n variables\n })\n };\n\n const json = await this.fetch(request);\n\n if (json.message) {\n throw new Error(json.message);\n }\n\n if (json.errors) {\n console.error(json.errors);\n throw new Error(\"Failed to fetch API\");\n }\n\n return json.data;\n }\n}\n"],"names":["ApiClient","config","headers","path","params","fetch","res","query","variables","options","fetchOptions","request","JSON","json","Error","console"],"mappings":"AAiBO,MAAMA;IAGT,YAAYC,MAAuB,CAAE;QACjC,IAAI,CAAC,MAAM,GAAGA;IAClB;IAEA,MAAM,MAAM,EAAEC,OAAO,EAAEC,IAAI,EAAE,GAAGC,QAA+B,EAAgB;QAC3E,OAAOC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,GAAGF,MAAM,EAAE;YAC1C,QAAQ;YACR,SAAS;gBACL,gBAAgB;gBAChB,YAAY,IAAI,CAAC,MAAM,CAAC,SAAS;gBACjC,eAAe,YAAY,IAAI,CAAC,MAAM,CAAC,MAAM;gBAC7C,GAAGD,OAAO;YACd;YACA,GAAGE,MAAM;QACb,GAAG,IAAI,CAACE,CAAAA,MAAOA,IAAI,IAAI;IAC3B;IAEA,MAAM,MAAM,EAAEC,KAAK,EAAEC,SAAS,EAAe,EAAEC,OAA+B,EAAE;QAG5E,MAAMC,eAA8DD,SAAS,UACvE;YAAE,OAAO;QAAW,IACpB;YAAE,MAAM;gBAAE,YAAY,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI;YAAG;QAAE;QAE3D,MAAME,UAAiC;YACnC,GAAGD,YAAY;YACf,MAAM;YACN,QAAQ;YACR,MAAME,KAAK,SAAS,CAAC;gBACjBL;gBACAC;YACJ;QACJ;QAEA,MAAMK,OAAO,MAAM,IAAI,CAAC,KAAK,CAACF;QAE9B,IAAIE,KAAK,OAAO,EACZ,MAAM,IAAIC,MAAMD,KAAK,OAAO;QAGhC,IAAIA,KAAK,MAAM,EAAE;YACbE,QAAQ,KAAK,CAACF,KAAK,MAAM;YACzB,MAAM,IAAIC,MAAM;QACpB;QAEA,OAAOD,KAAK,IAAI;IACpB;AACJ"}
@@ -1,4 +1,5 @@
1
1
  import type { IDataProvider, ListPagesOptions, ListPagesResult, PublicPage } from "../types.js";
2
+ import type { ActiveExperiment, VariantContent } from "../experiments/types.js";
2
3
  import type { ApiClient } from "../dataProviders/ApiClient.js";
3
4
  interface DefaultDataProviderConfig {
4
5
  apiClient: ApiClient;
@@ -9,6 +10,9 @@ export declare class DefaultDataProvider implements IDataProvider {
9
10
  getPageByPath(path: string): Promise<PublicPage | null>;
10
11
  getPageById(id: string): Promise<PublicPage | null>;
11
12
  listPages(options?: ListPagesOptions): Promise<ListPagesResult>;
13
+ getPageExperiment(path: string): Promise<ActiveExperiment | null>;
14
+ getVariantContent(variantId: string): Promise<VariantContent | null>;
15
+ getExperimentPaused(experimentId: string): Promise<boolean>;
12
16
  private checkForErrors;
13
17
  }
14
18
  export {};
@@ -1,6 +1,9 @@
1
1
  import { GET_PAGE_BY_PATH } from "./GET_PAGE_BY_PATH.js";
2
2
  import { GET_PAGE_BY_ID } from "./GET_PAGE_BY_ID.js";
3
3
  import { LIST_PUBLISHED_PAGES } from "./LIST_PUBLISHED_PAGES.js";
4
+ import { GET_PAGE_EXPERIMENT } from "./GET_PAGE_EXPERIMENT.js";
5
+ import { GET_VARIANT_CONTENT } from "./GET_VARIANT_CONTENT.js";
6
+ import { GET_EXPERIMENT_PAUSED } from "./GET_EXPERIMENT_PAUSED.js";
4
7
  const ignoreActions = [
5
8
  ".well-known",
6
9
  "_next"
@@ -51,6 +54,38 @@ class DefaultDataProvider {
51
54
  }
52
55
  };
53
56
  }
57
+ async getPageExperiment(path) {
58
+ const result = await this.config.apiClient.query({
59
+ query: GET_PAGE_EXPERIMENT,
60
+ variables: {
61
+ path
62
+ }
63
+ });
64
+ this.checkForErrors(`getPageExperiment:${path}`, result.websiteBuilder.getPageExperiment);
65
+ return result.websiteBuilder.getPageExperiment.data ?? null;
66
+ }
67
+ async getVariantContent(variantId) {
68
+ const result = await this.config.apiClient.query({
69
+ query: GET_VARIANT_CONTENT,
70
+ variables: {
71
+ id: variantId
72
+ }
73
+ });
74
+ this.checkForErrors(`getVariantContent:${variantId}`, result.websiteBuilder.getVariantContent);
75
+ return result.websiteBuilder.getVariantContent.data ?? null;
76
+ }
77
+ async getExperimentPaused(experimentId) {
78
+ const result = await this.config.apiClient.query({
79
+ query: GET_EXPERIMENT_PAUSED,
80
+ variables: {
81
+ experimentId
82
+ }
83
+ }, {
84
+ noStore: true
85
+ });
86
+ this.checkForErrors(`getExperimentPaused:${experimentId}`, result.websiteBuilder.getExperimentPaused);
87
+ return true === result.websiteBuilder.getExperimentPaused.data;
88
+ }
54
89
  checkForErrors(action, data) {
55
90
  if (data.error) {
56
91
  if (ignoreActions.some((item)=>action.includes(item))) return;
@@ -1 +1 @@
1
- {"version":3,"file":"dataProviders/DefaultDataProvider.js","sources":["../../src/dataProviders/DefaultDataProvider.ts"],"sourcesContent":["import type { IDataProvider, ListPagesOptions, ListPagesResult, PublicPage } from \"~/types.js\";\nimport type { ApiClient } from \"~/dataProviders/ApiClient.js\";\nimport { GET_PAGE_BY_PATH } from \"./GET_PAGE_BY_PATH.js\";\nimport { GET_PAGE_BY_ID } from \"./GET_PAGE_BY_ID.js\";\nimport { LIST_PUBLISHED_PAGES } from \"./LIST_PUBLISHED_PAGES.js\";\n\ninterface DefaultDataProviderConfig {\n apiClient: ApiClient;\n}\n\nconst ignoreActions = [\".well-known\", \"_next\"];\n\nexport class DefaultDataProvider implements IDataProvider {\n private config: DefaultDataProviderConfig;\n\n constructor(config: DefaultDataProviderConfig) {\n this.config = config;\n }\n\n public async getPageByPath(path: string): Promise<PublicPage | null> {\n const result = await this.config.apiClient.query({\n query: GET_PAGE_BY_PATH,\n variables: {\n path\n }\n });\n\n this.checkForErrors(`getPageByPath:${path}`, result.websiteBuilder.getPageByPath);\n\n return result.websiteBuilder.getPageByPath.data;\n }\n\n public async getPageById(id: string): Promise<PublicPage | null> {\n const result = await this.config.apiClient.query({\n query: GET_PAGE_BY_ID,\n variables: {\n id\n }\n });\n\n this.checkForErrors(\"getPageById\", result.websiteBuilder.getPageById);\n\n return result.websiteBuilder.getPageById.data;\n }\n\n public async listPages(options?: ListPagesOptions): Promise<ListPagesResult> {\n const { where, ...rest } = options ?? {};\n\n const result = await this.config.apiClient.query({\n query: LIST_PUBLISHED_PAGES,\n variables: {\n where: {\n ...where,\n published: true\n },\n ...rest\n }\n });\n\n this.checkForErrors(\"listPages\", result.websiteBuilder.listPages);\n\n return {\n data: result.websiteBuilder.listPages.data ?? [],\n meta: result.websiteBuilder.listPages.meta ?? {\n hasMoreItems: false,\n totalCount: 0,\n cursor: null\n }\n };\n }\n\n private checkForErrors(action: string, data: any) {\n if (data.error) {\n // TODO: investigate how these ignored actions make their way to the SDK.\n if (ignoreActions.some(item => action.includes(item))) {\n return;\n }\n console.error(`Could not execute \"${action}\". Reason: ${data.error.message}`);\n }\n }\n}\n"],"names":["ignoreActions","DefaultDataProvider","config","path","result","GET_PAGE_BY_PATH","id","GET_PAGE_BY_ID","options","where","rest","LIST_PUBLISHED_PAGES","action","data","item","console"],"mappings":";;;AAUA,MAAMA,gBAAgB;IAAC;IAAe;CAAQ;AAEvC,MAAMC;IAGT,YAAYC,MAAiC,CAAE;QAC3C,IAAI,CAAC,MAAM,GAAGA;IAClB;IAEA,MAAa,cAAcC,IAAY,EAA8B;QACjE,MAAMC,SAAS,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;YAC7C,OAAOC;YACP,WAAW;gBACPF;YACJ;QACJ;QAEA,IAAI,CAAC,cAAc,CAAC,CAAC,cAAc,EAAEA,MAAM,EAAEC,OAAO,cAAc,CAAC,aAAa;QAEhF,OAAOA,OAAO,cAAc,CAAC,aAAa,CAAC,IAAI;IACnD;IAEA,MAAa,YAAYE,EAAU,EAA8B;QAC7D,MAAMF,SAAS,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;YAC7C,OAAOG;YACP,WAAW;gBACPD;YACJ;QACJ;QAEA,IAAI,CAAC,cAAc,CAAC,eAAeF,OAAO,cAAc,CAAC,WAAW;QAEpE,OAAOA,OAAO,cAAc,CAAC,WAAW,CAAC,IAAI;IACjD;IAEA,MAAa,UAAUI,OAA0B,EAA4B;QACzE,MAAM,EAAEC,KAAK,EAAE,GAAGC,MAAM,GAAGF,WAAW,CAAC;QAEvC,MAAMJ,SAAS,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;YAC7C,OAAOO;YACP,WAAW;gBACP,OAAO;oBACH,GAAGF,KAAK;oBACR,WAAW;gBACf;gBACA,GAAGC,IAAI;YACX;QACJ;QAEA,IAAI,CAAC,cAAc,CAAC,aAAaN,OAAO,cAAc,CAAC,SAAS;QAEhE,OAAO;YACH,MAAMA,OAAO,cAAc,CAAC,SAAS,CAAC,IAAI,IAAI,EAAE;YAChD,MAAMA,OAAO,cAAc,CAAC,SAAS,CAAC,IAAI,IAAI;gBAC1C,cAAc;gBACd,YAAY;gBACZ,QAAQ;YACZ;QACJ;IACJ;IAEQ,eAAeQ,MAAc,EAAEC,IAAS,EAAE;QAC9C,IAAIA,KAAK,KAAK,EAAE;YAEZ,IAAIb,cAAc,IAAI,CAACc,CAAAA,OAAQF,OAAO,QAAQ,CAACE,QAC3C;YAEJC,QAAQ,KAAK,CAAC,CAAC,mBAAmB,EAAEH,OAAO,WAAW,EAAEC,KAAK,KAAK,CAAC,OAAO,EAAE;QAChF;IACJ;AACJ"}
1
+ {"version":3,"file":"dataProviders/DefaultDataProvider.js","sources":["../../src/dataProviders/DefaultDataProvider.ts"],"sourcesContent":["import type { IDataProvider, ListPagesOptions, ListPagesResult, PublicPage } from \"~/types.js\";\nimport type { ActiveExperiment, VariantContent } from \"~/experiments/types.js\";\nimport type { ApiClient } from \"~/dataProviders/ApiClient.js\";\nimport { GET_PAGE_BY_PATH } from \"./GET_PAGE_BY_PATH.js\";\nimport { GET_PAGE_BY_ID } from \"./GET_PAGE_BY_ID.js\";\nimport { LIST_PUBLISHED_PAGES } from \"./LIST_PUBLISHED_PAGES.js\";\nimport { GET_PAGE_EXPERIMENT } from \"./GET_PAGE_EXPERIMENT.js\";\nimport { GET_VARIANT_CONTENT } from \"./GET_VARIANT_CONTENT.js\";\nimport { GET_EXPERIMENT_PAUSED } from \"./GET_EXPERIMENT_PAUSED.js\";\n\ninterface DefaultDataProviderConfig {\n apiClient: ApiClient;\n}\n\nconst ignoreActions = [\".well-known\", \"_next\"];\n\nexport class DefaultDataProvider implements IDataProvider {\n private config: DefaultDataProviderConfig;\n\n constructor(config: DefaultDataProviderConfig) {\n this.config = config;\n }\n\n public async getPageByPath(path: string): Promise<PublicPage | null> {\n const result = await this.config.apiClient.query({\n query: GET_PAGE_BY_PATH,\n variables: {\n path\n }\n });\n\n this.checkForErrors(`getPageByPath:${path}`, result.websiteBuilder.getPageByPath);\n\n return result.websiteBuilder.getPageByPath.data;\n }\n\n public async getPageById(id: string): Promise<PublicPage | null> {\n const result = await this.config.apiClient.query({\n query: GET_PAGE_BY_ID,\n variables: {\n id\n }\n });\n\n this.checkForErrors(\"getPageById\", result.websiteBuilder.getPageById);\n\n return result.websiteBuilder.getPageById.data;\n }\n\n public async listPages(options?: ListPagesOptions): Promise<ListPagesResult> {\n const { where, ...rest } = options ?? {};\n\n const result = await this.config.apiClient.query({\n query: LIST_PUBLISHED_PAGES,\n variables: {\n where: {\n ...where,\n published: true\n },\n ...rest\n }\n });\n\n this.checkForErrors(\"listPages\", result.websiteBuilder.listPages);\n\n return {\n data: result.websiteBuilder.listPages.data ?? [],\n meta: result.websiteBuilder.listPages.meta ?? {\n hasMoreItems: false,\n totalCount: 0,\n cursor: null\n }\n };\n }\n\n public async getPageExperiment(path: string): Promise<ActiveExperiment | null> {\n const result = await this.config.apiClient.query({\n query: GET_PAGE_EXPERIMENT,\n variables: {\n path\n }\n });\n\n this.checkForErrors(`getPageExperiment:${path}`, result.websiteBuilder.getPageExperiment);\n\n return result.websiteBuilder.getPageExperiment.data ?? null;\n }\n\n public async getVariantContent(variantId: string): Promise<VariantContent | null> {\n const result = await this.config.apiClient.query({\n query: GET_VARIANT_CONTENT,\n variables: {\n id: variantId\n }\n });\n\n this.checkForErrors(\n `getVariantContent:${variantId}`,\n result.websiteBuilder.getVariantContent\n );\n\n return result.websiteBuilder.getVariantContent.data ?? null;\n }\n\n public async getExperimentPaused(experimentId: string): Promise<boolean> {\n // Uncached: the kill-switch must take effect immediately, even while page/variant content\n // stays cached.\n const result = await this.config.apiClient.query(\n {\n query: GET_EXPERIMENT_PAUSED,\n variables: { experimentId }\n },\n { noStore: true }\n );\n\n this.checkForErrors(\n `getExperimentPaused:${experimentId}`,\n result.websiteBuilder.getExperimentPaused\n );\n\n return result.websiteBuilder.getExperimentPaused.data === true;\n }\n\n private checkForErrors(action: string, data: any) {\n if (data.error) {\n // TODO: investigate how these ignored actions make their way to the SDK.\n if (ignoreActions.some(item => action.includes(item))) {\n return;\n }\n console.error(`Could not execute \"${action}\". Reason: ${data.error.message}`);\n }\n }\n}\n"],"names":["ignoreActions","DefaultDataProvider","config","path","result","GET_PAGE_BY_PATH","id","GET_PAGE_BY_ID","options","where","rest","LIST_PUBLISHED_PAGES","GET_PAGE_EXPERIMENT","variantId","GET_VARIANT_CONTENT","experimentId","GET_EXPERIMENT_PAUSED","action","data","item","console"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const GET_EXPERIMENT_PAUSED = "\n query GetExperimentPaused($experimentId: String!) {\n websiteBuilder {\n getExperimentPaused(experimentId: $experimentId) {\n data\n error {\n code\n message\n data\n }\n }\n }\n }\n";
@@ -0,0 +1,17 @@
1
+ const GET_EXPERIMENT_PAUSED = `
2
+ query GetExperimentPaused($experimentId: String!) {
3
+ websiteBuilder {
4
+ getExperimentPaused(experimentId: $experimentId) {
5
+ data
6
+ error {
7
+ code
8
+ message
9
+ data
10
+ }
11
+ }
12
+ }
13
+ }
14
+ `;
15
+ export { GET_EXPERIMENT_PAUSED };
16
+
17
+ //# sourceMappingURL=GET_EXPERIMENT_PAUSED.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataProviders/GET_EXPERIMENT_PAUSED.js","sources":["../../src/dataProviders/GET_EXPERIMENT_PAUSED.ts"],"sourcesContent":["export const GET_EXPERIMENT_PAUSED = /* GraphQL*/ `\n query GetExperimentPaused($experimentId: String!) {\n websiteBuilder {\n getExperimentPaused(experimentId: $experimentId) {\n data\n error {\n code\n message\n data\n }\n }\n }\n }\n`;\n"],"names":["GET_EXPERIMENT_PAUSED"],"mappings":"AAAO,MAAMA,wBAAqC,CAAC;;;;;;;;;;;;;AAanD,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const GET_PAGE_EXPERIMENT = "\n query GetPageExperiment($path: String!) {\n websiteBuilder {\n getPageExperiment(path: $path) {\n data {\n experimentId\n revisionId\n pageEntryId\n path\n status\n controlVariantId\n tenantId\n trafficSplit\n targeting\n analytics\n variants {\n variantId\n name\n }\n }\n error {\n code\n message\n data\n }\n }\n }\n }\n";
@@ -0,0 +1,32 @@
1
+ const GET_PAGE_EXPERIMENT = `
2
+ query GetPageExperiment($path: String!) {
3
+ websiteBuilder {
4
+ getPageExperiment(path: $path) {
5
+ data {
6
+ experimentId
7
+ revisionId
8
+ pageEntryId
9
+ path
10
+ status
11
+ controlVariantId
12
+ tenantId
13
+ trafficSplit
14
+ targeting
15
+ analytics
16
+ variants {
17
+ variantId
18
+ name
19
+ }
20
+ }
21
+ error {
22
+ code
23
+ message
24
+ data
25
+ }
26
+ }
27
+ }
28
+ }
29
+ `;
30
+ export { GET_PAGE_EXPERIMENT };
31
+
32
+ //# sourceMappingURL=GET_PAGE_EXPERIMENT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataProviders/GET_PAGE_EXPERIMENT.js","sources":["../../src/dataProviders/GET_PAGE_EXPERIMENT.ts"],"sourcesContent":["export const GET_PAGE_EXPERIMENT = /* GraphQL*/ `\n query GetPageExperiment($path: String!) {\n websiteBuilder {\n getPageExperiment(path: $path) {\n data {\n experimentId\n revisionId\n pageEntryId\n path\n status\n controlVariantId\n tenantId\n trafficSplit\n targeting\n analytics\n variants {\n variantId\n name\n }\n }\n error {\n code\n message\n data\n }\n }\n }\n }\n`;\n"],"names":["GET_PAGE_EXPERIMENT"],"mappings":"AAAO,MAAMA,sBAAmC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BjD,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const GET_VARIANT_CONTENT = "\n query GetVariantContent($id: ID!) {\n websiteBuilder {\n getVariantContent(id: $id) {\n data {\n id\n properties\n elements\n bindings\n extensions\n metadata\n }\n error {\n code\n message\n data\n }\n }\n }\n }\n";
@@ -0,0 +1,24 @@
1
+ const GET_VARIANT_CONTENT = `
2
+ query GetVariantContent($id: ID!) {
3
+ websiteBuilder {
4
+ getVariantContent(id: $id) {
5
+ data {
6
+ id
7
+ properties
8
+ elements
9
+ bindings
10
+ extensions
11
+ metadata
12
+ }
13
+ error {
14
+ code
15
+ message
16
+ data
17
+ }
18
+ }
19
+ }
20
+ }
21
+ `;
22
+ export { GET_VARIANT_CONTENT };
23
+
24
+ //# sourceMappingURL=GET_VARIANT_CONTENT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataProviders/GET_VARIANT_CONTENT.js","sources":["../../src/dataProviders/GET_VARIANT_CONTENT.ts"],"sourcesContent":["export const GET_VARIANT_CONTENT = /* GraphQL*/ `\n query GetVariantContent($id: ID!) {\n websiteBuilder {\n getVariantContent(id: $id) {\n data {\n id\n properties\n elements\n bindings\n extensions\n metadata\n }\n error {\n code\n message\n data\n }\n }\n }\n }\n`;\n"],"names":["GET_VARIANT_CONTENT"],"mappings":"AAAO,MAAMA,sBAAmC,CAAC;;;;;;;;;;;;;;;;;;;;AAoBjD,CAAC"}
@@ -1,8 +1,12 @@
1
1
  import type { IDataProvider, PublicRedirect } from "../types.js";
2
2
  import type { PublicPage } from "../types.js";
3
+ import type { ActiveExperiment, VariantContent } from "../experiments/types.js";
3
4
  export declare class NullDataProvider implements IDataProvider {
4
5
  getPageById(): Promise<PublicPage | null>;
5
6
  getPageByPath(): Promise<PublicPage | null>;
7
+ getPageExperiment(): Promise<ActiveExperiment | null>;
8
+ getVariantContent(): Promise<VariantContent | null>;
9
+ getExperimentPaused(): Promise<boolean>;
6
10
  listPages(): Promise<{
7
11
  data: never[];
8
12
  meta: {
@@ -5,6 +5,15 @@ class NullDataProvider {
5
5
  getPageByPath() {
6
6
  return Promise.resolve(null);
7
7
  }
8
+ getPageExperiment() {
9
+ return Promise.resolve(null);
10
+ }
11
+ getVariantContent() {
12
+ return Promise.resolve(null);
13
+ }
14
+ getExperimentPaused() {
15
+ return Promise.resolve(false);
16
+ }
8
17
  async listPages() {
9
18
  return {
10
19
  data: [],
@@ -1 +1 @@
1
- {"version":3,"file":"dataProviders/NullDataProvider.js","sources":["../../src/dataProviders/NullDataProvider.ts"],"sourcesContent":["import type { IDataProvider, PublicRedirect } from \"~/types.js\";\nimport type { PublicPage } from \"~/types.js\";\n\nexport class NullDataProvider implements IDataProvider {\n getPageById(): Promise<PublicPage | null> {\n return Promise.resolve(null);\n }\n\n getPageByPath(): Promise<PublicPage | null> {\n return Promise.resolve(null);\n }\n\n public async listPages() {\n return { data: [], meta: { hasMoreItems: false, totalCount: 0, cursor: null } };\n }\n\n listRedirects(): Promise<PublicRedirect[]> {\n return Promise.resolve([]);\n }\n}\n"],"names":["NullDataProvider","Promise"],"mappings":"AAGO,MAAMA;IACT,cAA0C;QACtC,OAAOC,QAAQ,OAAO,CAAC;IAC3B;IAEA,gBAA4C;QACxC,OAAOA,QAAQ,OAAO,CAAC;IAC3B;IAEA,MAAa,YAAY;QACrB,OAAO;YAAE,MAAM,EAAE;YAAE,MAAM;gBAAE,cAAc;gBAAO,YAAY;gBAAG,QAAQ;YAAK;QAAE;IAClF;IAEA,gBAA2C;QACvC,OAAOA,QAAQ,OAAO,CAAC,EAAE;IAC7B;AACJ"}
1
+ {"version":3,"file":"dataProviders/NullDataProvider.js","sources":["../../src/dataProviders/NullDataProvider.ts"],"sourcesContent":["import type { IDataProvider, PublicRedirect } from \"~/types.js\";\nimport type { PublicPage } from \"~/types.js\";\nimport type { ActiveExperiment, VariantContent } from \"~/experiments/types.js\";\n\nexport class NullDataProvider implements IDataProvider {\n getPageById(): Promise<PublicPage | null> {\n return Promise.resolve(null);\n }\n\n getPageByPath(): Promise<PublicPage | null> {\n return Promise.resolve(null);\n }\n\n getPageExperiment(): Promise<ActiveExperiment | null> {\n return Promise.resolve(null);\n }\n\n getVariantContent(): Promise<VariantContent | null> {\n return Promise.resolve(null);\n }\n\n getExperimentPaused(): Promise<boolean> {\n return Promise.resolve(false);\n }\n\n public async listPages() {\n return { data: [], meta: { hasMoreItems: false, totalCount: 0, cursor: null } };\n }\n\n listRedirects(): Promise<PublicRedirect[]> {\n return Promise.resolve([]);\n }\n}\n"],"names":["NullDataProvider","Promise"],"mappings":"AAIO,MAAMA;IACT,cAA0C;QACtC,OAAOC,QAAQ,OAAO,CAAC;IAC3B;IAEA,gBAA4C;QACxC,OAAOA,QAAQ,OAAO,CAAC;IAC3B;IAEA,oBAAsD;QAClD,OAAOA,QAAQ,OAAO,CAAC;IAC3B;IAEA,oBAAoD;QAChD,OAAOA,QAAQ,OAAO,CAAC;IAC3B;IAEA,sBAAwC;QACpC,OAAOA,QAAQ,OAAO,CAAC;IAC3B;IAEA,MAAa,YAAY;QACrB,OAAO;YAAE,MAAM,EAAE;YAAE,MAAM;gBAAE,cAAc;gBAAO,YAAY;gBAAG,QAAQ;YAAK;QAAE;IAClF;IAEA,gBAA2C;QACvC,OAAOA,QAAQ,OAAO,CAAC,EAAE;IAC7B;AACJ"}
@@ -0,0 +1,31 @@
1
+ import type { AnalyticsProvider, ExposureEvent } from "./types.js";
2
+ /**
3
+ * The minimal shape of a PostHog client (server-side `posthog-node` or browser `posthog-js`).
4
+ * The project supplies its own client so this package stays dependency-free and framework
5
+ * agnostic — no PostHog package is bundled into the SDK.
6
+ */
7
+ export interface PostHogClientLike {
8
+ capture(params: {
9
+ distinctId: string;
10
+ event: string;
11
+ properties?: Record<string, any>;
12
+ }): void | Promise<void>;
13
+ }
14
+ export interface PostHogAnalyticsProviderOptions {
15
+ /** Event name to emit. Defaults to "$experiment_exposure". */
16
+ exposureEventName?: string;
17
+ }
18
+ /**
19
+ * Maps the canonical {@link ExposureEvent} onto a PostHog capture call. PostHog attributes
20
+ * experiment results from the `$feature/<experimentKey>` property carried on the exposure
21
+ * event (and, project-side, on subsequent metric events) — no native PostHog feature flag is
22
+ * required. This is the ONLY place PostHog-specific field names exist; they never reach the
23
+ * data model, the assignment logic, or the render path.
24
+ */
25
+ export declare class PostHogAnalyticsProvider implements AnalyticsProvider {
26
+ private client;
27
+ private options;
28
+ readonly name = "posthog";
29
+ constructor(client: PostHogClientLike, options?: PostHogAnalyticsProviderOptions);
30
+ emitExposure(event: ExposureEvent): Promise<void>;
31
+ }
@@ -0,0 +1,28 @@
1
+ class PostHogAnalyticsProvider {
2
+ constructor(client, options = {}){
3
+ this.client = client;
4
+ this.options = options;
5
+ this.name = "posthog";
6
+ }
7
+ async emitExposure(event) {
8
+ const featureKey = event.experimentKey ?? event.experimentId;
9
+ await this.client.capture({
10
+ distinctId: event.visitorId,
11
+ event: this.options.exposureEventName ?? "$experiment_exposure",
12
+ properties: {
13
+ [`$feature/${featureKey}`]: event.variantId,
14
+ experiment_id: event.experimentId,
15
+ variant_id: event.variantId,
16
+ revision_id: event.revisionId,
17
+ tenant_id: event.tenantId,
18
+ page_id: event.pageId,
19
+ path: event.path,
20
+ $current_url: event.path,
21
+ timestamp: event.timestamp
22
+ }
23
+ });
24
+ }
25
+ }
26
+ export { PostHogAnalyticsProvider };
27
+
28
+ //# sourceMappingURL=PostHogAnalyticsProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments/PostHogAnalyticsProvider.js","sources":["../../src/experiments/PostHogAnalyticsProvider.ts"],"sourcesContent":["import type { AnalyticsProvider, ExposureEvent } from \"./types.js\";\n\n/**\n * The minimal shape of a PostHog client (server-side `posthog-node` or browser `posthog-js`).\n * The project supplies its own client so this package stays dependency-free and framework\n * agnostic — no PostHog package is bundled into the SDK.\n */\nexport interface PostHogClientLike {\n capture(params: {\n distinctId: string;\n event: string;\n properties?: Record<string, any>;\n }): void | Promise<void>;\n}\n\nexport interface PostHogAnalyticsProviderOptions {\n /** Event name to emit. Defaults to \"$experiment_exposure\". */\n exposureEventName?: string;\n}\n\n/**\n * Maps the canonical {@link ExposureEvent} onto a PostHog capture call. PostHog attributes\n * experiment results from the `$feature/<experimentKey>` property carried on the exposure\n * event (and, project-side, on subsequent metric events) — no native PostHog feature flag is\n * required. This is the ONLY place PostHog-specific field names exist; they never reach the\n * data model, the assignment logic, or the render path.\n */\nexport class PostHogAnalyticsProvider implements AnalyticsProvider {\n public readonly name = \"posthog\";\n\n constructor(\n private client: PostHogClientLike,\n private options: PostHogAnalyticsProviderOptions = {}\n ) {}\n\n async emitExposure(event: ExposureEvent): Promise<void> {\n const featureKey = event.experimentKey ?? event.experimentId;\n\n await this.client.capture({\n distinctId: event.visitorId,\n event: this.options.exposureEventName ?? \"$experiment_exposure\",\n properties: {\n // PostHog reads this property to attribute exposures and metrics to a variant.\n [`$feature/${featureKey}`]: event.variantId,\n experiment_id: event.experimentId,\n variant_id: event.variantId,\n revision_id: event.revisionId,\n tenant_id: event.tenantId,\n page_id: event.pageId,\n path: event.path,\n $current_url: event.path,\n timestamp: event.timestamp\n }\n });\n }\n}\n"],"names":["PostHogAnalyticsProvider","client","options","event","featureKey"],"mappings":"AA2BO,MAAMA;IAGT,YACYC,MAAyB,EACzBC,UAA2C,CAAC,CAAC,CACvD;aAFUD,MAAM,GAANA;aACAC,OAAO,GAAPA;aAJI,IAAI,GAAG;IAKpB;IAEH,MAAM,aAAaC,KAAoB,EAAiB;QACpD,MAAMC,aAAaD,MAAM,aAAa,IAAIA,MAAM,YAAY;QAE5D,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;YACtB,YAAYA,MAAM,SAAS;YAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,IAAI;YACzC,YAAY;gBAER,CAAC,CAAC,SAAS,EAAEC,YAAY,CAAC,EAAED,MAAM,SAAS;gBAC3C,eAAeA,MAAM,YAAY;gBACjC,YAAYA,MAAM,SAAS;gBAC3B,aAAaA,MAAM,UAAU;gBAC7B,WAAWA,MAAM,QAAQ;gBACzB,SAASA,MAAM,MAAM;gBACrB,MAAMA,MAAM,IAAI;gBAChB,cAAcA,MAAM,IAAI;gBACxB,WAAWA,MAAM,SAAS;YAC9B;QACJ;IACJ;AACJ"}
@@ -0,0 +1,4 @@
1
+ import type { AnalyticsProvider } from "./types.js";
2
+ export declare const registerAnalyticsProvider: (provider: AnalyticsProvider) => void;
3
+ export declare const getAnalyticsProvider: (name: string) => AnalyticsProvider | undefined;
4
+ export declare const clearAnalyticsProviders: () => void;
@@ -0,0 +1,11 @@
1
+ const providers = new Map();
2
+ const registerAnalyticsProvider = (provider)=>{
3
+ providers.set(provider.name, provider);
4
+ };
5
+ const getAnalyticsProvider = (name)=>providers.get(name);
6
+ const clearAnalyticsProviders = ()=>{
7
+ providers.clear();
8
+ };
9
+ export { clearAnalyticsProviders, getAnalyticsProvider, registerAnalyticsProvider };
10
+
11
+ //# sourceMappingURL=analyticsProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments/analyticsProvider.js","sources":["../../src/experiments/analyticsProvider.ts"],"sourcesContent":["import type { AnalyticsProvider } from \"./types.js\";\n\n/**\n * A small registry mapping a provider id (e.g. \"posthog\") to its adapter. The render path\n * looks the provider up by the experiment's analytics.provider value, so adding a GA4 adapter\n * later requires no change to assignment or the render path.\n */\nconst providers = new Map<string, AnalyticsProvider>();\n\nexport const registerAnalyticsProvider = (provider: AnalyticsProvider): void => {\n providers.set(provider.name, provider);\n};\n\nexport const getAnalyticsProvider = (name: string): AnalyticsProvider | undefined => {\n return providers.get(name);\n};\n\nexport const clearAnalyticsProviders = (): void => {\n providers.clear();\n};\n"],"names":["providers","Map","registerAnalyticsProvider","provider","getAnalyticsProvider","name","clearAnalyticsProviders"],"mappings":"AAOA,MAAMA,YAAY,IAAIC;AAEf,MAAMC,4BAA4B,CAACC;IACtCH,UAAU,GAAG,CAACG,SAAS,IAAI,EAAEA;AACjC;AAEO,MAAMC,uBAAuB,CAACC,OAC1BL,UAAU,GAAG,CAACK;AAGlB,MAAMC,0BAA0B;IACnCN,UAAU,KAAK;AACnB"}
@@ -0,0 +1,27 @@
1
+ import type { ActiveExperiment, ExperimentTargeting, VariantAssignment, VisitorContext } from "./types.js";
2
+ /**
3
+ * cyrb53 — a fast, well-distributed non-cryptographic string hash. Deterministic across
4
+ * runtimes, which is what makes server-side bucketing stable for a given visitor + experiment.
5
+ */
6
+ export declare const cyrb53: (str: string, seed?: number) => number;
7
+ /** Hash a seed to a stable number in the half-open interval [0, 1). */
8
+ export declare const hashToUnit: (seed: string) => number;
9
+ /**
10
+ * Whether a visitor matches the experiment's geo/device targeting. Tenant scoping is implicit:
11
+ * the experiment id is unique within a tenant, so a visitor's bucket never crosses tenants.
12
+ */
13
+ export declare const matchesTargeting: (targeting: ExperimentTargeting, ctx: VisitorContext) => boolean;
14
+ /**
15
+ * Deterministically assign a visitor to the control bucket or a variant.
16
+ *
17
+ * 1. If geo/device targeting excludes the visitor → control, excluded (no exposure).
18
+ * 2. If the visitor falls outside the traffic percentage → control, excluded (no exposure).
19
+ * 3. Otherwise bucket among the control + variants by their weights, seeded by
20
+ * visitorId + experimentId so the result is stable and tenant-isolated.
21
+ */
22
+ export declare const assignVariant: (experiment: ActiveExperiment, ctx: VisitorContext) => VariantAssignment;
23
+ /**
24
+ * A forced assignment for QA/review. Overrides bucketing and is never counted in results.
25
+ * Returns null when the forced id does not match the control or any ready variant.
26
+ */
27
+ export declare const forcedAssignment: (experiment: ActiveExperiment, forcedVariantId: string) => VariantAssignment | null;
@@ -0,0 +1,91 @@
1
+ const cyrb53 = (str, seed = 0)=>{
2
+ let h1 = 0xdeadbeef ^ seed;
3
+ let h2 = 0x41c6ce57 ^ seed;
4
+ for(let i = 0; i < str.length; i++){
5
+ const ch = str.charCodeAt(i);
6
+ h1 = Math.imul(h1 ^ ch, 2654435761);
7
+ h2 = Math.imul(h2 ^ ch, 1597334677);
8
+ }
9
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
10
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
11
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
12
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
13
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
14
+ };
15
+ const hashToUnit = (seed)=>cyrb53(seed) / 9007199254740992;
16
+ const matchesTargeting = (targeting, ctx)=>{
17
+ if (targeting.geo && targeting.geo.length > 0) {
18
+ if (!ctx.country || !targeting.geo.includes(ctx.country)) return false;
19
+ }
20
+ if (targeting.device && targeting.device.length > 0) {
21
+ if (!ctx.device || !targeting.device.includes(ctx.device)) return false;
22
+ }
23
+ return true;
24
+ };
25
+ const assignVariant = (experiment, ctx)=>{
26
+ const controlVariantId = experiment.controlVariantId;
27
+ const control = {
28
+ variantId: controlVariantId,
29
+ isControl: true,
30
+ excluded: true,
31
+ forced: false
32
+ };
33
+ if (!matchesTargeting(experiment.targeting, ctx)) return control;
34
+ const trafficPercentage = experiment.targeting.trafficPercentage ?? 100;
35
+ const inclusion = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}:inclusion`);
36
+ if (inclusion >= trafficPercentage / 100) return control;
37
+ const readyVariantIds = new Set(experiment.variants.map((variant)=>variant.variantId));
38
+ const buckets = [
39
+ {
40
+ id: controlVariantId,
41
+ weight: Math.max(0, experiment.trafficSplit.control)
42
+ }
43
+ ];
44
+ for (const [variantId, weight] of Object.entries(experiment.trafficSplit.variants))if (readyVariantIds.has(variantId) && weight > 0) buckets.push({
45
+ id: variantId,
46
+ weight
47
+ });
48
+ const totalWeight = buckets.reduce((sum, bucket)=>sum + bucket.weight, 0);
49
+ if (totalWeight <= 0) return {
50
+ variantId: controlVariantId,
51
+ isControl: true,
52
+ excluded: false,
53
+ forced: false
54
+ };
55
+ let roll = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}`) * totalWeight;
56
+ for (const bucket of buckets){
57
+ roll -= bucket.weight;
58
+ if (roll < 0) return {
59
+ variantId: bucket.id,
60
+ isControl: bucket.id === controlVariantId,
61
+ excluded: false,
62
+ forced: false
63
+ };
64
+ }
65
+ const last = buckets[buckets.length - 1];
66
+ return {
67
+ variantId: last.id,
68
+ isControl: last.id === controlVariantId,
69
+ excluded: false,
70
+ forced: false
71
+ };
72
+ };
73
+ const forcedAssignment = (experiment, forcedVariantId)=>{
74
+ if (forcedVariantId === experiment.controlVariantId) return {
75
+ variantId: forcedVariantId,
76
+ isControl: true,
77
+ excluded: false,
78
+ forced: true
79
+ };
80
+ const isReady = experiment.variants.some((variant)=>variant.variantId === forcedVariantId);
81
+ if (!isReady) return null;
82
+ return {
83
+ variantId: forcedVariantId,
84
+ isControl: false,
85
+ excluded: false,
86
+ forced: true
87
+ };
88
+ };
89
+ export { assignVariant, cyrb53, forcedAssignment, hashToUnit, matchesTargeting };
90
+
91
+ //# sourceMappingURL=bucketing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments/bucketing.js","sources":["../../src/experiments/bucketing.ts"],"sourcesContent":["import type {\n ActiveExperiment,\n ExperimentTargeting,\n VariantAssignment,\n VisitorContext\n} from \"./types.js\";\n\n/**\n * cyrb53 — a fast, well-distributed non-cryptographic string hash. Deterministic across\n * runtimes, which is what makes server-side bucketing stable for a given visitor + experiment.\n */\nexport const cyrb53 = (str: string, seed = 0): number => {\n let h1 = 0xdeadbeef ^ seed;\n let h2 = 0x41c6ce57 ^ seed;\n for (let i = 0; i < str.length; i++) {\n const ch = str.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return 4294967296 * (2097151 & h2) + (h1 >>> 0);\n};\n\n/** Hash a seed to a stable number in the half-open interval [0, 1). */\nexport const hashToUnit = (seed: string): number => {\n return cyrb53(seed) / 9007199254740992;\n};\n\n/**\n * Whether a visitor matches the experiment's geo/device targeting. Tenant scoping is implicit:\n * the experiment id is unique within a tenant, so a visitor's bucket never crosses tenants.\n */\nexport const matchesTargeting = (targeting: ExperimentTargeting, ctx: VisitorContext): boolean => {\n if (targeting.geo && targeting.geo.length > 0) {\n if (!ctx.country || !targeting.geo.includes(ctx.country)) {\n return false;\n }\n }\n if (targeting.device && targeting.device.length > 0) {\n if (!ctx.device || !targeting.device.includes(ctx.device)) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Deterministically assign a visitor to the control bucket or a variant.\n *\n * 1. If geo/device targeting excludes the visitor → control, excluded (no exposure).\n * 2. If the visitor falls outside the traffic percentage → control, excluded (no exposure).\n * 3. Otherwise bucket among the control + variants by their weights, seeded by\n * visitorId + experimentId so the result is stable and tenant-isolated.\n */\nexport const assignVariant = (\n experiment: ActiveExperiment,\n ctx: VisitorContext\n): VariantAssignment => {\n const controlVariantId = experiment.controlVariantId;\n const control: VariantAssignment = {\n variantId: controlVariantId,\n isControl: true,\n excluded: true,\n forced: false\n };\n\n if (!matchesTargeting(experiment.targeting, ctx)) {\n return control;\n }\n\n const trafficPercentage = experiment.targeting.trafficPercentage ?? 100;\n const inclusion = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}:inclusion`);\n if (inclusion >= trafficPercentage / 100) {\n return control;\n }\n\n // Build the bucket list: control first, then every variant that is both ready (present in\n // experiment.variants) and assigned a weight in the traffic split.\n const readyVariantIds = new Set(experiment.variants.map(variant => variant.variantId));\n const buckets: Array<{ id: string; weight: number }> = [\n { id: controlVariantId, weight: Math.max(0, experiment.trafficSplit.control) }\n ];\n for (const [variantId, weight] of Object.entries(experiment.trafficSplit.variants)) {\n if (readyVariantIds.has(variantId) && weight > 0) {\n buckets.push({ id: variantId, weight });\n }\n }\n\n const totalWeight = buckets.reduce((sum, bucket) => sum + bucket.weight, 0);\n if (totalWeight <= 0) {\n return { variantId: controlVariantId, isControl: true, excluded: false, forced: false };\n }\n\n let roll = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}`) * totalWeight;\n for (const bucket of buckets) {\n roll -= bucket.weight;\n if (roll < 0) {\n return {\n variantId: bucket.id,\n isControl: bucket.id === controlVariantId,\n excluded: false,\n forced: false\n };\n }\n }\n\n // Floating-point edge: fall back to the last bucket.\n const last = buckets[buckets.length - 1];\n return {\n variantId: last.id,\n isControl: last.id === controlVariantId,\n excluded: false,\n forced: false\n };\n};\n\n/**\n * A forced assignment for QA/review. Overrides bucketing and is never counted in results.\n * Returns null when the forced id does not match the control or any ready variant.\n */\nexport const forcedAssignment = (\n experiment: ActiveExperiment,\n forcedVariantId: string\n): VariantAssignment | null => {\n if (forcedVariantId === experiment.controlVariantId) {\n return { variantId: forcedVariantId, isControl: true, excluded: false, forced: true };\n }\n const isReady = experiment.variants.some(variant => variant.variantId === forcedVariantId);\n if (!isReady) {\n return null;\n }\n return { variantId: forcedVariantId, isControl: false, excluded: false, forced: true };\n};\n"],"names":["cyrb53","str","seed","h1","h2","i","ch","Math","hashToUnit","matchesTargeting","targeting","ctx","assignVariant","experiment","controlVariantId","control","trafficPercentage","inclusion","readyVariantIds","Set","variant","buckets","variantId","weight","Object","totalWeight","sum","bucket","roll","last","forcedAssignment","forcedVariantId","isReady"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};