Skip to content

refactor(journeys): extract framework-neutral @modular-frontend/journeys-engine - #55

Merged
kibertoad merged 3 commits into
mainfrom
refactor/journeys-engine
Jul 5, 2026
Merged

refactor(journeys): extract framework-neutral @modular-frontend/journeys-engine#55
kibertoad merged 3 commits into
mainfrom
refactor/journeys-engine

Conversation

@kibertoad

@kibertoad kibertoad commented Jul 5, 2026

Copy link
Copy Markdown
Owner

PR-02 of the Vue-support initiative. Splits the framework-neutral guts of @modular-react/journeys into a new @modular-frontend/journeys-engine package so a future Vue binding can reuse them, mirroring the @modular-frontend/core extraction in #54.

What moves

New package @modular-frontend/journeys-engine (v1.7.1, depends only on @modular-frontend/core; happy-dom is a dev dep for the storage-backed persistence tests). It holds the runtime, validation, persistence, authoring helpers (defineJourney / defineTransition / selectModule), handles, simulate-journey, the test harness, and the full type surface, plus their non-React tests. Two build entries: index + /testing.

@modular-react/journeys keeps its React files (outlet, module-tab, provider, plugin, instance-hooks, use-journey-state, use-wait-for-exit, mount-adapter) and re-exports the engine, so its public export surface is unchanged. A thin testing.ts re-exports the engine's /testing entry, keeping the @modular-react/journeys/testing import path working.

Deviations from the tracker's PR-02 plan

Both forced by the code, and recorded in the tracker:

  • mount-adapter.ts stays in the binding, not the engine: createJourneyMountAdapter supplies Outlet: JourneyOutlet (a React component), so it is binding-specific glue over the neutral RuntimeMountAdapter seam rather than engine logic.
  • JourneyNavContribution.icon used the React.ComponentType namespace; it moves to the neutral UiComponent seam, matching how NavigationItem.icon was neutralized in refactor(core): extract framework-neutral @modular-frontend/core #54. Source-compatible for authors (a React component still satisfies UiComponent).

Error-message prefixes stay [@modular-react/journeys] / [@modular-react/journeys/testing] on purpose: they name the package users import and point at real import paths, so the moved tests pass unmodified.

Verification

  • @modular-frontend/journeys-engine: 346 tests (26 files); @modular-react/journeys: 72 tests (8 files). 346 + 72 = the pre-split total.
  • @modular-react/compositions (the one createJourneyMountAdapter consumer): 115 tests pass.
  • Full workspace typecheck (110 tasks) and pnpm lint clean.
  • The only failing tests are the pre-existing Windows EPERM .test-output flake in the two router CLIs, unrelated to this change.

Also resolves decision D2 (engine scope = @modular-frontend) and marks PR-01 done (landed in #54) in the tracker.

Summary by CodeRabbit

  • New Features

    • Introduced a shared journeys engine package with a public API for journey authoring, runtime, validation, persistence, and test helpers.
    • Added a test harness for simulating and inspecting journey runtime behavior.
  • Documentation

    • Added package-level usage docs and updated the Vue support tracker with current progress and completed items.
  • Refactor

    • Consolidated journey-related imports to use the new shared package across the app and tests.
    • Updated build and test configuration to support the new package layout.

…eys-engine

PR-02 of the Vue-support initiative. Split the framework-neutral guts of
@modular-react/journeys into a new @modular-frontend/journeys-engine package so
a future Vue binding can reuse them, mirroring the @modular-frontend/core
extraction from #54.

- New packages/journeys-engine (@modular-frontend/journeys-engine): the runtime,
  validation, persistence, authoring helpers (defineJourney/defineTransition/
  selectModule), handles, simulate-journey, the test harness, and the full type
  surface, plus their non-React tests. Depends only on @modular-frontend/core
  (happy-dom is a dev dep for the storage-backed persistence tests). Two entries:
  index + /testing.
- @modular-react/journeys keeps its React files (outlet, module-tab, provider,
  plugin, instance-hooks, use-journey-state, use-wait-for-exit, mount-adapter)
  and re-exports the engine, so its public export surface is unchanged. A thin
  testing.ts re-exports the engine's /testing entry, keeping the existing
  @modular-react/journeys/testing import path working.

Two forced deviations from the tracker's PR-02 plan:

- mount-adapter.ts stays in the binding, not the engine: createJourneyMountAdapter
  supplies Outlet: JourneyOutlet (a React component), so it is binding-specific
  glue over the neutral RuntimeMountAdapter seam rather than engine logic.
- JourneyNavContribution.icon used the React.ComponentType namespace; it moves to
  the neutral UiComponent seam, matching how NavigationItem.icon was neutralized
  in #54. Source-compatible for authors (a React component still satisfies
  UiComponent).

Error-message prefixes stay [@modular-react/journeys] / [@modular-react/journeys/
testing] on purpose: they name the package users import and point at real import
paths, so the moved tests pass unmodified.

Verification: engine 346 tests (26 files) + journeys 72 tests (8 files) = the
pre-split total; @modular-react/compositions (the createJourneyMountAdapter
consumer) 115 tests pass; full workspace typecheck (110 tasks) and pnpm lint
clean. The only failing tests are the pre-existing Windows EPERM .test-output
flake in the two router CLIs, unrelated to this change.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kibertoad, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed27f10f-d5de-4058-b73a-38cb2ed9b812

📥 Commits

Reviewing files that changed from the base of the PR and between fc020dc and d24c763.

📒 Files selected for processing (4)
  • docs/vue-support-tracker.md
  • packages/journeys-engine/README.md
  • packages/journeys-engine/src/testing.ts
  • packages/journeys/src/index.ts
📝 Walkthrough

Walkthrough

This PR extracts a new framework-neutral @modular-frontend/journeys-engine package containing runtime, validation, persistence, authoring, handle, and testing modules previously local to packages/journeys. All engine source/test files switch imports from @modular-react/core to @modular-frontend/core. The journeys package is rewired to depend on and re-export from the new package, and its local test-harness implementation is replaced by a re-export. Documentation tracker is updated to reflect completion.

Changes

journeys-engine extraction and journeys package rewiring

Layer / File(s) Summary
Package scaffolding
packages/journeys-engine/package.json, README.md, tsconfig.json, vite.config.ts, vitest.config.ts
New package manifest, README, TS config, Vite library build config, and Vitest config for @modular-frontend/journeys-engine.
Public entrypoint
packages/journeys-engine/src/index.ts
Re-exports authoring, persistence, runtime, validation, handle, and type surface as the package's public API.
Core import migration
packages/journeys-engine/src/*.ts, *.test.ts, *.test-d.ts
All engine source and test files switch imports from @modular-react/core to @modular-frontend/core; JourneyNavContribution.icon type changes from React.ComponentType to UiComponent.
Test harness implementation
packages/journeys-engine/src/testing.ts
Adds createTestHarness with fireExit, goBack, goForward, inspect, plus InstanceSnapshot/JourneyTestHarness types.
journeys package rewiring
packages/journeys/package.json, src/index.ts, src/mount-adapter.ts, src/outlet*.tsx, src/plugin.tsx, src/provider*.tsx, src/testing.ts, src/use-journey-state.test.tsx, vite.config.ts
Adds workspace dependency and switches imports/re-exports and test wiring to the new @modular-frontend/journeys-engine package; local test harness replaced with re-export.
Documentation
docs/vue-support-tracker.md
Updates Phase 0 status, extraction table, decision D2, PR entries, and status board to reflect completed extraction.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant TestHarness
  participant JourneyRuntime
  participant RuntimeInternals

  Test->>TestHarness: fireExit(id, name, output)
  TestHarness->>RuntimeInternals: __getRecord(id)
  RuntimeInternals-->>TestHarness: instance record
  TestHarness->>RuntimeInternals: __getRegistered(journey)
  RuntimeInternals-->>TestHarness: registered journey
  TestHarness->>RuntimeInternals: __bindStepCallbacks(...).exit
  RuntimeInternals-->>JourneyRuntime: dispatch exit callback
Loading

Possibly related PRs

  • kibertoad/modular-react#21: Introduces the persistence adapter factories/types (createWebStoragePersistence, createMemoryPersistence) re-exported unchanged from @modular-frontend/journeys-engine in this PR.
  • kibertoad/modular-react#40: Introduces runtime.goForward/JourneyInstance.future wiring that the new createTestHarness(...).goForward(...) in this PR builds on.

Suggested labels: minor

Suggested reviewers: diogomiguel

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: extracting a framework-neutral journeys engine into @modular-frontend/journeys-engine.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/journeys-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/journeys/src/index.ts (1)

18-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale doc comment references removed local file.

The comment says getInternals is "still exported from ./runtime.js for internal use (the outlet, the test harness itself)", but runtime.ts has moved to @modular-frontend/journeys-engine and no longer exists in this package (confirmed by outlet.tsx now importing getInternals directly from @modular-frontend/journeys-engine). Update the comment to reference the new location to avoid confusing future readers.

📝 Proposed comment fix
 // `getInternals` intentionally omitted from the public surface — test code
 // that used to reach through it should migrate to `createTestHarness` in
-// `@modular-react/journeys/testing`. The symbol is still exported from
-// `./runtime.js` for internal use (the outlet, the test harness itself).
+// `@modular-react/journeys/testing`. The symbol is still exported from
+// `@modular-frontend/journeys-engine` for internal use (the outlet, the
+// test harness itself).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/journeys/src/index.ts` around lines 18 - 25, The doc comment above
the exports in `index.ts` still refers to `./runtime.js`, but that local module
no longer exists. Update the comment to point to
`@modular-frontend/journeys-engine` as the place where `getInternals` is now
available for internal use, keeping the note aligned with the current
`outlet.tsx` and `createJourneyRuntime` usage.
packages/journeys-engine/src/testing.ts (1)

60-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the repeated record/registration/loading guard into a shared helper.

fireExit, goBack, and goForward each re-implement the same recordOrThrow__getRegistered null-check → loading-status check sequence. Consolidating this into one internal helper (e.g., returning { record, reg } or throwing with a caller-supplied operation label) would cut duplication and centralize future guard changes.

♻️ Example consolidation
+  function activeRecordAndReg(id: InstanceId, opLabel: string) {
+    const record = recordOrThrow(id);
+    const reg = internals.__getRegistered(record.journeyId);
+    if (!reg) {
+      throw new Error(
+        `[`@modular-react/journeys/testing`] Journey "${record.journeyId}" is not registered with this runtime.`,
+      );
+    }
+    if (record.status === "loading") {
+      throw new Error(
+        `[`@modular-react/journeys/testing`] ${opLabel} called on instance "${id}" while status=loading. ` +
+          `Await the runtime's async load probe before dispatching.`,
+      );
+    }
+    return { record, reg };
+  }
+
   return {
     fireExit(id, name, output) {
-      const record = recordOrThrow(id);
-      const reg = internals.__getRegistered(record.journeyId);
-      if (!reg) {
-        throw new Error(
-          `[`@modular-react/journeys/testing`] Journey "${record.journeyId}" is not registered with this runtime.`,
-        );
-      }
-      if (record.status === "loading") {
-        throw new Error(
-          `[`@modular-react/journeys/testing`] fireExit("${name}") called on instance "${id}" while status=loading. ` +
-            `Await the runtime's async load probe (typically \`await Promise.resolve()\` a few times, or expose a subscribe hook in your test) before dispatching exits.`,
-        );
-      }
+      const { record, reg } = activeRecordAndReg(id, `fireExit("${name}")`);
       if (record.status !== "active") {
         throw new Error(
           `[`@modular-react/journeys/testing`] fireExit("${name}") called on terminal instance "${id}" (status=${record.status}).`,
         );
       }
       internals.__bindStepCallbacks(record, reg).exit(name, output);
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/journeys-engine/src/testing.ts` around lines 60 - 142, Repeated
record, registration, and loading checks are duplicated across fireExit, goBack,
and goForward; extract that shared validation into one internal helper.
Centralize the recordOrThrow call, __getRegistered lookup, and status=loading
guard in a helper that returns the validated record/reg pair or throws with a
caller-provided operation label, then have fireExit, goBack, and goForward use
it so future guard changes only happen in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/vue-support-tracker.md`:
- Around line 221-222: The PR-02 row in the status board is missing its pull
request reference even though it is marked done. Update the PR-02 entry in the
markdown table to include the PR link/reference, matching the convention used by
the PR-01 row and the board guidance, so the completed item is consistently
labeled in the tracker.
- Around line 39-46: The table currently reads as if both engine packages are
already completed, but `@modular-frontend/compositions-engine` is still planned;
update the wording in this section to clearly distinguish completed vs pending
extraction work. Adjust the table/header in the `Shared engine packages
extracted in Phase 0` block (or split into done and planned rows) so
`@modular-frontend/journeys-engine` and `@modular-frontend/compositions-engine`
reflect their actual status consistently.

In `@packages/journeys-engine/README.md`:
- Around line 22-23: The journeys-engine README currently lists
createJourneyMountAdapter as an exported mount adapter, but that symbol is not
shipped by this package. Remove the mount adapter bullet from the engine docs
and keep the remaining type-surface description intact, so the README only
advertises exports actually provided by journeys-engine.

---

Nitpick comments:
In `@packages/journeys-engine/src/testing.ts`:
- Around line 60-142: Repeated record, registration, and loading checks are
duplicated across fireExit, goBack, and goForward; extract that shared
validation into one internal helper. Centralize the recordOrThrow call,
__getRegistered lookup, and status=loading guard in a helper that returns the
validated record/reg pair or throws with a caller-provided operation label, then
have fireExit, goBack, and goForward use it so future guard changes only happen
in one place.

In `@packages/journeys/src/index.ts`:
- Around line 18-25: The doc comment above the exports in `index.ts` still
refers to `./runtime.js`, but that local module no longer exists. Update the
comment to point to `@modular-frontend/journeys-engine` as the place where
`getInternals` is now available for internal use, keeping the note aligned with
the current `outlet.tsx` and `createJourneyRuntime` usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3771a5a2-afff-4ec4-ba40-8cf4a23b33b9

📥 Commits

Reviewing files that changed from the base of the PR and between 7249c1e and fc020dc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (57)
  • docs/vue-support-tracker.md
  • packages/journeys-engine/README.md
  • packages/journeys-engine/package.json
  • packages/journeys-engine/src/build-input.test.ts
  • packages/journeys-engine/src/define-journey.ts
  • packages/journeys-engine/src/define-transition.test-d.ts
  • packages/journeys-engine/src/define-transition.test.ts
  • packages/journeys-engine/src/define-transition.ts
  • packages/journeys-engine/src/handle.test-d.ts
  • packages/journeys-engine/src/handle.test.ts
  • packages/journeys-engine/src/handle.ts
  • packages/journeys-engine/src/index.ts
  • packages/journeys-engine/src/invoke-cycle-safety.test.ts
  • packages/journeys-engine/src/invoke.test.ts
  • packages/journeys-engine/src/mount-kinds.test-d.ts
  • packages/journeys-engine/src/persistence.test-d.ts
  • packages/journeys-engine/src/persistence.test.ts
  • packages/journeys-engine/src/persistence.ts
  • packages/journeys-engine/src/register-options.test-d.ts
  • packages/journeys-engine/src/register-options.test.ts
  • packages/journeys-engine/src/runtime-go-back.test.ts
  • packages/journeys-engine/src/runtime-go-forward.test.ts
  • packages/journeys-engine/src/runtime-rewind-to.test.ts
  • packages/journeys-engine/src/runtime.test-d.ts
  • packages/journeys-engine/src/runtime.test.ts
  • packages/journeys-engine/src/runtime.ts
  • packages/journeys-engine/src/select-module.test-d.ts
  • packages/journeys-engine/src/select-module.test.ts
  • packages/journeys-engine/src/select-module.ts
  • packages/journeys-engine/src/simulate-journey-invoke.test.ts
  • packages/journeys-engine/src/simulate-journey.test-d.ts
  • packages/journeys-engine/src/simulate-journey.test.ts
  • packages/journeys-engine/src/simulate-journey.ts
  • packages/journeys-engine/src/testing.test.ts
  • packages/journeys-engine/src/testing.ts
  • packages/journeys-engine/src/types.ts
  • packages/journeys-engine/src/validation.test.ts
  • packages/journeys-engine/src/validation.ts
  • packages/journeys-engine/src/wildcard-transitions.test-d.ts
  • packages/journeys-engine/src/wildcard-transitions.test.ts
  • packages/journeys-engine/tsconfig.json
  • packages/journeys-engine/vite.config.ts
  • packages/journeys-engine/vitest.config.ts
  • packages/journeys/package.json
  • packages/journeys/src/index.ts
  • packages/journeys/src/mount-adapter.ts
  • packages/journeys/src/mount-kinds-runtime.test.tsx
  • packages/journeys/src/outlet-invoke.test.tsx
  • packages/journeys/src/outlet-preload.test.tsx
  • packages/journeys/src/outlet.test.tsx
  • packages/journeys/src/outlet.tsx
  • packages/journeys/src/plugin.tsx
  • packages/journeys/src/provider.test.tsx
  • packages/journeys/src/provider.tsx
  • packages/journeys/src/testing.ts
  • packages/journeys/src/use-journey-state.test.tsx
  • packages/journeys/vite.config.ts

Comment thread docs/vue-support-tracker.md Outdated
Comment thread docs/vue-support-tracker.md Outdated
Comment thread packages/journeys-engine/README.md Outdated
kibertoad added 2 commits July 5, 2026 23:55
createJourneyMountAdapter stays in the @modular-react/journeys binding (it
supplies the React JourneyOutlet), so the engine does not ship it. Listing it
under "What's included" advertised an export this package does not provide.
- vue-support-tracker: soften the shared-engines header so it no longer reads
  as if compositions-engine is already extracted, and add the #55 reference to
  the PR-02 status-board row.
- journeys/index.ts: point the getInternals note at @modular-frontend/journeys-engine;
  runtime.ts no longer lives in this package.
- journeys-engine/testing.ts: extract the repeated record + registration +
  loading guard shared by fireExit/goBack/goForward into one activeRecordAndReg
  helper.
@kibertoad

Copy link
Copy Markdown
Owner Author

Addressed the two nitpicks from the review as well (d24c763):

  • packages/journeys/src/index.ts: the getInternals note now points at @modular-frontend/journeys-engine instead of the stale ./runtime.js (that module moved out of this package).
  • packages/journeys-engine/src/testing.ts: extracted the repeated record + registration + loading guard shared by fireExit/goBack/goForward into one activeRecordAndReg(id, op) helper.

Verification after the changes: @modular-frontend/journeys-engine 346 tests and @modular-react/journeys 72 tests pass, typecheck clean for both.

@kibertoad
kibertoad merged commit 952475f into main Jul 5, 2026
14 of 15 checks passed
@kibertoad
kibertoad deleted the refactor/journeys-engine branch July 5, 2026 21:01
kibertoad added a commit that referenced this pull request Jul 6, 2026
…ompositions-engine (#56)

PR-03 of the Vue-support initiative. Splits the framework-neutral guts of
@modular-react/compositions into a new @modular-frontend/compositions-engine
package so a future Vue binding can reuse them, mirroring the journeys-engine
extraction (#55) and the frontend-core extraction (#54).

What moves: the pure files (runtime.ts, stores.ts, validation.ts,
define-composition.ts, types.ts) plus their non-.tsx tests. The engine depends
only on @modular-frontend/core; happy-dom is a dev dep for the test
environment. @modular-react/compositions keeps its React files (outlet.tsx,
provider.tsx, plugin.tsx, hooks.ts) and all .tsx tests, and re-exports the
engine so its public export surface is unchanged.

One React reference had to be neutralized, matching #54/PR-02:
CompositionZoneDescriptor.fallback used React.ComponentType; it moves to the
neutral UiComponent seam from @modular-frontend/core. Source-compatible for
authors (a React component still satisfies UiComponent). Error-message
prefixes stay [@modular-react/compositions] on purpose — they name the package
users import.

Test counts preserved: 52 in the engine + 63 in the binding = the pre-split
total of 115. Full workspace typecheck and lint clean.


Claude-Session: https://claude.ai/code/session_01Sf7deW3iRQaaSjZuj3akTA

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant