diff --git a/.changeset/spotty-parrots-clap.md b/.changeset/spotty-parrots-clap.md new file mode 100644 index 000000000..f5d919640 --- /dev/null +++ b/.changeset/spotty-parrots-clap.md @@ -0,0 +1,7 @@ +--- +"@stackflow/plugin-history-sync": minor +"@stackflow/plugin-preload": minor +"@stackflow/link": minor +--- + +Sort routes by variable count and refactor useRoutes(), normalizeRouteInput() function diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index 8138db784..03b229659 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -1,9 +1,5 @@ import type { UrlPatternOptions } from "@stackflow/plugin-history-sync"; -import { - makeTemplate, - normalizeRoute, - useRoutes, -} from "@stackflow/plugin-history-sync"; +import { makeTemplate, useRoutes } from "@stackflow/plugin-history-sync"; import { usePreloader } from "@stackflow/plugin-preload"; import type { ActivityComponentType } from "@stackflow/react"; import { useActions } from "@stackflow/react"; @@ -44,16 +40,13 @@ export const Link: TypeLink = forwardRef( const [preloaded, flagPreloaded] = useReducer(() => true, false); const href = useMemo(() => { - const route = routes[props.activityName]; + const match = routes.find((r) => r.activityName === props.activityName); - if (!route) { + if (!match) { return undefined; } - const template = makeTemplate( - normalizeRoute(route)[0], - props.urlPatternOptions, - ); + const template = makeTemplate(match.path, props.urlPatternOptions); const path = template.fill(props.activityParams); return path; diff --git a/extensions/plugin-history-sync/src/ActivityRoute.ts b/extensions/plugin-history-sync/src/ActivityRoute.ts new file mode 100644 index 000000000..3aeb6954e --- /dev/null +++ b/extensions/plugin-history-sync/src/ActivityRoute.ts @@ -0,0 +1,4 @@ +export type ActivityRoute = { + activityName: string; + path: string; +}; diff --git a/extensions/plugin-history-sync/src/ActivityRouteMapInput.ts b/extensions/plugin-history-sync/src/ActivityRouteMapInput.ts new file mode 100644 index 000000000..cfda45506 --- /dev/null +++ b/extensions/plugin-history-sync/src/ActivityRouteMapInput.ts @@ -0,0 +1,3 @@ +export type ActivityRouteMapInput = { + [activityName in string]?: string | string[]; +}; diff --git a/extensions/plugin-history-sync/src/RoutesContext.tsx b/extensions/plugin-history-sync/src/RoutesContext.tsx index ff9fc1764..a3bb2e7d9 100644 --- a/extensions/plugin-history-sync/src/RoutesContext.tsx +++ b/extensions/plugin-history-sync/src/RoutesContext.tsx @@ -1,13 +1,11 @@ import { createContext, useContext } from "react"; -export type RoutesMap = { - [activityName in string]?: string | string[]; -}; +import type { ActivityRoute } from "./ActivityRoute"; -export const RoutesContext = createContext({}); +export const RoutesContext = createContext([]); interface RoutesProviderProps { - routes: RoutesMap; + routes: ActivityRoute[]; children: React.ReactNode; } export const RoutesProvider: React.FC = (props) => ( diff --git a/extensions/plugin-history-sync/src/historySyncPlugin.tsx b/extensions/plugin-history-sync/src/historySyncPlugin.tsx index 0bd404e8e..e95ecf414 100644 --- a/extensions/plugin-history-sync/src/historySyncPlugin.tsx +++ b/extensions/plugin-history-sync/src/historySyncPlugin.tsx @@ -11,11 +11,12 @@ import { safeParseState, } from "./historyState"; import { last } from "./last"; +import { makeHistoryTaskQueue } from "./makeHistoryTaskQueue"; import type { UrlPatternOptions } from "./makeTemplate"; import { makeTemplate } from "./makeTemplate"; -import { normalizeRoute } from "./normalizeRoute"; -import { makeHistoryTaskQueue } from "./queue"; +import { normalizeActivityRouteMap } from "./normalizeActivityRouteMap"; import { RoutesProvider } from "./RoutesContext"; +import { sortActivityRoutes } from "./sortActivityRoutes"; const SECOND = 1000; const MINUTE = 60 * SECOND; @@ -46,6 +47,10 @@ export function historySyncPlugin< const { location } = history; + const activityRoutes = sortActivityRoutes( + normalizeActivityRouteMap(options.routes), + ); + return () => { let pushFlag = 0; let silentFlag = false; @@ -57,7 +62,7 @@ export function historySyncPlugin< wrapStack({ stack }) { return ( - + {stack.render()} @@ -86,7 +91,7 @@ export function historySyncPlugin< ]; } - function resolvePath() { + function resolveCurrentPath() { if ( initialContext?.req?.path && typeof initialContext.req.path === "string" @@ -101,34 +106,29 @@ export function historySyncPlugin< return location.pathname + location.search; } - const path = resolvePath(); - const activityNames = Object.keys(options.routes); - - if (path) { - for (const activityName of activityNames) { - const routes = normalizeRoute(options.routes[activityName as K]); - - for (const route of routes) { - const template = makeTemplate(route, options.urlPatternOptions); - const activityParams = template.parse(path); - - if (activityParams) { - const activityId = id(); - - return [ - makeEvent("Pushed", { - activityId, - activityName, - activityParams: { - ...activityParams, - }, - eventDate: new Date().getTime() - MINUTE, - activityContext: { - path, - }, - }), - ]; - } + const currentPath = resolveCurrentPath(); + + if (currentPath) { + for (const { activityName, path } of activityRoutes) { + const template = makeTemplate(path, options.urlPatternOptions); + const activityParams = template.parse(currentPath); + + if (activityParams) { + const activityId = id(); + + return [ + makeEvent("Pushed", { + activityId, + activityName, + activityParams: { + ...activityParams, + }, + eventDate: new Date().getTime() - MINUTE, + activityContext: { + path: currentPath, + }, + }), + ]; } } } @@ -137,10 +137,10 @@ export function historySyncPlugin< const fallbackActivityName = options.fallbackActivity({ initialContext, }); - const fallbackActivityRoutes = normalizeRoute( - options.routes[fallbackActivityName], + const fallbackActivityRoute = activityRoutes.find( + (r) => r.activityName === fallbackActivityName, ); - const fallbackActivityPath = fallbackActivityRoutes[0]; + const fallbackActivityPath = fallbackActivityRoute?.path; return [ makeEvent("Pushed", { @@ -157,10 +157,10 @@ export function historySyncPlugin< onInit({ actions: { getStack, dispatchEvent, push, stepPush } }) { const rootActivity = getStack().activities[0]; - const template = makeTemplate( - normalizeRoute(options.routes[rootActivity.name])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === rootActivity.name, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); const lastStep = last(rootActivity.steps); @@ -311,10 +311,10 @@ export function historySyncPlugin< return; } - const template = makeTemplate( - normalizeRoute(options.routes[activity.name])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === activity.name, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); requestHistoryTick(() => { silentFlag = true; @@ -334,10 +334,10 @@ export function historySyncPlugin< return; } - const template = makeTemplate( - normalizeRoute(options.routes[activity.name])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === activity.name, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); requestHistoryTick(() => { silentFlag = true; @@ -357,10 +357,10 @@ export function historySyncPlugin< return; } - const template = makeTemplate( - normalizeRoute(options.routes[activity.name])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === activity.name, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); requestHistoryTick(() => { silentFlag = true; @@ -379,10 +379,10 @@ export function historySyncPlugin< return; } - const template = makeTemplate( - normalizeRoute(options.routes[activity.name])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === activity.name, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); requestHistoryTick(() => { silentFlag = true; @@ -398,10 +398,10 @@ export function historySyncPlugin< }); }, onBeforePush({ actionParams, actions: { overrideActionParams } }) { - const template = makeTemplate( - normalizeRoute(options.routes[actionParams.activityName])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === actionParams.activityName, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); const path = template.fill(actionParams.activityParams); overrideActionParams({ @@ -416,10 +416,10 @@ export function historySyncPlugin< actionParams, actions: { overrideActionParams, getStack }, }) { - const template = makeTemplate( - normalizeRoute(options.routes[actionParams.activityName])[0], - options.urlPatternOptions, + const match = activityRoutes.find( + (r) => r.activityName === actionParams.activityName, ); + const template = makeTemplate(match!.path, options.urlPatternOptions); const path = template.fill(actionParams.activityParams); overrideActionParams({ diff --git a/extensions/plugin-history-sync/src/index.ts b/extensions/plugin-history-sync/src/index.ts index 625ce29fc..cb8bb334a 100644 --- a/extensions/plugin-history-sync/src/index.ts +++ b/extensions/plugin-history-sync/src/index.ts @@ -1,5 +1,4 @@ export { useHistoryTick } from "./HistoryQueueContext"; export * from "./historySyncPlugin"; export { makeTemplate, UrlPatternOptions } from "./makeTemplate"; -export { normalizeRoute } from "./normalizeRoute"; export { useRoutes } from "./RoutesContext"; diff --git a/extensions/plugin-history-sync/src/queue.ts b/extensions/plugin-history-sync/src/makeHistoryTaskQueue.ts similarity index 100% rename from extensions/plugin-history-sync/src/queue.ts rename to extensions/plugin-history-sync/src/makeHistoryTaskQueue.ts diff --git a/extensions/plugin-history-sync/src/makeTemplate.ts b/extensions/plugin-history-sync/src/makeTemplate.ts index 234a708de..f8b4e0ee6 100644 --- a/extensions/plugin-history-sync/src/makeTemplate.ts +++ b/extensions/plugin-history-sync/src/makeTemplate.ts @@ -44,10 +44,10 @@ export interface UrlPatternOptions { } export function makeTemplate( - templateStr: string, + path: string, urlPatternOptions?: UrlPatternOptions, ) { - const pattern = new UrlPattern(`${templateStr}(/)`, urlPatternOptions); + const pattern = new UrlPattern(`${path}(/)`, urlPatternOptions); return { fill(params: { [key: string]: string | undefined }) { @@ -95,5 +95,6 @@ export function makeTemplate( ...pathParams, }; }, + variableCount: (pattern as any).names.length, }; } diff --git a/extensions/plugin-history-sync/src/normalizeActivityRouteMap.spec.ts b/extensions/plugin-history-sync/src/normalizeActivityRouteMap.spec.ts new file mode 100644 index 000000000..62ec9d0cf --- /dev/null +++ b/extensions/plugin-history-sync/src/normalizeActivityRouteMap.spec.ts @@ -0,0 +1,23 @@ +import { normalizeActivityRouteMap } from "./normalizeActivityRouteMap"; + +test("normalizeActivityRouteMap", () => { + expect( + normalizeActivityRouteMap({ + Hello: ["/hello", "/hello-2"], + World: "/world", + }), + ).toStrictEqual([ + { + activityName: "Hello", + path: "/hello", + }, + { + activityName: "Hello", + path: "/hello-2", + }, + { + activityName: "World", + path: "/world", + }, + ]); +}); diff --git a/extensions/plugin-history-sync/src/normalizeActivityRouteMap.ts b/extensions/plugin-history-sync/src/normalizeActivityRouteMap.ts new file mode 100644 index 000000000..79575eaf2 --- /dev/null +++ b/extensions/plugin-history-sync/src/normalizeActivityRouteMap.ts @@ -0,0 +1,22 @@ +import type { ActivityRoute } from "./ActivityRoute"; +import type { ActivityRouteMapInput } from "./ActivityRouteMapInput"; +import { normalizeRouteInput } from "./normalizeRouteInput"; + +export function normalizeActivityRouteMap( + activityRouteMap: T, +): ActivityRoute[] { + const routes = Object.keys(activityRouteMap).flatMap((activityName) => { + const routeInput = activityRouteMap[activityName]; + + if (!routeInput) { + return []; + } + + return normalizeRouteInput(routeInput).map((path) => ({ + activityName, + path, + })); + }); + + return routes; +} diff --git a/extensions/plugin-history-sync/src/normalizeRoute.spec.ts b/extensions/plugin-history-sync/src/normalizeRoute.spec.ts deleted file mode 100644 index a0e0435db..000000000 --- a/extensions/plugin-history-sync/src/normalizeRoute.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { normalizeRoute } from "./normalizeRoute"; - -test("normalizeRoute - string이 들어오면 [string]으로 만듭니다", () => { - expect(normalizeRoute("/home")).toEqual(["/home"]); -}); - -test("normalizeRoute - string[]이 들어오면 string[]인채로 반환합니다", () => { - expect(normalizeRoute(["/home"])).toEqual(["/home"]); -}); diff --git a/extensions/plugin-history-sync/src/normalizeRoute.ts b/extensions/plugin-history-sync/src/normalizeRoute.ts deleted file mode 100644 index 9a7c2db58..000000000 --- a/extensions/plugin-history-sync/src/normalizeRoute.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function normalizeRoute(route: string | string[]) { - return typeof route === "string" ? [route] : route; -} diff --git a/extensions/plugin-history-sync/src/normalizeRouteInput.spec.ts b/extensions/plugin-history-sync/src/normalizeRouteInput.spec.ts new file mode 100644 index 000000000..94ee8b74f --- /dev/null +++ b/extensions/plugin-history-sync/src/normalizeRouteInput.spec.ts @@ -0,0 +1,9 @@ +import { normalizeRouteInput } from "./normalizeRouteInput"; + +test("normalizeRouteInput - string이 들어오면 [string]으로 만듭니다", () => { + expect(normalizeRouteInput("/home")).toEqual(["/home"]); +}); + +test("normalizeRouteInput - string[]이 들어오면 string[]인채로 반환합니다", () => { + expect(normalizeRouteInput(["/home"])).toEqual(["/home"]); +}); diff --git a/extensions/plugin-history-sync/src/normalizeRouteInput.ts b/extensions/plugin-history-sync/src/normalizeRouteInput.ts new file mode 100644 index 000000000..a981efb5c --- /dev/null +++ b/extensions/plugin-history-sync/src/normalizeRouteInput.ts @@ -0,0 +1,3 @@ +export function normalizeRouteInput(route: string | string[]) { + return typeof route === "string" ? [route] : route; +} diff --git a/extensions/plugin-history-sync/src/sortActivityRoutes.spec.ts b/extensions/plugin-history-sync/src/sortActivityRoutes.spec.ts new file mode 100644 index 000000000..ec9df5c74 --- /dev/null +++ b/extensions/plugin-history-sync/src/sortActivityRoutes.spec.ts @@ -0,0 +1,27 @@ +import { sortActivityRoutes } from "./sortActivityRoutes"; + +test("sortActivityRoutes - 우선순위가 높은 라우트가 먼저 놓여집니다", () => { + const routes = sortActivityRoutes([ + { activityName: "B", path: "/hello/:param" }, + { activityName: "A", path: "/hello/world" }, + ]); + + expect(routes).toStrictEqual([ + { activityName: "A", path: "/hello/world" }, + { activityName: "B", path: "/hello/:param" }, + ]); +}); + +test("sortActivityRoutes - 한 액티비티가 여러 라우트를 가지는 경우, 여러번 route에 등록됩니다", () => { + const routes = sortActivityRoutes([ + { activityName: "B", path: "/hello/:param" }, + { activityName: "B", path: "/hello/second" }, + { activityName: "A", path: "/hello/world" }, + ]); + + expect(routes).toStrictEqual([ + { activityName: "B", path: "/hello/second" }, + { activityName: "A", path: "/hello/world" }, + { activityName: "B", path: "/hello/:param" }, + ]); +}); diff --git a/extensions/plugin-history-sync/src/sortActivityRoutes.ts b/extensions/plugin-history-sync/src/sortActivityRoutes.ts new file mode 100644 index 000000000..2813db925 --- /dev/null +++ b/extensions/plugin-history-sync/src/sortActivityRoutes.ts @@ -0,0 +1,9 @@ +import type { ActivityRoute } from "./ActivityRoute"; +import { makeTemplate } from "./makeTemplate"; + +export function sortActivityRoutes(routes: ActivityRoute[]): ActivityRoute[] { + return [...routes].sort( + (a, b) => + makeTemplate(a.path).variableCount - makeTemplate(b.path).variableCount, + ); +} diff --git a/extensions/plugin-preload/src/usePreloader.ts b/extensions/plugin-preload/src/usePreloader.ts index 502328356..6bffb639d 100644 --- a/extensions/plugin-preload/src/usePreloader.ts +++ b/extensions/plugin-preload/src/usePreloader.ts @@ -1,9 +1,5 @@ import type { UrlPatternOptions } from "@stackflow/plugin-history-sync"; -import { - makeTemplate, - normalizeRoute, - useRoutes, -} from "@stackflow/plugin-history-sync"; +import { makeTemplate, useRoutes } from "@stackflow/plugin-history-sync"; import type { ActivityComponentType } from "@stackflow/react"; import { useMemo } from "react"; @@ -40,12 +36,10 @@ export function usePreloader( return null; } - const route = routes[activityName]; - const template = route - ? makeTemplate( - normalizeRoute(route)[0], - usePreloaderOptions?.urlPatternOptions, - ) + const match = routes.find((r) => r.activityName === activityName); + + const template = match + ? makeTemplate(match.path, usePreloaderOptions?.urlPatternOptions) : undefined; const path = template?.fill(activityParams);