Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/catalog/tests/catalog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ test.describe("catalog SPA", () => {
.first();
await profileCompleteRow.locator("summary").click();
await expect(profileCompleteRow.getByText("plan")).toBeVisible();
// The example journey wraps `profileComplete` with `defineTransition({ targets })`,
// so the harvester reports `targetsDeclared: true` and the UI shows a
// "declared" badge. Reassures the reader that the destination set is
// authoritative (not AST best-effort).
await expect(profileCompleteRow.getByTestId("declared-targets-badge")).toBeVisible();
// The destination chip's module name is itself a link to the module page.
await profileCompleteRow.getByRole("link", { name: "plan" }).click();
await expect(page).toHaveURL(/\/modules\/plan$/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@example-onboarding/billing-module": "workspace:*",
"@example-onboarding/plan-module": "workspace:*",
"@example-onboarding/profile-module": "workspace:*",
"@modular-react/journeys": "^0.1.0"
"@modular-react/journeys": "^1.0.0"
},
"devDependencies": {
"typescript": "^6.0.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineJourney, defineJourneyHandle } from "@modular-react/journeys";
import { defineJourney, defineJourneyHandle, defineTransition } from "@modular-react/journeys";
import type { PlanHint, SubscriptionPlan } from "@example-onboarding/app-shared";
import type profileModule from "@example-onboarding/profile-module";
import type planModule from "@example-onboarding/plan-module";
Expand Down Expand Up @@ -27,6 +27,14 @@ export interface OnboardingState {
| null;
}

// Bind `defineTransition` to the journey's modules + state once so every
// wrapped handler below gets contextual narrowing on `next.module` /
// `next.entry` and autocomplete on `targets`. Bare-function handlers stay
// fully supported — only the handlers that fan out to lazy steps need to
// migrate. Naming mirrors `selectModule`: a descriptive verb, not an
// abbreviation.
const transition = defineTransition<OnboardingModules, OnboardingState>();

export const customerOnboardingJourney = defineJourney<OnboardingModules, OnboardingState>()({
id: "customer-onboarding",
version: "1.0.0",
Expand Down Expand Up @@ -63,45 +71,69 @@ export const customerOnboardingJourney = defineJourney<OnboardingModules, Onboar
transitions: {
profile: {
review: {
profileComplete: ({ output, state }) => ({
state: { ...state, hint: output.hint },
next: {
module: "plan",
entry: "choose",
input: { customerId: state.customerId, hint: output.hint },
},
// `transition({ targets })` lets `<JourneyOutlet preload="precise">`
// (the default) speculatively warm the chunks for the steps this exit
// can advance into. With billing/collect now lazy-loaded, declaring
// the targets here ensures the chunk is hot before the rep clicks
// into it. Bare-function handlers below stay unchanged.
profileComplete: transition({
targets: [{ module: "plan", entry: "choose" }],
handle: ({ output, state }) => ({
state: { ...state, hint: output.hint },
next: {
module: "plan",
entry: "choose",
input: { customerId: state.customerId, hint: output.hint },
},
}),
}),
readyToBuy: ({ output }) => ({
next: {
module: "billing",
entry: "collect",
input: { customerId: output.customerId, amount: output.amount },
},
readyToBuy: transition({
targets: [{ module: "billing", entry: "collect" }],
handle: ({ output }) => ({
next: {
module: "billing",
entry: "collect",
input: { customerId: output.customerId, amount: output.amount },
},
}),
}),
needsMoreDetails: ({ output }) => ({
abort: { reason: "profile-incomplete", missing: output.missing },
}),
cancelled: () => ({ abort: { reason: "rep-cancelled" } }),
// Demonstrates a terminal-only annotation: `targets: ["abort"]`
// tells the catalog harvester this handler may abort (no AST walk
// needed) and constrains the handler return to just the abort arm
// at compile time.
cancelled: transition({
targets: ["abort"],
handle: () => ({ abort: { reason: "rep-cancelled" } }),
}),
},
},
plan: {
choose: {
allowBack: true,
choseStandard: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "collect",
input: { customerId: state.customerId, amount: output.plan.monthly },
},
choseStandard: transition({
targets: [{ module: "billing", entry: "collect" }],
handle: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "collect",
input: { customerId: state.customerId, amount: output.plan.monthly },
},
}),
}),
choseWithTrial: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "startTrial",
input: { customerId: state.customerId, plan: output.plan },
},
choseWithTrial: transition({
targets: [{ module: "billing", entry: "startTrial" }],
handle: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "startTrial",
input: { customerId: state.customerId, plan: output.plan },
},
}),
}),
noFit: ({ output }) => ({
abort: { reason: "plan-no-fit", detail: output.reason },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { defineEntry, defineModule, schema } from "@modular-react/core";
import { billingExits } from "./exits.js";
import { CollectPayment, type CollectPaymentInput } from "./CollectPayment.js";
import type { CollectPaymentInput } from "./CollectPayment.js";
import { StartTrial, type StartTrialInput } from "./StartTrial.js";

export { billingExits };
export type { BillingExits } from "./exits.js";
export type { CollectPaymentInput } from "./CollectPayment.js";

export default defineModule({
id: "billing",
Expand All @@ -15,8 +16,13 @@ export default defineModule({
},
exitPoints: billingExits,
entryPoints: {
// Lazy-loaded — `CollectPayment` is only fetched when a journey actually
// reaches the `collect` step (or when an outlet preloads it during idle
// time, see the journey definition's `defineTransition({ targets })`
// annotations). Eliminates the bundle cost of the payment-collection
// surface for journeys that branch into `startTrial` instead.
collect: defineEntry({
component: CollectPayment,
lazy: () => import("./CollectPayment.js").then((m) => ({ default: m.CollectPayment })),
input: schema<CollectPaymentInput>(),
// Rollback: if the rep steps back from `collect`, the journey state
// reverts to the snapshot taken before entering it — any "paid"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@example-onboarding/plan-module": "workspace:*",
"@example-onboarding/profile-module": "workspace:*",
"@modular-react/core": "^1.0.0",
"@modular-react/journeys": "^0.1.0",
"@modular-react/journeys": "^1.0.0",
"@modular-react/react": "^1.0.0",
"@react-router-modules/core": "^2.0.0",
"@react-router-modules/runtime": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@example-rr-invoke/checkout-confirm-module": "workspace:*",
"@example-rr-invoke/checkout-review-module": "workspace:*",
"@example-rr-invoke/verify-identity-journey": "workspace:*",
"@modular-react/journeys": "^0.1.0"
"@modular-react/journeys": "^1.0.0"
},
"devDependencies": {
"typescript": "^6.0.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@example-rr-invoke/age-verify-module": "workspace:*",
"@example-rr-invoke/app-shared": "workspace:*",
"@modular-react/journeys": "^0.1.0"
"@modular-react/journeys": "^1.0.0"
},
"devDependencies": {
"typescript": "^6.0.2"
Expand Down
2 changes: 1 addition & 1 deletion examples/react-router/journey-invoke/shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@example-rr-invoke/checkout-review-module": "workspace:*",
"@example-rr-invoke/verify-identity-journey": "workspace:*",
"@modular-react/core": "^1.0.0",
"@modular-react/journeys": "^0.1.0",
"@modular-react/journeys": "^1.0.0",
"@modular-react/react": "^1.0.0",
"@react-router-modules/core": "^2.0.0",
"@react-router-modules/runtime": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@example-tsr-onboarding/billing-module": "workspace:*",
"@example-tsr-onboarding/plan-module": "workspace:*",
"@example-tsr-onboarding/profile-module": "workspace:*",
"@modular-react/journeys": "^0.1.0"
"@modular-react/journeys": "^1.0.0"
},
"devDependencies": {
"typescript": "^6.0.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineJourney, defineJourneyHandle } from "@modular-react/journeys";
import { defineJourney, defineJourneyHandle, defineTransition } from "@modular-react/journeys";
import type { PlanHint, SubscriptionPlan } from "@example-tsr-onboarding/app-shared";
import type profileModule from "@example-tsr-onboarding/profile-module";
import type planModule from "@example-tsr-onboarding/plan-module";
Expand Down Expand Up @@ -27,6 +27,13 @@ export interface OnboardingState {
| null;
}

// Bind `defineTransition` to the journey's modules + state once so every
// wrapped handler below gets contextual narrowing on `next.module` /
// `next.entry` and autocomplete on `targets`. Bare-function handlers stay
// fully supported — only the handlers that fan out to lazy steps need to
// migrate.
const transition = defineTransition<OnboardingModules, OnboardingState>();

export const customerOnboardingJourney = defineJourney<OnboardingModules, OnboardingState>()({
id: "customer-onboarding",
version: "1.0.0",
Expand Down Expand Up @@ -67,45 +74,69 @@ export const customerOnboardingJourney = defineJourney<OnboardingModules, Onboar
transitions: {
profile: {
review: {
profileComplete: ({ output, state }) => ({
state: { ...state, hint: output.hint },
next: {
module: "plan",
entry: "choose",
input: { customerId: state.customerId, hint: output.hint },
},
// `transition({ targets })` lets `<JourneyOutlet preload="precise">`
// (the default) speculatively warm the chunks for the steps this exit
// can advance into. With billing/collect now lazy-loaded, declaring
// the targets here ensures the chunk is hot before the rep clicks
// into it. Bare-function handlers below stay unchanged.
profileComplete: transition({
targets: [{ module: "plan", entry: "choose" }],
handle: ({ output, state }) => ({
state: { ...state, hint: output.hint },
next: {
module: "plan",
entry: "choose",
input: { customerId: state.customerId, hint: output.hint },
},
}),
}),
readyToBuy: ({ output }) => ({
next: {
module: "billing",
entry: "collect",
input: { customerId: output.customerId, amount: output.amount },
},
readyToBuy: transition({
targets: [{ module: "billing", entry: "collect" }],
handle: ({ output }) => ({
next: {
module: "billing",
entry: "collect",
input: { customerId: output.customerId, amount: output.amount },
},
}),
}),
needsMoreDetails: ({ output }) => ({
abort: { reason: "profile-incomplete", missing: output.missing },
}),
cancelled: () => ({ abort: { reason: "rep-cancelled" } }),
// Demonstrates a terminal-only annotation: `targets: ["abort"]`
// tells the catalog harvester this handler may abort (no AST walk
// needed) and constrains the handler return to just the abort arm
// at compile time.
cancelled: transition({
targets: ["abort"],
handle: () => ({ abort: { reason: "rep-cancelled" } }),
}),
},
},
plan: {
choose: {
allowBack: true,
choseStandard: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "collect",
input: { customerId: state.customerId, amount: output.plan.monthly },
},
choseStandard: transition({
targets: [{ module: "billing", entry: "collect" }],
handle: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "collect",
input: { customerId: state.customerId, amount: output.plan.monthly },
},
}),
}),
choseWithTrial: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "startTrial",
input: { customerId: state.customerId, plan: output.plan },
},
choseWithTrial: transition({
targets: [{ module: "billing", entry: "startTrial" }],
handle: ({ output, state }) => ({
state: { ...state, selectedPlan: output.plan },
next: {
module: "billing",
entry: "startTrial",
input: { customerId: state.customerId, plan: output.plan },
},
}),
}),
noFit: ({ output }) => ({
abort: { reason: "plan-no-fit", detail: output.reason },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { defineEntry, defineModule, schema } from "@modular-react/core";
import { billingExits } from "./exits.js";
import { CollectPayment, type CollectPaymentInput } from "./CollectPayment.js";
import type { CollectPaymentInput } from "./CollectPayment.js";
import { StartTrial, type StartTrialInput } from "./StartTrial.js";

export { billingExits };
export type { BillingExits } from "./exits.js";
export type { CollectPaymentInput } from "./CollectPayment.js";

export default defineModule({
id: "billing",
Expand All @@ -19,8 +20,13 @@ export default defineModule({
},
exitPoints: billingExits,
entryPoints: {
// Lazy-loaded — `CollectPayment` is only fetched when a journey actually
// reaches the `collect` step (or when an outlet preloads it during idle
// time, see the journey definition's `defineTransition({ targets })`
// annotations). Eliminates the bundle cost of the payment-collection
// surface for journeys that branch into `startTrial` instead.
collect: defineEntry({
component: CollectPayment,
lazy: () => import("./CollectPayment.js").then((m) => ({ default: m.CollectPayment })),
input: schema<CollectPaymentInput>(),
// Rollback: if the rep steps back from `collect`, the journey state
// reverts to the snapshot taken before entering it — any "paid"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@example-tsr-onboarding/plan-module": "workspace:*",
"@example-tsr-onboarding/profile-module": "workspace:*",
"@modular-react/core": "^1.0.0",
"@modular-react/journeys": "^0.1.0",
"@modular-react/journeys": "^1.0.0",
"@modular-react/react": "^1.0.0",
"@tanstack-react-modules/core": "^2.0.0",
"@tanstack-react-modules/runtime": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@example-tsr-invoke/checkout-confirm-module": "workspace:*",
"@example-tsr-invoke/checkout-review-module": "workspace:*",
"@example-tsr-invoke/verify-identity-journey": "workspace:*",
"@modular-react/journeys": "^0.1.0"
"@modular-react/journeys": "^1.0.0"
},
"devDependencies": {
"typescript": "^6.0.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@example-tsr-invoke/age-verify-module": "workspace:*",
"@example-tsr-invoke/app-shared": "workspace:*",
"@modular-react/journeys": "^0.1.0"
"@modular-react/journeys": "^1.0.0"
},
"devDependencies": {
"typescript": "^6.0.2"
Expand Down
2 changes: 1 addition & 1 deletion examples/tanstack-router/journey-invoke/shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@example-tsr-invoke/checkout-review-module": "workspace:*",
"@example-tsr-invoke/verify-identity-journey": "workspace:*",
"@modular-react/core": "^1.0.0",
"@modular-react/journeys": "^0.1.0",
"@modular-react/journeys": "^1.0.0",
"@modular-react/react": "^1.0.0",
"@tanstack-react-modules/core": "^2.0.0",
"@tanstack-react-modules/runtime": "^2.0.0",
Expand Down
Loading
Loading