feat(react): support activity component/data preload outside React runtime context#716
Conversation
- Add Jest + @swc/jest + jsdom + Testing Library to integrations/react, following the inline-config convention of plugin-blocker/plugin-history-sync - Exclude *.spec.* files from esbuild/dts build output; add tsconfig.test.json so `yarn typecheck` covers spec files (incl. Register augmentations) - Type-only cast in PluginRenderer so library source stays type-checkable when specs augment the Register interface - Add a harness smoke spec using an inline renderer plugin (public render API) to avoid a workspace dependency cycle with plugin-renderer-basic Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FEP-2357-SPEC.md: Linear issue + locked interface design, plus spec-owner decisions (reject semantics, original-reason propagation, retry after failure; loader dedupe / chunk duplicate firing / atomicity left unspecified) - FEP-2357-TEST-PLAN.md: 32 given-when-then test items (A-G), approved by test reviewer after two review rounds Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Translates FEP-2357-TEST-PLAN.md items A2-A9, B1-B3, C1-C2, E1-E10 and
F1-F2 into Jest specs against the public entry (./index). `prepare` is
not implemented yet, so all 25 tests fail with "prepare is not a
function" — verified red for the right reason by temporarily wiring a
reference implementation (all green) and reverting it.
Register augmentation uses optional params only ({ id?: string });
registering required params breaks package-internal typecheck variance
in stackflow.tsx/useStepFlow.ts. Because Register merges globally,
every stackflow() call passes a complete components map via
baseComponents spread.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D1-D2 verify the current usePrepare behavior that the new prepare must match (spec §3 "thin wrapper"). These run against existing code and pass today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
G1-G4 are typecheck-only assertions placed in never-called function bodies (swc does not typecheck; runtime execution must be avoided). A1 lives here because Jest requires at least one test per spec file. Until prepare lands, `yarn workspace @stackflow/react typecheck` fails with TS2339 (Property 'prepare' does not exist on StackflowOutput) in both this file and prepare.spec.tsx — a single root cause. Verified that a typed reference implementation turns typecheck fully green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The smoke spec invited removal once real specs cover the same ground; prepare.spec.tsx/usePrepare.spec.tsx now exercise the same harness surface (spec pickup, @swc/jest, jsdom + Testing Library, workspace deps, inline renderer plugin). Keeping it would also break typecheck: Register augmentation merges package-wide, so its stackflow() call would need components for every Prepare* activity registered by the new specs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 2c19d56 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR extracts shared activity preparation logic into a reusable ChangesPrepare API implementation and integration
🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
@stackflow/link
@stackflow/plugin-basic-ui
@stackflow/plugin-blocker
@stackflow/plugin-google-analytics-4
@stackflow/plugin-history-sync
@stackflow/plugin-lifecycle
@stackflow/plugin-renderer-basic
@stackflow/plugin-renderer-web
@stackflow/react-ui-core
@stackflow/react
commit: |
Deploying stackflow-demo with
|
| Latest commit: |
f70e16e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6d3065fa.stackflow-demo.pages.dev |
| Branch Preview URL: | https://feature-fep-2357.stackflow-demo.pages.dev |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
stackflow-docs | f70e16e | Commit Preview URL | Jun 08 2026, 01:31 PM |
…an/spec docs Working code is the source of truth: test-case rationale now lives as self-contained comments in the spec files (contract summary + unspecified- behavior guardrails in headers, given/when/then per test), with provenance pointing to Linear FEP-2357. Process artifacts (harness setup, fixture sketches, self-audit checklists) belong in the PR/Linear, not the repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ained The A1-G4 numbering was an artifact of the deleted test plan — an index- less numbering scheme that rots on insertion. Test titles are the identifiers now; cross-references are descriptive; "unspecified in spec" phrasing becomes "not a contract" so comments presume no external doc. Issue provenance stays in commit messages (FEP-2357) per repo convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The compile-time guarantees follow from reusing the existing Prepare type (RegisteredActivityName / InferActivityParams generics already exercised across the codebase), and the output-shape runtime check was redundant — every runtime spec destructures and calls prepare anyway. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reuses the public Prepare type so the compile-time contract (activity name / params inference) is fixed ahead of implementation and the spec files typecheck green. The stub throws explicitly, keeping the runtime specs red with a uniform, unambiguous reason until a worker fills in the implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
stackflow() owns the prepare contract; usePrepare is the wrapper. The type now lives in Prepare.ts (same pattern as Actions/StepActions) and both sides reference it, instead of the core importing it from the hook. Also drop the implementation-guidance comment from the stub — the specs alone define what to build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ontracts The "separation of responsibility" header line now says what it means: prepare only fires work; loaderData injection and lazy rendering stay with the existing navigation path (prepare → push behaves like plain push). The core-store-non-touching line is rephrased as the observable capability it grants: prepare is usable before the core store initializes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the usePrepare body into a context-free `makePrepare` factory that takes the three stackflow() inputs it actually depends on (config, loadData, activityComponentMap) and returns the `Prepare` function. Wire it as the `prepare` field of the stackflow() output so chunk/data preloading can run before the React tree mounts, and refactor `usePrepare` into a thin wrapper over the same implementation so in-tree callers are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the Jest test facility and the prepare/usePrepare spec suites that were set up for FEP-2357, returning all test-only infrastructure to its main-branch state: - delete `prepare.spec.tsx`, `usePrepare.spec.tsx`, and `tsconfig.test.json` - restore `package.json` (remove the `test` script, jest config, and test devDependencies; revert `typecheck` to `tsc --noEmit`) - restore `esbuild.config.js` (revert the `*.spec.*` entry-point exclusion to the plain `./src/**/*` glob) and `tsconfig.json` (drop the spec excludes) - restore `PluginRenderer.tsx` (the spec-motivated `RegisteredActivityName` cast is no longer needed) - restore `yarn.lock` / `.pnp.cjs` to drop the test dependencies The `prepare` implementation (makePrepare, the stackflow() wiring, the usePrepare wrapper, the Prepare type export) and its changeset are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
Preloading an activity's resources before entering it — the lazy component chunk and the data described by the activity's
loader— is currently only possible through theusePreparehook.Because
usePrepareis a React hook, it depends on React Context (useConfig,useDataLoader,useActivityComponentMap) and can therefore only be called inside a mounted React tree. This makes it impossible to warm up chunks/data at the moments that matter most for initial-entry performance:In practice, consumers who need render-ahead preloading have had to reach for private APIs (e.g. the lazy component's internal
_load), which is fragile and unsupported.Solution
Expose the same preload capability as a plain function on the
stackflow()output, alongside the existing render-outside APIs (actions,stepActions):Design decisions:
preparecomes from the samestackflow()call asStack/actions, so config and components never have to be passed twice and can never drift from the running instance. Since it does not touch the core store, it is usable from module-evaluation time — before<Stack>is mounted.Preparetype is reused as-is: omittingactivityParamspreloads only the component chunk; passing params also fires the activity's data loader. The returnedPromise<void>resolves when all fired preload work settles.preparedoes not store loader results; injectingloaderDatainto activities remains the loader plugin's responsibility.prepare, so transient failures don't poison the cache. Fire-and-forget callers should attach.catch.usePreparebecomes a thin wrapper over the same implementation, so existing in-tree callers keep working unchanged.RegisteredActivityName/InferActivityParams<K>flow through unchanged, so invalid activity names or params remain compile-time errors.🤖 Generated with Claude Code