@funnelsgrove/runtime 0.1.0

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 (46) hide show
  1. package/README.md +42 -0
  2. package/dist/components/FunnelContext.d.ts +43 -0
  3. package/dist/components/FunnelContext.js +14 -0
  4. package/dist/components/FunnelEditorPanel.d.ts +22 -0
  5. package/dist/components/FunnelEditorPanel.js +116 -0
  6. package/dist/components/shared/PrimaryButton.d.ts +8 -0
  7. package/dist/components/shared/PrimaryButton.js +4 -0
  8. package/dist/config/builder-preview.protocol.d.ts +4 -0
  9. package/dist/config/builder-preview.protocol.js +4 -0
  10. package/dist/config/funnel-theme.d.ts +59 -0
  11. package/dist/config/funnel-theme.js +69 -0
  12. package/dist/config/funnel.manifest.types.d.ts +50 -0
  13. package/dist/config/funnel.manifest.types.js +1 -0
  14. package/dist/index.d.ts +21 -0
  15. package/dist/index.js +21 -0
  16. package/dist/runtime/browser-helpers.d.ts +9 -0
  17. package/dist/runtime/browser-helpers.js +57 -0
  18. package/dist/runtime/experiment-assignment.d.ts +4 -0
  19. package/dist/runtime/experiment-assignment.js +32 -0
  20. package/dist/runtime/funnel-flow.d.ts +23 -0
  21. package/dist/runtime/funnel-flow.js +66 -0
  22. package/dist/runtime/funnel-manifest.validation.d.ts +2 -0
  23. package/dist/runtime/funnel-manifest.validation.js +63 -0
  24. package/dist/runtime/funnel-runtime.d.ts +34 -0
  25. package/dist/runtime/funnel-runtime.js +75 -0
  26. package/dist/runtime/preview-bridge.d.ts +51 -0
  27. package/dist/runtime/preview-bridge.js +230 -0
  28. package/dist/runtime/route-resolver.d.ts +18 -0
  29. package/dist/runtime/route-resolver.js +91 -0
  30. package/dist/runtime/use-funnel-flow-controller.d.ts +58 -0
  31. package/dist/runtime/use-funnel-flow-controller.js +523 -0
  32. package/dist/sdk/userAnswers.d.ts +45 -0
  33. package/dist/sdk/userAnswers.js +10 -0
  34. package/dist/services/api.service.d.ts +81 -0
  35. package/dist/services/api.service.js +346 -0
  36. package/dist/services/logger.d.ts +8 -0
  37. package/dist/services/logger.js +16 -0
  38. package/dist/services/preview-frame.service.d.ts +4 -0
  39. package/dist/services/preview-frame.service.js +31 -0
  40. package/dist/services/runtime-api.config.d.ts +7 -0
  41. package/dist/services/runtime-api.config.js +40 -0
  42. package/dist/services/runtime-mode.service.d.ts +5 -0
  43. package/dist/services/runtime-mode.service.js +113 -0
  44. package/dist/steps/types.d.ts +22 -0
  45. package/dist/steps/types.js +17 -0
  46. package/package.json +39 -0
@@ -0,0 +1,66 @@
1
+ import { canUseDom } from './browser-helpers';
2
+ import { chooseAssignedVariantId, getExperimentAssignmentStorageKey, } from './experiment-assignment';
3
+ export const isPreviewStepLockRequested = () => {
4
+ if (!canUseDom()) {
5
+ return false;
6
+ }
7
+ try {
8
+ return new URLSearchParams(window.location.search).get('previewStepLock') === '1';
9
+ }
10
+ catch (_a) {
11
+ return false;
12
+ }
13
+ };
14
+ export const getRequestedEntryPointId = () => {
15
+ var _a, _b;
16
+ if (!canUseDom()) {
17
+ return null;
18
+ }
19
+ try {
20
+ const searchParams = new URLSearchParams(window.location.search);
21
+ return ((_a = searchParams.get('entryPoint')) === null || _a === void 0 ? void 0 : _a.trim()) || ((_b = searchParams.get('entry')) === null || _b === void 0 ? void 0 : _b.trim()) || null;
22
+ }
23
+ catch (_c) {
24
+ return null;
25
+ }
26
+ };
27
+ export const getRuntimeFunnelIdentity = () => {
28
+ var _a;
29
+ if (!canUseDom()) {
30
+ return 'default';
31
+ }
32
+ try {
33
+ const searchParams = new URLSearchParams(window.location.search);
34
+ return ((_a = searchParams.get('funnelId')) === null || _a === void 0 ? void 0 : _a.trim()) || 'default';
35
+ }
36
+ catch (_b) {
37
+ return 'default';
38
+ }
39
+ };
40
+ export const getAssignedVariantId = (experiment, userId, storage) => {
41
+ if (!userId.trim()) {
42
+ return null;
43
+ }
44
+ const storageKey = getExperimentAssignmentStorageKey(getRuntimeFunnelIdentity(), userId, experiment.experimentId);
45
+ const storedVariantId = storage.read(storageKey);
46
+ const selectedVariantId = chooseAssignedVariantId(experiment, userId, storedVariantId);
47
+ if (!selectedVariantId) {
48
+ return null;
49
+ }
50
+ if (storedVariantId !== selectedVariantId) {
51
+ storage.write(storageKey, selectedVariantId);
52
+ }
53
+ return selectedVariantId;
54
+ };
55
+ export const resolveNextStepFromContext = (input) => {
56
+ var _a, _b;
57
+ const configuredStepId = input.getConfiguredNextStepId(input.stepId, {
58
+ attributes: input.attributes,
59
+ userId: input.userId,
60
+ getAssignedVariantId: (experiment) => input.getAssignedVariantId(experiment, input.userId),
61
+ });
62
+ if (configuredStepId) {
63
+ return (_a = input.resolveRenderableStepId(configuredStepId)) !== null && _a !== void 0 ? _a : input.safeInitialStepId;
64
+ }
65
+ return (_b = input.resolveRenderableStepId(input.getSequentialNextStepId(input.stepId))) !== null && _b !== void 0 ? _b : input.safeInitialStepId;
66
+ };
@@ -0,0 +1,2 @@
1
+ import type { FunnelManifest } from '../config/funnel.manifest.types';
2
+ export declare const validateFunnelManifest: <T extends FunnelManifest>(manifest: T) => T;
@@ -0,0 +1,63 @@
1
+ const isDefinedString = (value) => {
2
+ return typeof value === 'string' && value.trim().length > 0;
3
+ };
4
+ const pushDuplicateErrors = (label, values, errors) => {
5
+ const seen = new Set();
6
+ const duplicates = new Set();
7
+ for (const value of values) {
8
+ if (seen.has(value)) {
9
+ duplicates.add(value);
10
+ continue;
11
+ }
12
+ seen.add(value);
13
+ }
14
+ if (duplicates.size > 0) {
15
+ errors.push(`duplicate ${label}: ${Array.from(duplicates).join(', ')}`);
16
+ }
17
+ };
18
+ const assertValidReference = (label, value, validIds, errors) => {
19
+ if (!isDefinedString(value) || !validIds.has(value)) {
20
+ errors.push(`${label} references missing step "${value || ''}"`);
21
+ }
22
+ };
23
+ export const validateFunnelManifest = (manifest) => {
24
+ const errors = [];
25
+ if (!Number.isInteger(manifest.templateArchitectureVersion) || manifest.templateArchitectureVersion < 1) {
26
+ errors.push('templateArchitectureVersion must be a positive integer');
27
+ }
28
+ if (manifest.steps.length === 0) {
29
+ errors.push('manifest must declare at least one step');
30
+ }
31
+ const stepIds = manifest.steps.map((step) => step.id);
32
+ const stepPaths = manifest.steps.map((step) => step.path);
33
+ const filePaths = manifest.steps.map((step) => step.filePath);
34
+ const componentKeys = manifest.steps.map((step) => step.componentKey);
35
+ const validStepIds = new Set(stepIds);
36
+ pushDuplicateErrors('step ids', stepIds, errors);
37
+ pushDuplicateErrors('step paths', stepPaths, errors);
38
+ pushDuplicateErrors('step file paths', filePaths, errors);
39
+ pushDuplicateErrors('step component keys', componentKeys, errors);
40
+ const defaultEntryPoints = manifest.entryPoints.filter((entryPoint) => entryPoint.isDefault === true);
41
+ if (defaultEntryPoints.length !== 1) {
42
+ errors.push(`manifest must declare exactly one default entry point, found ${defaultEntryPoints.length}`);
43
+ }
44
+ for (const entryPoint of manifest.entryPoints) {
45
+ assertValidReference(`entry point "${entryPoint.id}"`, entryPoint.stepId, validStepIds, errors);
46
+ }
47
+ for (const [stepId, edges] of Object.entries(manifest.edgesByStepId)) {
48
+ assertValidReference(`edge source "${stepId}"`, stepId, validStepIds, errors);
49
+ for (const edge of edges || []) {
50
+ assertValidReference(`edge from "${stepId}"`, edge.toStepId, validStepIds, errors);
51
+ }
52
+ }
53
+ for (const experiment of manifest.experiments) {
54
+ assertValidReference(`experiment "${experiment.experimentId}"`, experiment.stepId, validStepIds, errors);
55
+ for (const variant of experiment.variants) {
56
+ assertValidReference(`experiment variant "${experiment.experimentId}:${variant.id}"`, variant.routeToStepId, validStepIds, errors);
57
+ }
58
+ }
59
+ if (errors.length > 0) {
60
+ throw new Error(`Invalid funnel manifest:\n- ${errors.join('\n- ')}`);
61
+ }
62
+ return manifest;
63
+ };
@@ -0,0 +1,34 @@
1
+ import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types';
2
+ export type FunnelStepId = string;
3
+ export declare const getFunnelStepSequence: (manifest: FunnelManifest) => FunnelStepId[];
4
+ export declare const getFunnelEntryPoints: (manifest: FunnelManifest) => {
5
+ stepId: FunnelStepId;
6
+ id: string;
7
+ isDefault?: boolean;
8
+ }[];
9
+ export type FunnelExperimentConfig = FunnelManifestExperiment & {
10
+ stepId: FunnelStepId;
11
+ variants: Array<FunnelManifestExperiment['variants'][number] & {
12
+ routeToStepId: FunnelStepId;
13
+ }>;
14
+ };
15
+ export declare const getFunnelExperiments: (manifest: FunnelManifest) => FunnelExperimentConfig[];
16
+ export declare const getDefaultFunnelStepId: (manifest: FunnelManifest) => FunnelStepId;
17
+ export declare const getIsFunnelStepId: (manifest: FunnelManifest, value: string) => value is FunnelStepId;
18
+ export declare const getStepIdFromPath: (manifest: FunnelManifest, pathname: string) => FunnelStepId | null;
19
+ export declare const getDefaultEntryPointStepId: (manifest: FunnelManifest) => FunnelStepId;
20
+ export declare const getEntryPointStepId: (manifest: FunnelManifest, entryPointId: string | null | undefined) => FunnelStepId | null;
21
+ export declare const getSequentialNextStepId: (manifest: FunnelManifest, stepId: FunnelStepId) => FunnelStepId | null;
22
+ export declare const getChoiceTargetsForStep: (manifest: FunnelManifest, stepId: FunnelStepId) => {
23
+ yes?: FunnelStepId;
24
+ no?: FunnelStepId;
25
+ } | null;
26
+ export declare const resolveRuntimeInitialStepId: (input: {
27
+ manifest: FunnelManifest;
28
+ requestedStepId?: string | null;
29
+ entryPointId?: string | null;
30
+ }) => FunnelStepId;
31
+ export declare const resolveConfiguredNextStep: (manifest: FunnelManifest, stepId: FunnelStepId, context: {
32
+ attributes: Record<string, unknown>;
33
+ getAssignedVariantId: (experiment: FunnelManifestExperiment) => string | null;
34
+ }) => FunnelStepId | null;
@@ -0,0 +1,75 @@
1
+ import { getChoiceTargetsFromEdges, isManifestStepId, resolveInitialStepId, resolveNextStepId, } from './route-resolver';
2
+ export const getFunnelStepSequence = (manifest) => manifest.steps.map((step) => step.id);
3
+ export const getFunnelEntryPoints = (manifest) => manifest.entryPoints.map((entryPoint) => (Object.assign(Object.assign({}, entryPoint), { stepId: entryPoint.stepId })));
4
+ export const getFunnelExperiments = (manifest) => manifest.experiments.map((experiment) => (Object.assign(Object.assign({}, experiment), { stepId: experiment.stepId, variants: experiment.variants.map((variant) => (Object.assign(Object.assign({}, variant), { routeToStepId: variant.routeToStepId }))) })));
5
+ export const getDefaultFunnelStepId = (manifest) => {
6
+ var _a;
7
+ return ((_a = manifest.entryPoints.find((entryPoint) => {
8
+ return 'isDefault' in entryPoint && entryPoint.isDefault;
9
+ })) === null || _a === void 0 ? void 0 : _a.stepId) ||
10
+ getFunnelStepSequence(manifest)[0];
11
+ };
12
+ export const getIsFunnelStepId = (manifest, value) => {
13
+ return new Set(getFunnelStepSequence(manifest)).has(value);
14
+ };
15
+ export const getStepIdFromPath = (manifest, pathname) => {
16
+ const segment = pathname
17
+ .split('?')[0]
18
+ .split('#')[0]
19
+ .split('/')
20
+ .map((part) => part.trim())
21
+ .filter(Boolean)[0];
22
+ if (!segment || !getIsFunnelStepId(manifest, segment)) {
23
+ return null;
24
+ }
25
+ return segment;
26
+ };
27
+ export const getDefaultEntryPointStepId = (manifest) => {
28
+ return getDefaultFunnelStepId(manifest);
29
+ };
30
+ export const getEntryPointStepId = (manifest, entryPointId) => {
31
+ var _a;
32
+ if (!entryPointId) {
33
+ return null;
34
+ }
35
+ const stepId = (_a = manifest.entryPoints.find((entryPoint) => entryPoint.id === entryPointId)) === null || _a === void 0 ? void 0 : _a.stepId;
36
+ return stepId && getIsFunnelStepId(manifest, stepId) ? stepId : null;
37
+ };
38
+ export const getSequentialNextStepId = (manifest, stepId) => {
39
+ var _a;
40
+ const stepSequence = getFunnelStepSequence(manifest);
41
+ const stepIndex = stepSequence.indexOf(stepId);
42
+ if (stepIndex < 0) {
43
+ return null;
44
+ }
45
+ return (_a = stepSequence[stepIndex + 1]) !== null && _a !== void 0 ? _a : null;
46
+ };
47
+ export const getChoiceTargetsForStep = (manifest, stepId) => {
48
+ const choiceTargets = getChoiceTargetsFromEdges(manifest, stepId);
49
+ if (!choiceTargets) {
50
+ return null;
51
+ }
52
+ return {
53
+ yes: choiceTargets.yes && getIsFunnelStepId(manifest, choiceTargets.yes) ? choiceTargets.yes : undefined,
54
+ no: choiceTargets.no && getIsFunnelStepId(manifest, choiceTargets.no) ? choiceTargets.no : undefined,
55
+ };
56
+ };
57
+ export const resolveRuntimeInitialStepId = (input) => {
58
+ const stepId = resolveInitialStepId({
59
+ manifest: input.manifest,
60
+ requestedStepId: input.requestedStepId,
61
+ entryPointId: input.entryPointId,
62
+ });
63
+ return getIsFunnelStepId(input.manifest, stepId) ? stepId : getDefaultFunnelStepId(input.manifest);
64
+ };
65
+ export const resolveConfiguredNextStep = (manifest, stepId, context) => {
66
+ const nextStepId = resolveNextStepId({
67
+ manifest,
68
+ currentStepId: stepId,
69
+ attributes: context.attributes,
70
+ getAssignedVariantId: context.getAssignedVariantId,
71
+ });
72
+ return nextStepId && isManifestStepId(manifest, nextStepId) && getIsFunnelStepId(manifest, nextStepId)
73
+ ? nextStepId
74
+ : null;
75
+ };
@@ -0,0 +1,51 @@
1
+ import type { RuntimeMode } from '../services/runtime-mode.service';
2
+ import type { FunnelStepId } from './funnel-runtime';
3
+ export type PreviewQuickEditPatch = {
4
+ kind: 'tagText';
5
+ stepId: string;
6
+ tag: string;
7
+ index: number;
8
+ value: string;
9
+ } | {
10
+ kind: 'attribute';
11
+ stepId: string;
12
+ attribute: 'placeholder' | 'alt' | 'title' | 'aria-label';
13
+ index: number;
14
+ value: string;
15
+ } | {
16
+ kind: 'imageSrc';
17
+ stepId: string;
18
+ index: number;
19
+ value: string;
20
+ } | {
21
+ kind: 'cssUrl';
22
+ stepId: string;
23
+ index: number;
24
+ value: string;
25
+ } | {
26
+ kind: 'cssText';
27
+ stepId: string;
28
+ value: string;
29
+ };
30
+ type PreviewBridgeMessage = {
31
+ kind: 'quickEditPatch';
32
+ patch: PreviewQuickEditPatch;
33
+ } | {
34
+ kind: 'runtimeModeChanged';
35
+ mode: RuntimeMode;
36
+ } | {
37
+ kind: 'goToStep';
38
+ stepId: string;
39
+ };
40
+ export declare const parsePreviewQuickEditPatch: (value: unknown) => PreviewQuickEditPatch | null;
41
+ export declare const parsePreviewBridgeMessage: (value: unknown) => PreviewBridgeMessage | null;
42
+ export declare const applyPreviewQuickEditPatch: (patch: PreviewQuickEditPatch) => void;
43
+ export type PreviewBridgeOptions = {
44
+ activeStepId: FunnelStepId;
45
+ onGoToStep: (stepId: FunnelStepId) => void;
46
+ resolveRenderableStepId: (stepId: string) => FunnelStepId | null;
47
+ setRuntimeMode: (mode: RuntimeMode) => void;
48
+ shouldLockToInitialStep: boolean;
49
+ };
50
+ export declare function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }: PreviewBridgeOptions): void;
51
+ export {};
@@ -0,0 +1,230 @@
1
+ 'use client';
2
+ import { useEffect, useRef } from 'react';
3
+ import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, } from '../config/builder-preview.protocol';
4
+ import { isPreviewFrameRuntime } from '../services/preview-frame.service';
5
+ import { logger } from '../services/logger';
6
+ const isRecord = (value) => {
7
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
8
+ };
9
+ const isPositiveIndex = (value) => {
10
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1;
11
+ };
12
+ export const parsePreviewQuickEditPatch = (value) => {
13
+ if (!isRecord(value)) {
14
+ return null;
15
+ }
16
+ const kind = typeof value.kind === 'string' ? value.kind : '';
17
+ const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
18
+ const patchValue = typeof value.value === 'string' ? value.value : null;
19
+ if (!kind || !stepId || patchValue === null) {
20
+ return null;
21
+ }
22
+ if (kind === 'tagText') {
23
+ const tag = typeof value.tag === 'string' ? value.tag.trim().toLowerCase() : '';
24
+ const index = value.index;
25
+ if (!tag || !isPositiveIndex(index)) {
26
+ return null;
27
+ }
28
+ return {
29
+ kind,
30
+ stepId,
31
+ tag,
32
+ index,
33
+ value: patchValue,
34
+ };
35
+ }
36
+ if (kind === 'attribute') {
37
+ const attribute = value.attribute === 'placeholder' ||
38
+ value.attribute === 'alt' ||
39
+ value.attribute === 'title' ||
40
+ value.attribute === 'aria-label'
41
+ ? value.attribute
42
+ : null;
43
+ const index = value.index;
44
+ if (!attribute || !isPositiveIndex(index)) {
45
+ return null;
46
+ }
47
+ return {
48
+ kind,
49
+ stepId,
50
+ attribute,
51
+ index,
52
+ value: patchValue,
53
+ };
54
+ }
55
+ if (kind === 'imageSrc' || kind === 'cssUrl') {
56
+ const index = value.index;
57
+ if (!isPositiveIndex(index)) {
58
+ return null;
59
+ }
60
+ return {
61
+ kind,
62
+ stepId,
63
+ index,
64
+ value: patchValue,
65
+ };
66
+ }
67
+ if (kind === 'cssText') {
68
+ return {
69
+ kind,
70
+ stepId,
71
+ value: patchValue,
72
+ };
73
+ }
74
+ return null;
75
+ };
76
+ export const parsePreviewBridgeMessage = (value) => {
77
+ if (!isRecord(value)) {
78
+ return null;
79
+ }
80
+ const messageType = typeof value.type === 'string' ? value.type : '';
81
+ if (!messageType) {
82
+ return null;
83
+ }
84
+ if (messageType === 'builder.preview.quickEdit.patch') {
85
+ const patch = parsePreviewQuickEditPatch(value.patch);
86
+ return patch ? { kind: 'quickEditPatch', patch } : null;
87
+ }
88
+ if (messageType === BUILDER_PREVIEW_RUNTIME_MODE_CHANGED) {
89
+ return {
90
+ kind: 'runtimeModeChanged',
91
+ mode: value.mode === 'live' ? 'live' : 'test',
92
+ };
93
+ }
94
+ if (messageType === BUILDER_PREVIEW_GO_TO_STEP) {
95
+ const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
96
+ return stepId ? { kind: 'goToStep', stepId } : null;
97
+ }
98
+ return null;
99
+ };
100
+ export const applyPreviewQuickEditPatch = (patch) => {
101
+ if (typeof document === 'undefined') {
102
+ return;
103
+ }
104
+ const stageRoot = document.querySelector('.step-stage-transition');
105
+ if (!(stageRoot instanceof HTMLElement)) {
106
+ return;
107
+ }
108
+ if (patch.kind === 'tagText') {
109
+ const candidates = stageRoot.querySelectorAll(patch.tag);
110
+ const target = candidates.item(patch.index - 1);
111
+ if (target) {
112
+ target.textContent = patch.value;
113
+ }
114
+ return;
115
+ }
116
+ if (patch.kind === 'attribute') {
117
+ const candidates = stageRoot.querySelectorAll(`[${patch.attribute}]`);
118
+ const target = candidates.item(patch.index - 1);
119
+ if (target) {
120
+ target.setAttribute(patch.attribute, patch.value);
121
+ }
122
+ return;
123
+ }
124
+ if (patch.kind === 'imageSrc') {
125
+ const imageLikePathRegex = /\.(png|jpe?g|gif|webp|svg)(?:$|[?#])/i;
126
+ const candidates = Array.from(stageRoot.querySelectorAll('[src]')).filter((element) => {
127
+ const srcValue = element.getAttribute('src') || '';
128
+ return imageLikePathRegex.test(srcValue);
129
+ });
130
+ const target = candidates[patch.index - 1];
131
+ if (target) {
132
+ target.setAttribute('src', patch.value);
133
+ }
134
+ return;
135
+ }
136
+ if (patch.kind === 'cssUrl') {
137
+ const imageLikePathRegex = /(url\(\s*["']?)(\/[^"')]+\.(?:png|jpe?g|gif|webp|svg)(?:[?#][^"')]*)?)(["']?\s*\))/gi;
138
+ let globalMatchIndex = 0;
139
+ const styleElements = Array.from(stageRoot.querySelectorAll('style'));
140
+ for (const styleElement of styleElements) {
141
+ const source = styleElement.textContent || '';
142
+ let didReplace = false;
143
+ const nextContent = source.replace(imageLikePathRegex, (full, start, _pathPart, end) => {
144
+ globalMatchIndex += 1;
145
+ if (globalMatchIndex !== patch.index) {
146
+ return full;
147
+ }
148
+ didReplace = true;
149
+ return `${start}${patch.value}${end}`;
150
+ });
151
+ if (didReplace && nextContent !== source) {
152
+ styleElement.textContent = nextContent;
153
+ return;
154
+ }
155
+ }
156
+ return;
157
+ }
158
+ if (patch.kind === 'cssText') {
159
+ const styleElements = stageRoot.querySelectorAll('style');
160
+ const target = styleElements.item(styleElements.length - 1);
161
+ if (target) {
162
+ target.textContent = patch.value;
163
+ }
164
+ }
165
+ };
166
+ function emitPreviewActiveStepChanged(stepId) {
167
+ if (typeof window === 'undefined' || window.parent === window) {
168
+ return;
169
+ }
170
+ window.parent.postMessage({
171
+ type: BUILDER_PREVIEW_ACTIVE_STEP_CHANGED,
172
+ stepId,
173
+ }, '*');
174
+ }
175
+ function emitPreviewReady(stepId) {
176
+ if (typeof window === 'undefined' || window.parent === window) {
177
+ return;
178
+ }
179
+ window.parent.postMessage({
180
+ type: BUILDER_PREVIEW_READY,
181
+ stepId,
182
+ }, '*');
183
+ }
184
+ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }) {
185
+ const previewReadySentRef = useRef(false);
186
+ useEffect(() => {
187
+ emitPreviewActiveStepChanged(activeStepId);
188
+ }, [activeStepId]);
189
+ useEffect(() => {
190
+ if (!isPreviewFrameRuntime() || previewReadySentRef.current) {
191
+ return;
192
+ }
193
+ previewReadySentRef.current = true;
194
+ emitPreviewReady(activeStepId);
195
+ }, [activeStepId]);
196
+ useEffect(() => {
197
+ if (shouldLockToInitialStep) {
198
+ return;
199
+ }
200
+ const handlePreviewMessage = (event) => {
201
+ const action = parsePreviewBridgeMessage(event.data);
202
+ if (!action) {
203
+ return;
204
+ }
205
+ if (action.kind === 'quickEditPatch') {
206
+ if (action.patch.stepId !== activeStepId) {
207
+ return;
208
+ }
209
+ applyPreviewQuickEditPatch(action.patch);
210
+ return;
211
+ }
212
+ if (action.kind === 'runtimeModeChanged') {
213
+ setRuntimeMode(action.mode);
214
+ return;
215
+ }
216
+ const targetStepId = resolveRenderableStepId(action.stepId);
217
+ if (!targetStepId) {
218
+ logger.warn({
219
+ stepId: action.stepId,
220
+ }, '[preview-bridge] Ignoring preview navigation to non-renderable step');
221
+ return;
222
+ }
223
+ onGoToStep(targetStepId);
224
+ };
225
+ window.addEventListener('message', handlePreviewMessage);
226
+ return () => {
227
+ window.removeEventListener('message', handlePreviewMessage);
228
+ };
229
+ }, [activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep]);
230
+ }
@@ -0,0 +1,18 @@
1
+ import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types';
2
+ export declare const getChoiceTargetsFromEdges: (manifest: FunnelManifest, stepId: string) => {
3
+ yes?: string;
4
+ no?: string;
5
+ } | null;
6
+ export declare const isManifestStepId: (manifest: FunnelManifest, stepId: string | null | undefined) => boolean;
7
+ export declare const getPathForStep: (manifest: FunnelManifest, stepId: string) => string;
8
+ export declare const resolveInitialStepId: (input: {
9
+ manifest: FunnelManifest;
10
+ requestedStepId?: string | null;
11
+ entryPointId?: string | null;
12
+ }) => string;
13
+ export declare const resolveNextStepId: (input: {
14
+ manifest: FunnelManifest;
15
+ currentStepId: string;
16
+ attributes: Record<string, unknown>;
17
+ getAssignedVariantId: (experiment: FunnelManifestExperiment) => string | null;
18
+ }) => string | null;
@@ -0,0 +1,91 @@
1
+ const getStepById = (manifest, stepId) => {
2
+ if (!stepId) {
3
+ return null;
4
+ }
5
+ return manifest.steps.find((step) => step.id === stepId) || null;
6
+ };
7
+ const getEntryPointStepId = (manifest, entryPointId) => {
8
+ var _a;
9
+ if (!entryPointId) {
10
+ return null;
11
+ }
12
+ return ((_a = manifest.entryPoints.find((entryPoint) => entryPoint.id === entryPointId)) === null || _a === void 0 ? void 0 : _a.stepId) || null;
13
+ };
14
+ const getDefaultEntryPointStepId = (manifest) => {
15
+ var _a, _b;
16
+ return (((_a = manifest.entryPoints.find((entryPoint) => 'isDefault' in entryPoint && entryPoint.isDefault)) === null || _a === void 0 ? void 0 : _a.stepId) ||
17
+ ((_b = manifest.steps[0]) === null || _b === void 0 ? void 0 : _b.id) ||
18
+ '');
19
+ };
20
+ const getActiveExperimentForStep = (manifest, stepId) => {
21
+ return (manifest.experiments.find((experiment) => experiment.stepId === stepId && experiment.status === 'active') || null);
22
+ };
23
+ const resolveNextStepFromEdges = (edges, attributes) => {
24
+ var _a;
25
+ if (!edges || edges.length === 0) {
26
+ return null;
27
+ }
28
+ for (const edge of edges) {
29
+ if (!edge.conditionId) {
30
+ continue;
31
+ }
32
+ if (attributes[edge.conditionId] === true) {
33
+ return edge.toStepId;
34
+ }
35
+ }
36
+ return ((_a = edges.find((edge) => !edge.conditionId)) === null || _a === void 0 ? void 0 : _a.toStepId) || null;
37
+ };
38
+ export const getChoiceTargetsFromEdges = (manifest, stepId) => {
39
+ var _a;
40
+ const edges = manifest.edgesByStepId[stepId];
41
+ if (!edges || edges.length === 0) {
42
+ return null;
43
+ }
44
+ const targets = {};
45
+ for (const edge of edges) {
46
+ const conditionId = ((_a = edge.conditionId) === null || _a === void 0 ? void 0 : _a.trim()) || '';
47
+ const conditionMatch = conditionId.match(/:(yes|no)$/);
48
+ if (!(conditionMatch === null || conditionMatch === void 0 ? void 0 : conditionMatch[1])) {
49
+ continue;
50
+ }
51
+ if (conditionMatch[1] === 'yes') {
52
+ targets.yes = edge.toStepId;
53
+ }
54
+ if (conditionMatch[1] === 'no') {
55
+ targets.no = edge.toStepId;
56
+ }
57
+ }
58
+ return targets.yes || targets.no ? targets : null;
59
+ };
60
+ export const isManifestStepId = (manifest, stepId) => {
61
+ return Boolean(getStepById(manifest, stepId));
62
+ };
63
+ export const getPathForStep = (manifest, stepId) => {
64
+ var _a;
65
+ return ((_a = getStepById(manifest, stepId)) === null || _a === void 0 ? void 0 : _a.path) || `/${stepId}`;
66
+ };
67
+ export const resolveInitialStepId = (input) => {
68
+ const requestedStep = getStepById(input.manifest, input.requestedStepId);
69
+ if (requestedStep) {
70
+ return requestedStep.id;
71
+ }
72
+ const entryPointStepId = getEntryPointStepId(input.manifest, input.entryPointId);
73
+ const entryPointStep = getStepById(input.manifest, entryPointStepId);
74
+ if (entryPointStep) {
75
+ return entryPointStep.id;
76
+ }
77
+ return getDefaultEntryPointStepId(input.manifest);
78
+ };
79
+ export const resolveNextStepId = (input) => {
80
+ var _a;
81
+ const activeExperiment = getActiveExperimentForStep(input.manifest, input.currentStepId);
82
+ if (activeExperiment) {
83
+ const assignedVariantId = input.getAssignedVariantId(activeExperiment);
84
+ const assignedVariant = activeExperiment.variants.find((variant) => variant.id === assignedVariantId);
85
+ if (assignedVariant) {
86
+ return assignedVariant.routeToStepId;
87
+ }
88
+ return ((_a = activeExperiment.variants[0]) === null || _a === void 0 ? void 0 : _a.routeToStepId) || null;
89
+ }
90
+ return resolveNextStepFromEdges(input.manifest.edgesByStepId[input.currentStepId], input.attributes);
91
+ };