@funnelsgrove/cli 0.1.14 → 0.1.18
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/contracts/step-contract-v2.json +733 -0
- package/dist/analyticsOutput.d.ts +30 -15
- package/dist/analyticsOutput.js +45 -28
- package/dist/apiClient.d.ts +85 -0
- package/dist/apiClient.js +225 -8
- package/dist/cli.d.ts +149 -1
- package/dist/cli.js +1232 -71
- package/dist/diagnosticOutput.d.ts +42 -0
- package/dist/diagnosticOutput.js +83 -0
- package/dist/docsOutput.d.ts +19 -0
- package/dist/docsOutput.js +73 -0
- package/dist/docsTransaction.d.ts +81 -0
- package/dist/docsTransaction.js +1755 -0
- package/dist/funnelValidation.d.ts +14 -0
- package/dist/funnelValidation.js +297 -0
- package/dist/localSync.d.ts +79 -0
- package/dist/localSync.js +428 -53
- package/dist/projectValidator.d.ts +27 -0
- package/dist/projectValidator.js +208 -0
- package/dist/reskin.js +27 -1
- package/dist/stepContractMigration.d.ts +71 -0
- package/dist/stepContractMigration.js +1323 -0
- package/dist/templateDocs.js +8 -25
- package/docs-release-history.json +66 -0
- package/legacy-docs-catalog.json +1012 -0
- package/package.json +13 -4
- package/template_docs/.funnelsgrove-docs.json +144 -0
- package/template_docs/AGENTS.md +30 -58
- package/template_docs/CLAUDE.md +0 -2
- package/template_docs/docs/funnelsgrove/START-HERE.md +73 -0
- package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +59 -0
- package/template_docs/docs/funnelsgrove/contracts/content-answers.md +44 -0
- package/template_docs/docs/funnelsgrove/contracts/flow-routing.md +41 -0
- package/template_docs/docs/funnelsgrove/contracts/payments.md +42 -0
- package/template_docs/docs/funnelsgrove/contracts/step-metadata.md +57 -0
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
- package/template_docs/docs/funnelsgrove/qa/analytics.md +43 -0
- package/template_docs/docs/funnelsgrove/qa/local.md +42 -0
- package/template_docs/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
- package/template_docs/docs/funnelsgrove/qa/publish.md +32 -0
- package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +34 -0
- package/template_docs/docs/funnelsgrove/recipes/add-step.md +33 -0
- package/template_docs/docs/funnelsgrove/recipes/edit-step.md +31 -0
- package/template_docs/docs/funnelsgrove/steps/INDEX.md +26 -0
- package/template_docs/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
- package/template_docs/docs/funnelsgrove/steps/checkout.md +120 -0
- package/template_docs/docs/funnelsgrove/steps/form_input.md +115 -0
- package/template_docs/docs/funnelsgrove/steps/intro_hero.md +110 -0
- package/template_docs/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
- package/template_docs/docs/funnelsgrove/steps/paywall_offer.md +115 -0
- package/template_docs/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
- package/template_docs/docs/funnelsgrove/steps/purchase_completed.md +117 -0
- package/template_docs/docs/funnelsgrove/steps/single_step_choice.md +122 -0
- package/template_docs/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
- package/template_docs/docs/funnelsgrove/steps/social_proof.md +110 -0
- package/template_docs/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
- package/template_docs/docs/funnelsgrove/steps/subscription_management.md +112 -0
- package/template_docs/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
- package/template_docs/docs/funnelsgrove/steps/upsell_offer.md +112 -0
- package/template_docs/docs/funnelsgrove/steps/value_prop_story.md +110 -0
- package/template_docs/funnel-docs.config.json +43 -0
- package/template_scaffold/.env.example +41 -0
- package/template_scaffold/.funnelsgrove-contract-tools.json +19 -0
- package/template_scaffold/.funnelsgrove-docs.json +144 -0
- package/template_scaffold/.funnelsgrove-scaffold.json +1078 -0
- package/template_scaffold/AGENTS.md +42 -0
- package/template_scaffold/CLAUDE.md +1 -0
- package/template_scaffold/PLAN.md +81 -0
- package/template_scaffold/PRODUCT_SENSE.md +28 -0
- package/template_scaffold/context/build-context.md +34 -0
- package/template_scaffold/docs/AGENT_FUNNEL_IMPLEMENTATION_SPEC.md +5 -0
- package/template_scaffold/docs/ARCHITECTURE.md +5 -0
- package/template_scaffold/docs/FLOW_CONFIG_AND_ROUTING.md +5 -0
- package/template_scaffold/docs/PAYWALL_AND_PAYMENTS.md +5 -0
- package/template_scaffold/docs/README.md +5 -0
- package/template_scaffold/docs/funnelsgrove/START-HERE.md +73 -0
- package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +59 -0
- package/template_scaffold/docs/funnelsgrove/contracts/content-answers.md +44 -0
- package/template_scaffold/docs/funnelsgrove/contracts/flow-routing.md +41 -0
- package/template_scaffold/docs/funnelsgrove/contracts/payments.md +42 -0
- package/template_scaffold/docs/funnelsgrove/contracts/step-metadata.md +57 -0
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
- package/template_scaffold/docs/funnelsgrove/qa/analytics.md +43 -0
- package/template_scaffold/docs/funnelsgrove/qa/local.md +42 -0
- package/template_scaffold/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
- package/template_scaffold/docs/funnelsgrove/qa/publish.md +32 -0
- package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +34 -0
- package/template_scaffold/docs/funnelsgrove/recipes/add-step.md +33 -0
- package/template_scaffold/docs/funnelsgrove/recipes/edit-step.md +31 -0
- package/template_scaffold/docs/funnelsgrove/steps/INDEX.md +26 -0
- package/template_scaffold/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
- package/template_scaffold/docs/funnelsgrove/steps/checkout.md +120 -0
- package/template_scaffold/docs/funnelsgrove/steps/form_input.md +115 -0
- package/template_scaffold/docs/funnelsgrove/steps/intro_hero.md +110 -0
- package/template_scaffold/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
- package/template_scaffold/docs/funnelsgrove/steps/paywall_offer.md +115 -0
- package/template_scaffold/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
- package/template_scaffold/docs/funnelsgrove/steps/purchase_completed.md +117 -0
- package/template_scaffold/docs/funnelsgrove/steps/single_step_choice.md +122 -0
- package/template_scaffold/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
- package/template_scaffold/docs/funnelsgrove/steps/social_proof.md +110 -0
- package/template_scaffold/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
- package/template_scaffold/docs/funnelsgrove/steps/subscription_management.md +112 -0
- package/template_scaffold/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
- package/template_scaffold/docs/funnelsgrove/steps/upsell_offer.md +112 -0
- package/template_scaffold/docs/funnelsgrove/steps/value_prop_story.md +110 -0
- package/template_scaffold/docs/references/funnel-sdk-user-answers.md +5 -0
- package/template_scaffold/eslint.config.mjs +16 -0
- package/template_scaffold/funnel-agent-docs.test.ts +699 -0
- package/template_scaffold/funnel-docs.config.json +43 -0
- package/template_scaffold/funnel.config.json +5 -0
- package/template_scaffold/gitignore.template +5 -0
- package/template_scaffold/next-env.d.ts +6 -0
- package/template_scaffold/next.config.ts +53 -0
- package/template_scaffold/package-lock.json +7737 -0
- package/template_scaffold/package.json +37 -0
- package/template_scaffold/package.test.ts +23 -0
- package/template_scaffold/public/claimbee/bee.png +0 -0
- package/template_scaffold/public/claimbee/device-iphone.png +0 -0
- package/template_scaffold/public/claimbee/device-macbook.png +0 -0
- package/template_scaffold/public/claimbee/device-none.png +0 -0
- package/template_scaffold/public/claimbee/device-watch.png +0 -0
- package/template_scaffold/public/claimbee/glow.svg +3 -0
- package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
- package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
- package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
- package/template_scaffold/public/claimbee/testimonial-star.svg +3 -0
- package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
- package/template_scaffold/public/paywall/claim-history.png +0 -0
- package/template_scaffold/public/paywall/clear-progress.png +0 -0
- package/template_scaffold/public/paywall/hero.png +0 -0
- package/template_scaffold/public/paywall/money-alerts.png +0 -0
- package/template_scaffold/public/paywall/money-back-badge.png +0 -0
- package/template_scaffold/public/paywall/paywall-image.png +0 -0
- package/template_scaffold/public/paywall/r1.png +0 -0
- package/template_scaffold/public/paywall/r2.png +0 -0
- package/template_scaffold/public/paywall/r3.png +0 -0
- package/template_scaffold/public/paywall/r4.png +0 -0
- package/template_scaffold/public/paywall/r5.png +0 -0
- package/template_scaffold/public/paywall/r6.png +0 -0
- package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
- package/template_scaffold/public/please/bee.png +0 -0
- package/template_scaffold/public/please/device-iphone.png +0 -0
- package/template_scaffold/public/please/device-macbook.png +0 -0
- package/template_scaffold/public/please/device-none.png +0 -0
- package/template_scaffold/public/please/device-watch.png +0 -0
- package/template_scaffold/public/please/glow.svg +3 -0
- package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
- package/template_scaffold/public/please/steps-sprite-default.png +0 -0
- package/template_scaffold/public/please/testimonial-avatar.png +0 -0
- package/template_scaffold/public/please/testimonial-star.svg +3 -0
- package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
- package/template_scaffold/src/app/[stepId]/page.test.tsx +59 -0
- package/template_scaffold/src/app/[stepId]/page.tsx +62 -0
- package/template_scaffold/src/app/globals.css +49 -0
- package/template_scaffold/src/app/layout.test.ts +26 -0
- package/template_scaffold/src/app/layout.tsx +61 -0
- package/template_scaffold/src/app/page.tsx +8 -0
- package/template_scaffold/src/app/sdk/page.tsx +673 -0
- package/template_scaffold/src/app/subscription-dashboard/page.tsx +274 -0
- package/template_scaffold/src/assets/claimbee/bee.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
- package/template_scaffold/src/assets/claimbee/glow.svg +3 -0
- package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
- package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
- package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
- package/template_scaffold/src/assets/claimbee/testimonial-star.svg +3 -0
- package/template_scaffold/src/components/FunnelEditorPanel.tsx +194 -0
- package/template_scaffold/src/components/FunnelFlow.test.tsx +17 -0
- package/template_scaffold/src/components/FunnelFlow.tsx +114 -0
- package/template_scaffold/src/components/FunnelStepImage.test.ts +15 -0
- package/template_scaffold/src/components/FunnelStepImage.tsx +28 -0
- package/template_scaffold/src/components/step/FunnelStepHeader.tsx +35 -0
- package/template_scaffold/src/config/billing.plans.test.ts +143 -0
- package/template_scaffold/src/config/billing.plans.ts +69 -0
- package/template_scaffold/src/config/billing.test.plans.ts +32 -0
- package/template_scaffold/src/config/experiments.generated.ts +4 -0
- package/template_scaffold/src/config/experiments.test.ts +12 -0
- package/template_scaffold/src/config/experiments.ts +9 -0
- package/template_scaffold/src/config/funnel.config.ts +12 -0
- package/template_scaffold/src/config/funnel.manifest.test.ts +166 -0
- package/template_scaffold/src/config/funnel.manifest.ts +139 -0
- package/template_scaffold/src/config/offer-sets.generated.ts +4 -0
- package/template_scaffold/src/contract/funnel-project.validation.test.ts +80 -0
- package/template_scaffold/src/contract/funnel-project.validation.ts +44 -0
- package/template_scaffold/src/contract/funnel-validator.vite.config.ts +11 -0
- package/template_scaffold/src/contract/golden/cancellation_offer.fixture.ts +29 -0
- package/template_scaffold/src/contract/golden/checkout.fixture.ts +37 -0
- package/template_scaffold/src/contract/golden/form_input.fixture.ts +28 -0
- package/template_scaffold/src/contract/golden/golden-fixtures.test.ts +190 -0
- package/template_scaffold/src/contract/golden/intro_hero.fixture.ts +27 -0
- package/template_scaffold/src/contract/golden/multi_select_choice.fixture.ts +37 -0
- package/template_scaffold/src/contract/golden/paywall_offer.fixture.ts +30 -0
- package/template_scaffold/src/contract/golden/progress_interstitial.fixture.ts +28 -0
- package/template_scaffold/src/contract/golden/purchase_completed.fixture.ts +33 -0
- package/template_scaffold/src/contract/golden/single_step_choice.fixture.ts +36 -0
- package/template_scaffold/src/contract/golden/single_step_choice_emoji.fixture.ts +36 -0
- package/template_scaffold/src/contract/golden/social_proof.fixture.ts +27 -0
- package/template_scaffold/src/contract/golden/subscription_handoff.fixture.ts +30 -0
- package/template_scaffold/src/contract/golden/subscription_management.fixture.ts +29 -0
- package/template_scaffold/src/contract/golden/summary_confirmation.fixture.ts +27 -0
- package/template_scaffold/src/contract/golden/upsell_offer.fixture.ts +29 -0
- package/template_scaffold/src/contract/golden/value_prop_story.fixture.ts +27 -0
- package/template_scaffold/src/contract/validate-funnel.cli.test.ts +180 -0
- package/template_scaffold/src/contract/validate-funnel.cli.ts +199 -0
- package/template_scaffold/src/runtime/checkout-runtime-config.test.ts +60 -0
- package/template_scaffold/src/runtime/checkout-runtime-config.ts +26 -0
- package/template_scaffold/src/runtime/funnel-runtime.test.ts +25 -0
- package/template_scaffold/src/runtime/funnel-runtime.ts +118 -0
- package/template_scaffold/src/runtime/step-content-context.test.ts +38 -0
- package/template_scaffold/src/runtime/step-content-context.ts +65 -0
- package/template_scaffold/src/runtime/step-registry.ts +66 -0
- package/template_scaffold/src/runtime/step-variable-values.ts +31 -0
- package/template_scaffold/src/runtime/use-funnel-flow-controller.integration.test.ts +203 -0
- package/template_scaffold/src/runtime/use-funnel-flow-controller.test.ts +76 -0
- package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +53 -0
- package/template_scaffold/src/steps/content/email-capture.content.ts +29 -0
- package/template_scaffold/src/steps/content/manage-subscription.content.ts +62 -0
- package/template_scaffold/src/steps/content/paywall.content.ts +191 -0
- package/template_scaffold/src/steps/content/step-01.content.ts +23 -0
- package/template_scaffold/src/steps/content/step-02.content.ts +77 -0
- package/template_scaffold/src/steps/content/step-03.content.ts +40 -0
- package/template_scaffold/src/steps/content/step-04.content.ts +49 -0
- package/template_scaffold/src/steps/content/step-32-paywall.content.ts +352 -0
- package/template_scaffold/src/steps/content/subscription-started.content.ts +68 -0
- package/template_scaffold/src/steps/editor/email-capture.editor.ts +58 -0
- package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +179 -0
- package/template_scaffold/src/steps/editor/paywall.editor.ts +232 -0
- package/template_scaffold/src/steps/editor/step-01.editor.ts +22 -0
- package/template_scaffold/src/steps/editor/step-02.editor.ts +41 -0
- package/template_scaffold/src/steps/editor/step-03.editor.ts +58 -0
- package/template_scaffold/src/steps/editor/step-04.editor.ts +98 -0
- package/template_scaffold/src/steps/editor/step-32-paywall.editor.ts +478 -0
- package/template_scaffold/src/steps/editor/subscription-started.editor.ts +131 -0
- package/template_scaffold/src/steps/index.ts +25 -0
- package/template_scaffold/src/steps/paywall-checkout-options.test.ts +104 -0
- package/template_scaffold/src/steps/paywall-checkout-options.ts +130 -0
- package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +12 -0
- package/template_scaffold/src/steps/pricing/paywall.pricing.ts +12 -0
- package/template_scaffold/src/steps/pricing/shared-plan-order.ts +17 -0
- package/template_scaffold/src/steps/pricing/upsell-form.pricing.ts +16 -0
- package/template_scaffold/src/steps/shared-header-contract.test.ts +27 -0
- package/template_scaffold/src/steps/step-01.test.ts +38 -0
- package/template_scaffold/src/steps/step-01.tsx +108 -0
- package/template_scaffold/src/steps/step-02.test.ts +43 -0
- package/template_scaffold/src/steps/step-02.tsx +140 -0
- package/template_scaffold/src/steps/step-03.test.ts +15 -0
- package/template_scaffold/src/steps/step-03.tsx +262 -0
- package/template_scaffold/src/steps/step-04.test.ts +32 -0
- package/template_scaffold/src/steps/step-04.tsx +673 -0
- package/template_scaffold/src/steps/step-31-email-capture.test.ts +22 -0
- package/template_scaffold/src/steps/step-31-email-capture.tsx +195 -0
- package/template_scaffold/src/steps/step-32-paywall.test.ts +770 -0
- package/template_scaffold/src/steps/step-32-paywall.tsx +2253 -0
- package/template_scaffold/src/steps/step-32b-paywall-b.tsx +35 -0
- package/template_scaffold/src/steps/step-33-subscription-started.test.ts +26 -0
- package/template_scaffold/src/steps/step-33-subscription-started.tsx +86 -0
- package/template_scaffold/src/steps/step-34-upsell-form.test.ts +27 -0
- package/template_scaffold/src/steps/step-35-manage-subscription.tsx +46 -0
- package/template_scaffold/src/steps/step-content.registry.ts +45 -0
- package/template_scaffold/src/steps/step-image-migration.test.ts +20 -0
- package/template_scaffold/src/steps/styles/index.css +2 -0
- package/template_scaffold/src/steps/styles/shared/final-flow-shared.css +55 -0
- package/template_scaffold/src/steps/styles/shared/funnel-base.css +543 -0
- package/template_scaffold/src/steps/styles/shared/info-shared.css +139 -0
- package/template_scaffold/src/steps/styles/shared/progress-shared.css +117 -0
- package/template_scaffold/src/steps/styles/shared/question-shared.css +76 -0
- package/template_scaffold/src/steps/styles/shared/responsive-container.css +176 -0
- package/template_scaffold/src/steps/types.ts +7 -0
- package/template_scaffold/src/steps/use-claimbee-step-content.ts +15 -0
- package/template_scaffold/src/theme/theme.ts +81 -0
- package/template_scaffold/tsconfig.json +38 -0
- package/template_scaffold/vitest.config.ts +32 -0
- package/template_validation/.funnelsgrove-contract-tools.json +19 -0
- package/template_validation/src/contract/funnel-project.validation.ts +44 -0
- package/template_validation/src/contract/funnel-validator.vite.config.ts +11 -0
- package/template_validation/src/contract/validate-funnel.cli.ts +199 -0
- package/template_docs/docs/ab-experiments.md +0 -55
- package/template_docs/docs/analytics.md +0 -65
- package/template_docs/docs/editing-flow.md +0 -125
- package/template_docs/docs/editing-step.md +0 -125
- package/template_docs/docs/editor-and-content.md +0 -58
- package/template_docs/docs/funnel-runtime-architecture.md +0 -125
- package/template_docs/docs/meta-pixel-conversions-api.md +0 -50
- package/template_docs/docs/payment-plans-and-discounts.md +0 -106
- package/template_docs/docs/publishing-and-versioning.md +0 -117
- package/template_docs/docs/qa-checklist.md +0 -80
- package/template_docs/docs/sdk-api-endpoints.md +0 -78
- package/template_docs/docs/step-ui-guidelines.md +0 -110
- package/template_docs/docs/theme.md +0 -35
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
const manifestStep = {
|
|
4
|
+
id: 'member-results',
|
|
5
|
+
name: 'member-results',
|
|
6
|
+
path: '/member-results',
|
|
7
|
+
filePath: 'src/steps/step-member-results.tsx',
|
|
8
|
+
componentKey: 'stepMemberResults',
|
|
9
|
+
type: 'social_proof',
|
|
10
|
+
title: 'Member results',
|
|
11
|
+
} as const satisfies FunnelManifestStep;
|
|
12
|
+
|
|
13
|
+
const componentMeta = {
|
|
14
|
+
id: 'member-results',
|
|
15
|
+
name: 'member-results',
|
|
16
|
+
type: 'social_proof',
|
|
17
|
+
figmaNodeId: 'golden:social-proof',
|
|
18
|
+
title: 'Member results',
|
|
19
|
+
description: 'Show attributable testimonials and evidence.',
|
|
20
|
+
} as const satisfies FunnelStepMeta;
|
|
21
|
+
|
|
22
|
+
export const goldenFixture = {
|
|
23
|
+
stepType: 'social_proof',
|
|
24
|
+
manifestStep,
|
|
25
|
+
componentMeta,
|
|
26
|
+
actionOwner: 'shared-shell',
|
|
27
|
+
} as const;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
const manifestStep = {
|
|
4
|
+
id: 'open-app',
|
|
5
|
+
name: 'open-app',
|
|
6
|
+
path: '/open-app',
|
|
7
|
+
filePath: 'src/steps/step-open-app.tsx',
|
|
8
|
+
componentKey: 'stepOpenApp',
|
|
9
|
+
type: 'subscription_handoff',
|
|
10
|
+
kind: 'subscription-handoff',
|
|
11
|
+
title: 'Continue in the app',
|
|
12
|
+
} as const satisfies FunnelManifestStep;
|
|
13
|
+
|
|
14
|
+
const componentMeta = {
|
|
15
|
+
id: 'open-app',
|
|
16
|
+
name: 'open-app',
|
|
17
|
+
type: 'subscription_handoff',
|
|
18
|
+
kind: 'subscription-handoff',
|
|
19
|
+
figmaNodeId: 'golden:subscription-handoff',
|
|
20
|
+
title: 'Continue in the app',
|
|
21
|
+
description: 'Continue through a non-terminal app handoff.',
|
|
22
|
+
} as const satisfies FunnelStepMeta;
|
|
23
|
+
|
|
24
|
+
export const goldenFixture = {
|
|
25
|
+
stepType: 'subscription_handoff',
|
|
26
|
+
manifestStep,
|
|
27
|
+
componentMeta,
|
|
28
|
+
helper: 'runtime handoff helper',
|
|
29
|
+
isRegistrationCompletion: false,
|
|
30
|
+
} as const;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
const manifestStep = {
|
|
4
|
+
id: 'manage-subscription',
|
|
5
|
+
name: 'manage-subscription',
|
|
6
|
+
path: '/manage-subscription',
|
|
7
|
+
filePath: 'src/steps/step-manage-subscription.tsx',
|
|
8
|
+
componentKey: 'stepManageSubscription',
|
|
9
|
+
type: 'subscription_management',
|
|
10
|
+
kind: 'manage-subscription',
|
|
11
|
+
title: 'Manage subscription',
|
|
12
|
+
} as const satisfies FunnelManifestStep;
|
|
13
|
+
|
|
14
|
+
const componentMeta = {
|
|
15
|
+
id: 'manage-subscription',
|
|
16
|
+
name: 'manage-subscription',
|
|
17
|
+
type: 'subscription_management',
|
|
18
|
+
kind: 'manage-subscription',
|
|
19
|
+
figmaNodeId: 'golden:subscription-management',
|
|
20
|
+
title: 'Manage subscription',
|
|
21
|
+
description: 'Inspect and manage an existing subscription.',
|
|
22
|
+
} as const satisfies FunnelStepMeta;
|
|
23
|
+
|
|
24
|
+
export const goldenFixture = {
|
|
25
|
+
stepType: 'subscription_management',
|
|
26
|
+
manifestStep,
|
|
27
|
+
componentMeta,
|
|
28
|
+
helper: 'runtime subscription helpers',
|
|
29
|
+
} as const;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
const manifestStep = {
|
|
4
|
+
id: 'review-plan',
|
|
5
|
+
name: 'review-plan',
|
|
6
|
+
path: '/review-plan',
|
|
7
|
+
filePath: 'src/steps/step-review-plan.tsx',
|
|
8
|
+
componentKey: 'stepReviewPlan',
|
|
9
|
+
type: 'summary_confirmation',
|
|
10
|
+
title: 'Review your plan',
|
|
11
|
+
} as const satisfies FunnelManifestStep;
|
|
12
|
+
|
|
13
|
+
const componentMeta = {
|
|
14
|
+
id: 'review-plan',
|
|
15
|
+
name: 'review-plan',
|
|
16
|
+
type: 'summary_confirmation',
|
|
17
|
+
figmaNodeId: 'golden:summary-confirmation',
|
|
18
|
+
title: 'Review your plan',
|
|
19
|
+
description: 'Review existing answers before continuing.',
|
|
20
|
+
} as const satisfies FunnelStepMeta;
|
|
21
|
+
|
|
22
|
+
export const goldenFixture = {
|
|
23
|
+
stepType: 'summary_confirmation',
|
|
24
|
+
manifestStep,
|
|
25
|
+
componentMeta,
|
|
26
|
+
actionOwner: 'either',
|
|
27
|
+
} as const;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
const manifestStep = {
|
|
4
|
+
id: 'premium-upgrade',
|
|
5
|
+
name: 'premium-upgrade',
|
|
6
|
+
path: '/premium-upgrade',
|
|
7
|
+
filePath: 'src/steps/step-premium-upgrade.tsx',
|
|
8
|
+
componentKey: 'stepPremiumUpgrade',
|
|
9
|
+
type: 'upsell_offer',
|
|
10
|
+
kind: 'upsell',
|
|
11
|
+
title: 'Add premium support',
|
|
12
|
+
} as const satisfies FunnelManifestStep;
|
|
13
|
+
|
|
14
|
+
const componentMeta = {
|
|
15
|
+
id: 'premium-upgrade',
|
|
16
|
+
name: 'premium-upgrade',
|
|
17
|
+
type: 'upsell_offer',
|
|
18
|
+
kind: 'upsell',
|
|
19
|
+
figmaNodeId: 'golden:upsell-offer',
|
|
20
|
+
title: 'Add premium support',
|
|
21
|
+
description: 'Offer an optional post-purchase upgrade.',
|
|
22
|
+
} as const satisfies FunnelStepMeta;
|
|
23
|
+
|
|
24
|
+
export const goldenFixture = {
|
|
25
|
+
stepType: 'upsell_offer',
|
|
26
|
+
manifestStep,
|
|
27
|
+
componentMeta,
|
|
28
|
+
semanticHelpers: ['trackCheckoutStarted', 'trackPaymentInfoSubmitted', 'trackCheckoutCompleted'],
|
|
29
|
+
} as const;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
const manifestStep = {
|
|
4
|
+
id: 'your-plan',
|
|
5
|
+
name: 'your-plan',
|
|
6
|
+
path: '/your-plan',
|
|
7
|
+
filePath: 'src/steps/step-your-plan.tsx',
|
|
8
|
+
componentKey: 'stepYourPlan',
|
|
9
|
+
type: 'value_prop_story',
|
|
10
|
+
title: 'Your plan',
|
|
11
|
+
} as const satisfies FunnelManifestStep;
|
|
12
|
+
|
|
13
|
+
const componentMeta = {
|
|
14
|
+
id: 'your-plan',
|
|
15
|
+
name: 'your-plan',
|
|
16
|
+
type: 'value_prop_story',
|
|
17
|
+
figmaNodeId: 'golden:value-prop-story',
|
|
18
|
+
title: 'Your plan',
|
|
19
|
+
description: 'Narrative explanation of the product value.',
|
|
20
|
+
} as const satisfies FunnelStepMeta;
|
|
21
|
+
|
|
22
|
+
export const goldenFixture = {
|
|
23
|
+
stepType: 'value_prop_story',
|
|
24
|
+
manifestStep,
|
|
25
|
+
componentMeta,
|
|
26
|
+
actionOwner: 'shared-shell',
|
|
27
|
+
} as const;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
createFunnelContractDiagnostic,
|
|
8
|
+
CURRENT_STEP_CONTRACT_VERSION,
|
|
9
|
+
type FunnelContractDiagnostic,
|
|
10
|
+
} from '@funnelsgrove/runtime';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
const loadCli = async () => {
|
|
15
|
+
return import('./validate-funnel.cli');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const diagnostic = createFunnelContractDiagnostic({
|
|
19
|
+
code: 'FG-PARITY-001',
|
|
20
|
+
file: 'src/steps/example.tsx',
|
|
21
|
+
stepId: 'example',
|
|
22
|
+
expected: { name: 'example' },
|
|
23
|
+
received: { name: 'renamed-example' },
|
|
24
|
+
reason: 'Manifest and component metadata differ.',
|
|
25
|
+
guide: 'docs/funnelsgrove/contracts/step-metadata.md',
|
|
26
|
+
repair: 'Restore exact metadata parity.',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
type RunResult = {
|
|
30
|
+
exitCode: number;
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const runWith = async (
|
|
36
|
+
diagnostics: readonly FunnelContractDiagnostic[],
|
|
37
|
+
): Promise<RunResult> => {
|
|
38
|
+
const cli = await loadCli();
|
|
39
|
+
let stdout = '';
|
|
40
|
+
let stderr = '';
|
|
41
|
+
const exitCode = await cli.runValidateFunnelCli({
|
|
42
|
+
args: ['--json'],
|
|
43
|
+
loadValidation: async () => ({
|
|
44
|
+
contractVersion: cli.VALIDATOR_CONTRACT_VERSION,
|
|
45
|
+
diagnostics,
|
|
46
|
+
}),
|
|
47
|
+
writeStdout: (text: string) => {
|
|
48
|
+
stdout += text;
|
|
49
|
+
},
|
|
50
|
+
writeStderr: (text: string) => {
|
|
51
|
+
stderr += text;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return { exitCode, stdout, stderr };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('validate funnel CLI protocol', () => {
|
|
58
|
+
it('writes exactly one JSON object and exits zero for a valid project', async () => {
|
|
59
|
+
const result = await runWith([]);
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
exitCode: 0,
|
|
63
|
+
stdout: `${JSON.stringify({
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
contractVersion: CURRENT_STEP_CONTRACT_VERSION,
|
|
66
|
+
valid: true,
|
|
67
|
+
diagnostics: [],
|
|
68
|
+
})}\n`,
|
|
69
|
+
stderr: '',
|
|
70
|
+
});
|
|
71
|
+
expect(result.stdout.trim().split('\n')).toHaveLength(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('preserves blocker diagnostics exactly and renders human detail to stderr', async () => {
|
|
75
|
+
const result = await runWith([diagnostic]);
|
|
76
|
+
|
|
77
|
+
expect(result.exitCode).toBe(1);
|
|
78
|
+
expect(JSON.parse(result.stdout)).toEqual({
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
contractVersion: CURRENT_STEP_CONTRACT_VERSION,
|
|
81
|
+
valid: false,
|
|
82
|
+
diagnostics: [diagnostic],
|
|
83
|
+
});
|
|
84
|
+
expect(result.stderr).toContain('[FG-PARITY-001]');
|
|
85
|
+
expect(result.stderr).toContain('Manifest and component metadata differ.');
|
|
86
|
+
expect(result.stderr).toContain('src/steps/example.tsx');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('keeps import log noise off stdout and maps malformed imports to exit two', async () => {
|
|
90
|
+
const cli = await loadCli();
|
|
91
|
+
let stdout = '';
|
|
92
|
+
let stderr = '';
|
|
93
|
+
const exitCode = await cli.runValidateFunnelCli({
|
|
94
|
+
args: ['--json'],
|
|
95
|
+
loadValidation: async () => {
|
|
96
|
+
process.stdout.write('project import noise\n');
|
|
97
|
+
throw new Error('malformed project import');
|
|
98
|
+
},
|
|
99
|
+
writeStdout: (text: string) => {
|
|
100
|
+
stdout += text;
|
|
101
|
+
},
|
|
102
|
+
writeStderr: (text: string) => {
|
|
103
|
+
stderr += text;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(exitCode).toBe(2);
|
|
108
|
+
expect(stdout).not.toContain('project import noise');
|
|
109
|
+
expect(stdout.trim().split('\n')).toHaveLength(1);
|
|
110
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
contractVersion: CURRENT_STEP_CONTRACT_VERSION,
|
|
113
|
+
valid: false,
|
|
114
|
+
diagnostics: [{
|
|
115
|
+
code: 'FG-CLIENT-001',
|
|
116
|
+
severity: 'error',
|
|
117
|
+
}],
|
|
118
|
+
});
|
|
119
|
+
expect(stderr).toContain('project import noise');
|
|
120
|
+
expect(stderr).toContain('malformed project import');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects unsupported arguments as a protocol error without running imports', async () => {
|
|
124
|
+
const cli = await loadCli();
|
|
125
|
+
let loaded = false;
|
|
126
|
+
let stdout = '';
|
|
127
|
+
let stderr = '';
|
|
128
|
+
const exitCode = await cli.runValidateFunnelCli({
|
|
129
|
+
args: ['--json', '--unknown'],
|
|
130
|
+
loadValidation: async () => {
|
|
131
|
+
loaded = true;
|
|
132
|
+
return { contractVersion: CURRENT_STEP_CONTRACT_VERSION, diagnostics: [] };
|
|
133
|
+
},
|
|
134
|
+
writeStdout: (text: string) => {
|
|
135
|
+
stdout += text;
|
|
136
|
+
},
|
|
137
|
+
writeStderr: (text: string) => {
|
|
138
|
+
stderr += text;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(exitCode).toBe(2);
|
|
143
|
+
expect(loaded).toBe(false);
|
|
144
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
contractVersion: CURRENT_STEP_CONTRACT_VERSION,
|
|
147
|
+
valid: false,
|
|
148
|
+
});
|
|
149
|
+
expect(stderr).toContain('Usage: validate-funnel.cli.ts [--json]');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('runs through vite-node with one clean stdout protocol object', async () => {
|
|
153
|
+
const templateRoot = path.resolve(fileURLToPath(new URL('../..', import.meta.url)));
|
|
154
|
+
const viteNode = path.resolve(templateRoot, '../../node_modules/.bin/vite-node');
|
|
155
|
+
const script = path.resolve(templateRoot, 'src/contract/validate-funnel.cli.ts');
|
|
156
|
+
|
|
157
|
+
const result = await execFileAsync(viteNode, [
|
|
158
|
+
'--config',
|
|
159
|
+
'src/contract/funnel-validator.vite.config.ts',
|
|
160
|
+
script,
|
|
161
|
+
'--json',
|
|
162
|
+
], {
|
|
163
|
+
cwd: templateRoot,
|
|
164
|
+
env: {
|
|
165
|
+
...process.env,
|
|
166
|
+
VITE_CJS_IGNORE_WARNING: 'true',
|
|
167
|
+
},
|
|
168
|
+
timeout: 20_000,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.stderr).toBe('');
|
|
172
|
+
expect(result.stdout.trim().split('\n')).toHaveLength(1);
|
|
173
|
+
expect(JSON.parse(result.stdout)).toEqual({
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
contractVersion: CURRENT_STEP_CONTRACT_VERSION,
|
|
176
|
+
valid: true,
|
|
177
|
+
diagnostics: [],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import {
|
|
4
|
+
CURRENT_STEP_CONTRACT_VERSION,
|
|
5
|
+
type FunnelContractDiagnostic,
|
|
6
|
+
} from '@funnelsgrove/runtime';
|
|
7
|
+
|
|
8
|
+
export const VALIDATOR_CONTRACT_VERSION = CURRENT_STEP_CONTRACT_VERSION;
|
|
9
|
+
|
|
10
|
+
export type FunnelValidationProtocol = {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
contractVersion: number;
|
|
13
|
+
valid: boolean;
|
|
14
|
+
diagnostics: readonly FunnelContractDiagnostic[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type LoadedValidation = {
|
|
18
|
+
contractVersion: number;
|
|
19
|
+
diagnostics: readonly FunnelContractDiagnostic[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type TextWriter = (text: string) => void;
|
|
23
|
+
|
|
24
|
+
export type RunValidateFunnelCliOptions = {
|
|
25
|
+
args?: readonly string[];
|
|
26
|
+
loadValidation?: () => Promise<LoadedValidation>;
|
|
27
|
+
writeStdout?: TextWriter;
|
|
28
|
+
writeStderr?: TextWriter;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FunnelValidationExitCode = 0 | 1 | 2;
|
|
32
|
+
|
|
33
|
+
const internalDiagnostic = (): FunnelContractDiagnostic => ({
|
|
34
|
+
code: 'FG-CLIENT-001',
|
|
35
|
+
severity: 'error',
|
|
36
|
+
file: 'src/contract/validate-funnel.cli.ts',
|
|
37
|
+
stepId: null,
|
|
38
|
+
expected: 'loadable funnel project validation module',
|
|
39
|
+
received: 'internal validator failure',
|
|
40
|
+
reason: 'The funnel project validator could not load or run.',
|
|
41
|
+
guide: 'docs/funnelsgrove/qa/local.md',
|
|
42
|
+
repair: 'Fix project import errors, then rerun fgrove validate.',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const loadProjectValidation = async (): Promise<LoadedValidation> => {
|
|
46
|
+
const projectValidation = await import('./funnel-project.validation');
|
|
47
|
+
return {
|
|
48
|
+
contractVersion: projectValidation.FUNNEL_PROJECT_CONTRACT_VERSION,
|
|
49
|
+
diagnostics: projectValidation.validateFunnelProject(),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const describeUnknownError = (error: unknown): string => {
|
|
54
|
+
try {
|
|
55
|
+
if (error instanceof Error && error.message.trim()) {
|
|
56
|
+
return error.message;
|
|
57
|
+
}
|
|
58
|
+
return String(error);
|
|
59
|
+
} catch {
|
|
60
|
+
return 'unknown validator failure';
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const renderValue = (value: unknown): string => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(value) ?? String(value);
|
|
67
|
+
} catch {
|
|
68
|
+
return '[unserializable]';
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const renderDiagnostic = (diagnostic: FunnelContractDiagnostic): string => {
|
|
73
|
+
const location = diagnostic.file
|
|
74
|
+
? `${diagnostic.file}${diagnostic.stepId ? ` (${diagnostic.stepId})` : ''}`
|
|
75
|
+
: diagnostic.stepId ?? 'project';
|
|
76
|
+
return [
|
|
77
|
+
`[${diagnostic.code}] ${diagnostic.reason}`,
|
|
78
|
+
` Location: ${location}`,
|
|
79
|
+
` Expected: ${renderValue(diagnostic.expected)}`,
|
|
80
|
+
` Received: ${renderValue(diagnostic.received)}`,
|
|
81
|
+
` Repair: ${diagnostic.repair}`,
|
|
82
|
+
` Guide: ${diagnostic.guide}`,
|
|
83
|
+
].join('\n');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const invokeWithoutStdoutNoise = async <T>(
|
|
87
|
+
operation: () => Promise<T>,
|
|
88
|
+
writeStderr: TextWriter,
|
|
89
|
+
): Promise<T> => {
|
|
90
|
+
const originalWrite = process.stdout.write;
|
|
91
|
+
let captured = '';
|
|
92
|
+
process.stdout.write = ((chunk: string | Uint8Array, ...args: unknown[]) => {
|
|
93
|
+
const encoding = typeof args[0] === 'string' ? args[0] as BufferEncoding : undefined;
|
|
94
|
+
captured += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString(encoding);
|
|
95
|
+
const callback = args.find((arg) => typeof arg === 'function');
|
|
96
|
+
if (typeof callback === 'function') {
|
|
97
|
+
callback();
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}) as typeof process.stdout.write;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return await operation();
|
|
104
|
+
} finally {
|
|
105
|
+
process.stdout.write = originalWrite;
|
|
106
|
+
if (captured) {
|
|
107
|
+
writeStderr(captured);
|
|
108
|
+
if (!captured.endsWith('\n')) {
|
|
109
|
+
writeStderr('\n');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const isLoadedValidation = (value: LoadedValidation): boolean => {
|
|
116
|
+
return value.contractVersion === VALIDATOR_CONTRACT_VERSION
|
|
117
|
+
&& Array.isArray(value.diagnostics);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const emitProtocol = (
|
|
121
|
+
protocol: FunnelValidationProtocol,
|
|
122
|
+
writeStdout: TextWriter,
|
|
123
|
+
): void => {
|
|
124
|
+
writeStdout(`${JSON.stringify(protocol)}\n`);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const protocolFor = (
|
|
128
|
+
diagnostics: readonly FunnelContractDiagnostic[],
|
|
129
|
+
): FunnelValidationProtocol => ({
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
contractVersion: VALIDATOR_CONTRACT_VERSION,
|
|
132
|
+
valid: diagnostics.length === 0,
|
|
133
|
+
diagnostics,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const usageError = (
|
|
137
|
+
json: boolean,
|
|
138
|
+
writeStdout: TextWriter,
|
|
139
|
+
writeStderr: TextWriter,
|
|
140
|
+
): FunnelValidationExitCode => {
|
|
141
|
+
const diagnostic = internalDiagnostic();
|
|
142
|
+
writeStderr('Usage: validate-funnel.cli.ts [--json]\n');
|
|
143
|
+
if (json) {
|
|
144
|
+
emitProtocol(protocolFor([diagnostic]), writeStdout);
|
|
145
|
+
}
|
|
146
|
+
return 2;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const runValidateFunnelCli = async (
|
|
150
|
+
options: RunValidateFunnelCliOptions = {},
|
|
151
|
+
): Promise<FunnelValidationExitCode> => {
|
|
152
|
+
const args = options.args ?? process.argv.slice(2);
|
|
153
|
+
const writeStdout = options.writeStdout ?? ((text) => {
|
|
154
|
+
process.stdout.write(text);
|
|
155
|
+
});
|
|
156
|
+
const writeStderr = options.writeStderr ?? ((text) => {
|
|
157
|
+
process.stderr.write(text);
|
|
158
|
+
});
|
|
159
|
+
const json = args.includes('--json');
|
|
160
|
+
if (args.some((arg) => arg !== '--json') || args.filter((arg) => arg === '--json').length > 1) {
|
|
161
|
+
return usageError(json, writeStdout, writeStderr);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const loaded = await invokeWithoutStdoutNoise(
|
|
166
|
+
options.loadValidation ?? loadProjectValidation,
|
|
167
|
+
writeStderr,
|
|
168
|
+
);
|
|
169
|
+
if (!isLoadedValidation(loaded)) {
|
|
170
|
+
throw new Error('The project validator returned an invalid protocol result.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const protocol = protocolFor(loaded.diagnostics);
|
|
174
|
+
if (json) {
|
|
175
|
+
emitProtocol(protocol, writeStdout);
|
|
176
|
+
}
|
|
177
|
+
for (const diagnostic of loaded.diagnostics) {
|
|
178
|
+
writeStderr(`${renderDiagnostic(diagnostic)}\n`);
|
|
179
|
+
}
|
|
180
|
+
return protocol.valid ? 0 : 1;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const diagnostic = internalDiagnostic();
|
|
183
|
+
writeStderr(`Funnel validation failed internally: ${describeUnknownError(error)}\n`);
|
|
184
|
+
if (json) {
|
|
185
|
+
emitProtocol(protocolFor([diagnostic]), writeStdout);
|
|
186
|
+
}
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const isCliEntrypoint = (argv: readonly string[], modulePath: string): boolean => {
|
|
192
|
+
const resolvedModulePath = path.resolve(modulePath);
|
|
193
|
+
return path.basename(argv[1] ?? '') === 'vite-node'
|
|
194
|
+
|| argv.slice(1).some((argument) => path.resolve(argument) === resolvedModulePath);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (isCliEntrypoint(process.argv, fileURLToPath(import.meta.url))) {
|
|
198
|
+
process.exitCode = await runValidateFunnelCli();
|
|
199
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
4
|
+
|
|
5
|
+
const resetClaimbeeCheckoutEnv = () => {
|
|
6
|
+
process.env = { ...ORIGINAL_ENV };
|
|
7
|
+
delete process.env.NEXT_PUBLIC_FUNNEL_PUBLISHABLE_KEY;
|
|
8
|
+
delete process.env.NEXT_PUBLIC_CLAIMBEE_TEST_STRIPE_PUBLISHABLE_KEY;
|
|
9
|
+
delete process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('checkoutRuntimeConfig', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.resetModules();
|
|
15
|
+
resetClaimbeeCheckoutEnv();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.resetModules();
|
|
20
|
+
process.env = { ...ORIGINAL_ENV };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does not invent a checkout SDK publishable key from generic Stripe env', async () => {
|
|
24
|
+
process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY = 'pk_test_generic_runtime_value';
|
|
25
|
+
|
|
26
|
+
const { checkoutRuntimeConfig } = await import('./checkout-runtime-config');
|
|
27
|
+
|
|
28
|
+
expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('prefers the funnel sdk publishable key when the project runtime provides one', async () => {
|
|
32
|
+
vi.doMock('@funnelsgrove/runtime', async () => {
|
|
33
|
+
const actual = await vi.importActual<typeof import('@funnelsgrove/runtime')>(
|
|
34
|
+
'@funnelsgrove/runtime',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
runtimePublicConfig: {
|
|
40
|
+
...actual.runtimePublicConfig,
|
|
41
|
+
funnelSdkPublishableKey: 'sdk_pub_claimbee_project',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const { checkoutRuntimeConfig, resolveClaimbeeCheckoutMode } = await import('./checkout-runtime-config');
|
|
47
|
+
|
|
48
|
+
expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBe(
|
|
49
|
+
'sdk_pub_claimbee_project',
|
|
50
|
+
);
|
|
51
|
+
expect(resolveClaimbeeCheckoutMode('live')).toBe('live');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses the server-provided runtime mode when no preview key is injected', async () => {
|
|
55
|
+
const { resolveClaimbeeCheckoutMode } = await import('./checkout-runtime-config');
|
|
56
|
+
|
|
57
|
+
expect(resolveClaimbeeCheckoutMode('live')).toBe('live');
|
|
58
|
+
expect(resolveClaimbeeCheckoutMode('test')).toBe('test');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolvePreviewPaymentPublishableKey,
|
|
3
|
+
runtimePublicConfig,
|
|
4
|
+
type RuntimeMode,
|
|
5
|
+
} from '@funnelsgrove/runtime';
|
|
6
|
+
|
|
7
|
+
const resolveCheckoutFunnelSdkPublishableKey = (): string | null => {
|
|
8
|
+
const trimmedCandidateKey = runtimePublicConfig.funnelSdkPublishableKey.trim();
|
|
9
|
+
if (trimmedCandidateKey) {
|
|
10
|
+
return resolvePreviewPaymentPublishableKey(trimmedCandidateKey) || null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const checkoutRuntimeConfig = {
|
|
17
|
+
apiBaseUrl: runtimePublicConfig.apiBaseUrl.trim() || null,
|
|
18
|
+
funnelId: runtimePublicConfig.funnelId.trim() || null,
|
|
19
|
+
funnelSdkPublishableKey: resolveCheckoutFunnelSdkPublishableKey(),
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export const resolveClaimbeeCheckoutMode = (
|
|
23
|
+
runtimeMode: RuntimeMode,
|
|
24
|
+
): RuntimeMode => {
|
|
25
|
+
return runtimeMode;
|
|
26
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { funnelManifest } from '@/config/funnel.manifest';
|
|
3
|
+
import {
|
|
4
|
+
defaultFunnelStepId,
|
|
5
|
+
resolveConfiguredNextStep,
|
|
6
|
+
resolveRuntimeInitialStepId,
|
|
7
|
+
} from './funnel-runtime';
|
|
8
|
+
|
|
9
|
+
describe('funnel runtime', () => {
|
|
10
|
+
it('falls back to the first manifest step when no valid step is requested', () => {
|
|
11
|
+
expect(funnelManifest.steps[0]?.id).toBe(defaultFunnelStepId);
|
|
12
|
+
expect(resolveRuntimeInitialStepId({ requestedStepId: null })).toBe(defaultFunnelStepId);
|
|
13
|
+
expect(resolveRuntimeInitialStepId({ requestedStepId: 'missing-step' })).toBe(defaultFunnelStepId);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('resolves the normal edge after an experiment source route is active', () => {
|
|
17
|
+
expect(
|
|
18
|
+
resolveConfiguredNextStep('paywall', {
|
|
19
|
+
attributes: {},
|
|
20
|
+
ignoreExperiment: true,
|
|
21
|
+
resolveExperimentVariantKey: () => 'control',
|
|
22
|
+
}),
|
|
23
|
+
).toBe('subscription-started');
|
|
24
|
+
});
|
|
25
|
+
});
|