diff --git a/.changeset/clear-wasps-make.md b/.changeset/clear-wasps-make.md new file mode 100644 index 000000000..a2e9958b6 --- /dev/null +++ b/.changeset/clear-wasps-make.md @@ -0,0 +1,5 @@ +--- +"@stackflow/plugin-history-sync": patch +--- + +fix(plugin-history-sync): respect initial route pattern diff --git a/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts b/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts index 5ba28d372..25443645c 100644 --- a/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts +++ b/extensions/plugin-history-sync/src/historySyncPlugin.spec.ts @@ -1463,4 +1463,158 @@ describe("historySyncPlugin", () => { */ expect(queryResponse.data.hello).toEqual("world"); }); + + test("historySyncPlugin - uses the initially entered route pattern among multiple patterns", async () => { + history = createMemoryHistory({ + initialEntries: ["/foo"], + }); + + const coreStore = stackflow({ + activityNames: ["Home"], + plugins: [ + historySyncPlugin({ + history, + routes: { + Home: ["/", "/foo", "/bar", "/baz"], + }, + fallbackActivity: () => "Home", + }), + ], + }); + + actions = makeActionsProxy({ + actions: coreStore.actions, + }); + + expect(path(history.location)).toEqual("/foo"); + + await actions.push({ + activityId: "a1", + activityName: "Home", + activityParams: {}, + }); + + expect(path(history.location)).toEqual("/foo/"); + }); + + test("historySyncPlugin - uses the corresponding pattern when entering with a different route pattern", async () => { + history = createMemoryHistory({ + initialEntries: ["/bar"], + }); + + const coreStore = stackflow({ + activityNames: ["Home", "Article"], + plugins: [ + historySyncPlugin({ + history, + routes: { + Home: ["/", "/foo", "/bar", "/baz"], + Article: "/articles/:articleId", + }, + fallbackActivity: () => "Home", + }), + ], + }); + + actions = makeActionsProxy({ + actions: coreStore.actions, + }); + + expect(path(history.location)).toEqual("/bar"); + + await actions.push({ + activityId: "a1", + activityName: "Article", + activityParams: { + articleId: "123", + }, + }); + + expect(path(history.location)).toEqual("/articles/123/"); + + history.back(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(path(history.location)).toEqual("/bar/"); + }); + + test("historySyncPlugin - maintains the initially entered route pattern even during replace", async () => { + history = createMemoryHistory({ + initialEntries: ["/baz"], + }); + + const coreStore = stackflow({ + activityNames: ["Home", "Article"], + plugins: [ + historySyncPlugin({ + history, + routes: { + Home: ["/", "/foo", "/bar", "/baz"], + Article: "/articles/:articleId", + }, + fallbackActivity: () => "Home", + }), + ], + }); + + actions = makeActionsProxy({ + actions: coreStore.actions, + }); + + expect(path(history.location)).toEqual("/baz"); + + await actions.push({ + activityId: "a1", + activityName: "Article", + activityParams: { + articleId: "123", + }, + }); + + expect(path(history.location)).toEqual("/articles/123/"); + + history.back(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(path(history.location)).toEqual("/baz/"); + }); + + test("historySyncPlugin - each activity maintains its own route pattern when entered with different patterns", async () => { + history = createMemoryHistory({ + initialEntries: ["/foo"], + }); + + const coreStore = stackflow({ + activityNames: ["Home", "SecondHome"], + plugins: [ + historySyncPlugin({ + history, + routes: { + Home: ["/", "/foo", "/bar"], + SecondHome: ["/second", "/second-foo", "/second-bar"], + }, + fallbackActivity: () => "Home", + }), + ], + }); + + actions = makeActionsProxy({ + actions: coreStore.actions, + }); + + expect(path(history.location)).toEqual("/foo"); + + await actions.push({ + activityId: "s1", + activityName: "SecondHome", + activityParams: {}, + }); + + expect(path(history.location)).toEqual("/second/"); + + history.back(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(path(history.location)).toEqual("/foo/"); + }); }); diff --git a/extensions/plugin-history-sync/src/historySyncPlugin.tsx b/extensions/plugin-history-sync/src/historySyncPlugin.tsx index 507473121..24de0aa12 100644 --- a/extensions/plugin-history-sync/src/historySyncPlugin.tsx +++ b/extensions/plugin-history-sync/src/historySyncPlugin.tsx @@ -97,6 +97,20 @@ export function historySyncPlugin< const activityRoutes = sortActivityRoutes(normalizeActivityRouteMap(routes)); + const findMatchingRoute = (activityName: string, path?: string) => { + if (path) { + // Try to find route that matches both activity name and path + const matchByPath = activityRoutes.find((route) => { + if (route.activityName !== activityName) return false; + const template = makeTemplate(route, options.urlPatternOptions); + return template.parse(path) !== null; + }); + if (matchByPath) return matchByPath; + } + // Fallback to first route with matching activity name + return activityRoutes.find((r) => r.activityName === activityName)!; + }; + return () => { let pushFlag = 0; let silentFlag = false; @@ -322,7 +336,15 @@ export function historySyncPlugin< ).parse(currentPath) ?? urlSearchParamsToMap(pathToUrl(currentPath).searchParams), activityContext: { - path: currentPath, + path: makeTemplate( + targetActivityRoute, + options.urlPatternOptions, + ).fill( + makeTemplate( + targetActivityRoute, + options.urlPatternOptions, + ).parse(currentPath) ?? {}, + ), lazyActivityComponentRenderContext: { shouldRenderImmediately: true, }, @@ -342,9 +364,10 @@ export function historySyncPlugin< const stack = getStack(); const rootActivity = stack.activities[0]; - const match = activityRoutes.find( - (r) => r.activityName === rootActivity.name, - )!; + const activityPath = rootActivity.context && "path" in rootActivity.context + ? (rootActivity.context.path as string) + : undefined; + const match = findMatchingRoute(rootActivity.name, activityPath); const template = makeTemplate(match, options.urlPatternOptions); const lastStep = last(rootActivity.steps); @@ -505,10 +528,10 @@ export function historySyncPlugin< return; } - const match = activityRoutes.find( - (r) => r.activityName === activity.name, - )!; - + const activityPath = activity.context && "path" in activity.context + ? (activity.context.path as string) + : undefined; + const match = findMatchingRoute(activity.name, activityPath); const template = makeTemplate(match, options.urlPatternOptions); requestHistoryTick(() => { @@ -529,10 +552,10 @@ export function historySyncPlugin< return; } - const match = activityRoutes.find( - (r) => r.activityName === activity.name, - )!; - + const activityPath = activity.context && "path" in activity.context + ? (activity.context.path as string) + : undefined; + const match = findMatchingRoute(activity.name, activityPath); const template = makeTemplate(match, options.urlPatternOptions); requestHistoryTick(() => { @@ -553,10 +576,10 @@ export function historySyncPlugin< return; } - const match = activityRoutes.find( - (r) => r.activityName === activity.name, - )!; - + const activityPath = activity.context && "path" in activity.context + ? (activity.context.path as string) + : undefined; + const match = findMatchingRoute(activity.name, activityPath); const template = makeTemplate(match, options.urlPatternOptions); requestHistoryTick(() => { @@ -576,10 +599,10 @@ export function historySyncPlugin< return; } - const match = activityRoutes.find( - (r) => r.activityName === activity.name, - )!; - + const activityPath = activity.context && "path" in activity.context + ? (activity.context.path as string) + : undefined; + const match = findMatchingRoute(activity.name, activityPath); const template = makeTemplate(match, options.urlPatternOptions); requestHistoryTick(() => {