Skip to content
Open
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
15 changes: 0 additions & 15 deletions .changeset/coerce-activity-params-to-string.md

This file was deleted.

12 changes: 0 additions & 12 deletions .changeset/step-context-path.md

This file was deleted.

12 changes: 12 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @stackflow/core

## 1.4.0

### Minor Changes

- cef9c62: Add optional `stepContext.path?: string` to `StepPushedEvent` and `StepReplacedEvent` (purely additive, no breaking change). `@stackflow/plugin-history-sync` uses this to preserve `encode`-output URLs through the store across every step navigation path — including `popstate` forward across step boundaries — instead of relying on plugin-internal state.

This addresses three regressions surfaced in PR review:

1. **`encode` output not in `history.location`** — post-effect hooks (`onPushed` / `onReplaced` / `onStepPushed` / `onStepReplaced` / `onInit`) called `template.fillWithoutEncode(activity.params)` against the post-coercion strings, skipping `encode` and writing coerced values into the URL. Now they read the encoded URL pre-computed in pre-effect hooks (`activityContext.path` / `stepContext.path`), with `fillWithoutEncode` as a defensive fallback only.
2. **`encode` called with coerced strings on popstate forward re-push** — the popstate `isForward` and `isStepForward` branches reconstructed push events without preserving `activityContext` / `stepContext`, causing `onBeforePush` / `onBeforeStepPush` to call `template.fill` with already-coerced strings. Now those branches pass `activityContext: targetActivity.context` / `stepContext: targetStep.context`, and the pre-effect hooks short-circuit when the path is already present (`"path" in actionParams.activityContext`).
3. **Test gap: `path(history.location)` was never asserted under non-identity `encode`** — every existing test asserted `activity.context.path` only. Added 15 new tests asserting the URL surface under non-identity encode, including popstate-forward across activity AND step boundaries, `defaultHistory` ancestor URLs, SSR replay, and `replace`-with-active-steps.

## 1.3.2

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stackflow/core",
"version": "1.3.2",
"version": "1.4.0",
"repository": {
"type": "git",
"url": "https://github.com/daangn/stackflow.git",
Expand Down
33 changes: 33 additions & 0 deletions docs/components/ChangelogContent.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
## 2026.05.08

Coerce activity/step params to `string | undefined` at the plugin boundary. [`cef9c62`](https://github.com/daangn/stackflow/commit/cef9c62700eeda32fa7698b991281e8506df670e)

Before this change, `push("X", { visible: true })` would store the boolean `true` in the core store while URL-arrival parsed the same URL as `{ visible: "true" }`, so `useActivityParams`<K>`()` returned different runtime types depending on how the user reached the activity. This PR coerces non-string values to strings inside `plugin-history-sync`'s `onBeforePush` / `onBeforeReplace` / `onBeforeStepPush` / `onBeforeStepReplace` hooks (after `encode` consumes the typed params to build the URL), and on the `decode`-path in `overrideInitialEvents`, so the core store always contains `{ [key: string]: string | undefined }`. `encode` still receives the typed params `U` from `template.fill`. Post-effect hooks (`onPushed`, `onReplaced`, `onStepPushed`, `onStepReplaced`, `onInit`) now use the new `fillWithoutEncode` to avoid re-running `encode` on already-coerced store values.

This is a behavioral change for consumers that relied on internal push preserving non-string values in the store (a pre-existing divergence from URL-arrival behavior). See the docs update for the migration note.

Migration notes:

- If you authored a `decode` hook that returns typed values (e.g. `decode: (p) => ({ count: Number(p.count) })`), those return values are now coerced back to strings in the store to match the declared `ActivityBaseParams` contract. Move runtime type coercion to the usage site (`Number(useActivityParams().count)`).
- If your app registers a plugin AFTER `historySyncPlugin` in the plugins array and that plugin re-injects typed values via `overrideActionParams`, those values will NOT be coerced by this plugin. Register `historySyncPlugin` last among plugins that mutate `activityParams` to preserve the string-only invariant.
- Cross-deploy hydration: when a user reloads on a deploy that includes this fix after a previous deploy serialized typed values into `history.state`, the params are coerced to strings at hydration time inside the `parseState` early-return. No consumer change required — the post-fix runtime contract (`useActivityParams()` returns `string | undefined`) holds across version boundaries.

Released packages:
- 📦 [@stackflow/plugin-history-sync@1.11.0](https://npmjs.com/package/@stackflow/plugin-history-sync/v/1.11.0)

---

Add optional `stepContext.path?: string` to `StepPushedEvent` and `StepReplacedEvent` (purely additive, no breaking change). `@stackflow/plugin-history-sync` uses this to preserve `encode`-output URLs through the store across every step navigation path — including `popstate` forward across step boundaries — instead of relying on plugin-internal state. [`cef9c62`](https://github.com/daangn/stackflow/commit/cef9c62700eeda32fa7698b991281e8506df670e)

This addresses three regressions surfaced in PR review:

1. **`encode` output not in `history.location`** — post-effect hooks (`onPushed` / `onReplaced` / `onStepPushed` / `onStepReplaced` / `onInit`) called `template.fillWithoutEncode(activity.params)` against the post-coercion strings, skipping `encode` and writing coerced values into the URL. Now they read the encoded URL pre-computed in pre-effect hooks (`activityContext.path` / `stepContext.path`), with `fillWithoutEncode` as a defensive fallback only.
2. **`encode` called with coerced strings on popstate forward re-push** — the popstate `isForward` and `isStepForward` branches reconstructed push events without preserving `activityContext` / `stepContext`, causing `onBeforePush` / `onBeforeStepPush` to call `template.fill` with already-coerced strings. Now those branches pass `activityContext: targetActivity.context` / `stepContext: targetStep.context`, and the pre-effect hooks short-circuit when the path is already present (`"path" in actionParams.activityContext`).
3. **Test gap: `path(history.location)` was never asserted under non-identity `encode`** — every existing test asserted `activity.context.path` only. Added 15 new tests asserting the URL surface under non-identity encode, including popstate-forward across activity AND step boundaries, `defaultHistory` ancestor URLs, SSR replay, and `replace`-with-active-steps.

Released packages:
- 📦 [@stackflow/core@1.4.0](https://npmjs.com/package/@stackflow/core/v/1.4.0)
- 📦 [@stackflow/plugin-history-sync@1.11.0](https://npmjs.com/package/@stackflow/plugin-history-sync/v/1.11.0)

---

## 2026.04.30

Fix `fallbackActivity` callback being invoked on every initialization regardless of route matching outcome. Restored the pre-1.8.0 contract: the callback is now called only when no route matches `currentPath`. Apps that perform side effects in this callback (e.g. Sentry logging for unknown deep links) no longer fire on successful matches. [`2c5786a`](https://github.com/daangn/stackflow/commit/2c5786a2934c3d2b74c20e8c57465ae03b3d3416)
Expand Down
26 changes: 26 additions & 0 deletions extensions/plugin-history-sync/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# @stackflow/plugin-history-sync

## 1.11.0

### Minor Changes

- cef9c62: Coerce activity/step params to `string | undefined` at the plugin boundary.

Before this change, `push("X", { visible: true })` would store the boolean `true` in the core store while URL-arrival parsed the same URL as `{ visible: "true" }`, so `useActivityParams<K>()` returned different runtime types depending on how the user reached the activity. This PR coerces non-string values to strings inside `plugin-history-sync`'s `onBeforePush` / `onBeforeReplace` / `onBeforeStepPush` / `onBeforeStepReplace` hooks (after `encode` consumes the typed params to build the URL), and on the `decode`-path in `overrideInitialEvents`, so the core store always contains `{ [key: string]: string | undefined }`. `encode` still receives the typed params `U` from `template.fill`. Post-effect hooks (`onPushed`, `onReplaced`, `onStepPushed`, `onStepReplaced`, `onInit`) now use the new `fillWithoutEncode` to avoid re-running `encode` on already-coerced store values.

This is a behavioral change for consumers that relied on internal push preserving non-string values in the store (a pre-existing divergence from URL-arrival behavior). See the docs update for the migration note.

Migration notes:

- If you authored a `decode` hook that returns typed values (e.g. `decode: (p) => ({ count: Number(p.count) })`), those return values are now coerced back to strings in the store to match the declared `ActivityBaseParams` contract. Move runtime type coercion to the usage site (`Number(useActivityParams().count)`).
- If your app registers a plugin AFTER `historySyncPlugin` in the plugins array and that plugin re-injects typed values via `overrideActionParams`, those values will NOT be coerced by this plugin. Register `historySyncPlugin` last among plugins that mutate `activityParams` to preserve the string-only invariant.
- Cross-deploy hydration: when a user reloads on a deploy that includes this fix after a previous deploy serialized typed values into `history.state`, the params are coerced to strings at hydration time inside the `parseState` early-return. No consumer change required — the post-fix runtime contract (`useActivityParams()` returns `string | undefined`) holds across version boundaries.

### Patch Changes

- cef9c62: Add optional `stepContext.path?: string` to `StepPushedEvent` and `StepReplacedEvent` (purely additive, no breaking change). `@stackflow/plugin-history-sync` uses this to preserve `encode`-output URLs through the store across every step navigation path — including `popstate` forward across step boundaries — instead of relying on plugin-internal state.

This addresses three regressions surfaced in PR review:

1. **`encode` output not in `history.location`** — post-effect hooks (`onPushed` / `onReplaced` / `onStepPushed` / `onStepReplaced` / `onInit`) called `template.fillWithoutEncode(activity.params)` against the post-coercion strings, skipping `encode` and writing coerced values into the URL. Now they read the encoded URL pre-computed in pre-effect hooks (`activityContext.path` / `stepContext.path`), with `fillWithoutEncode` as a defensive fallback only.
2. **`encode` called with coerced strings on popstate forward re-push** — the popstate `isForward` and `isStepForward` branches reconstructed push events without preserving `activityContext` / `stepContext`, causing `onBeforePush` / `onBeforeStepPush` to call `template.fill` with already-coerced strings. Now those branches pass `activityContext: targetActivity.context` / `stepContext: targetStep.context`, and the pre-effect hooks short-circuit when the path is already present (`"path" in actionParams.activityContext`).
3. **Test gap: `path(history.location)` was never asserted under non-identity `encode`** — every existing test asserted `activity.context.path` only. Added 15 new tests asserting the URL surface under non-identity encode, including popstate-forward across activity AND step boundaries, `defaultHistory` ancestor URLs, SSR replay, and `replace`-with-active-steps.

## 1.10.1

### Patch Changes
Expand Down
4 changes: 2 additions & 2 deletions extensions/plugin-history-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stackflow/plugin-history-sync",
"version": "1.10.1",
"version": "1.11.0",
"repository": {
"type": "git",
"url": "https://github.com/daangn/stackflow.git",
Expand Down Expand Up @@ -49,7 +49,7 @@
"devDependencies": {
"@graphql-tools/schema": "^10.0.5",
"@stackflow/config": "^1.2.1",
"@stackflow/core": "^1.3.0",
"@stackflow/core": "^1.4.0",
"@stackflow/esbuild-config": "^1.0.3",
"@stackflow/react": "^1.7.0",
"@swc/core": "^1.6.6",
Expand Down
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5621,7 +5621,7 @@ __metadata:
languageName: unknown
linkType: soft

"@stackflow/core@npm:^1.1.0, @stackflow/core@npm:^1.1.1, @stackflow/core@npm:^1.2.0, @stackflow/core@npm:^1.3.0, @stackflow/core@npm:^1.3.1, @stackflow/core@workspace:core":
"@stackflow/core@npm:^1.1.0, @stackflow/core@npm:^1.1.1, @stackflow/core@npm:^1.2.0, @stackflow/core@npm:^1.3.0, @stackflow/core@npm:^1.3.1, @stackflow/core@npm:^1.4.0, @stackflow/core@workspace:core":
version: 0.0.0-use.local
resolution: "@stackflow/core@workspace:core"
dependencies:
Expand Down Expand Up @@ -5875,7 +5875,7 @@ __metadata:
dependencies:
"@graphql-tools/schema": "npm:^10.0.5"
"@stackflow/config": "npm:^1.2.1"
"@stackflow/core": "npm:^1.3.0"
"@stackflow/core": "npm:^1.4.0"
"@stackflow/esbuild-config": "npm:^1.0.3"
"@stackflow/react": "npm:^1.7.0"
"@swc/core": "npm:^1.6.6"
Expand Down
Loading