@ahnseungchan/webviewkit 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,6 +5,8 @@ var BACK_TIMEOUT_MS = 1e3;
5
5
  var BRIDGE_TIMEOUT_MS = 5e3;
6
6
  var BRIDGE_HOST_WAIT_MS = 3e3;
7
7
  var BRIDGE_HOST_POLL_MS = 100;
8
+ var KEEP_ALIVE_LIMIT = 5;
9
+ var DEV_MOUNTED_PAGES_WARN = 15;
8
10
 
9
11
  // src/bridge/detect-host.ts
10
12
  var FLUTTER_READY_EVENT = "flutterInAppWebViewPlatformReady";
@@ -779,19 +781,61 @@ import styled4 from "@emotion/styled";
779
781
 
780
782
  // src/stack-router/stack-router-provider.tsx
781
783
  import styled3 from "@emotion/styled";
782
- import { createContext as createContext2, useContext as useContext2, useMemo } from "react";
784
+ import {
785
+ createContext as createContext2,
786
+ useContext as useContext3,
787
+ useEffect as useEffect6,
788
+ useMemo,
789
+ useRef as useRef6
790
+ } from "react";
783
791
 
784
792
  // src/stack-router/_components/header.tsx
785
793
  import styled from "@emotion/styled";
794
+ import { useContext as useContext2 } from "react";
795
+
796
+ // src/stack-router/activity-context.tsx
797
+ import { createContext, useContext, useEffect as useEffect2, useRef as useRef2 } from "react";
798
+ var ActivityContext = createContext(null);
799
+ function useActivity() {
800
+ const context = useContext(ActivityContext);
801
+ if (!context) {
802
+ throw new Error(
803
+ "useActivity must be used inside a StackRouterProvider page"
804
+ );
805
+ }
806
+ return context;
807
+ }
808
+ function useParams() {
809
+ return useActivity().params;
810
+ }
811
+ function useOnRevive(callback) {
812
+ const { revision, data } = useActivity();
813
+ const callbackRef = useRef2(callback);
814
+ callbackRef.current = callback;
815
+ const lastRevisionRef = useRef2(revision);
816
+ useEffect2(() => {
817
+ if (revision > lastRevisionRef.current) callbackRef.current(data);
818
+ lastRevisionRef.current = revision;
819
+ }, [revision, data]);
820
+ }
821
+
822
+ // src/stack-router/_components/header.tsx
786
823
  import { jsx as jsx3, jsxs as jsxs2 } from "@emotion/react/jsx-runtime";
787
824
  var Header = ({ title }) => {
825
+ var _a;
826
+ const activity = useContext2(ActivityContext);
788
827
  const { currentPath: currentPath2, canGoBack } = useStackRouter();
828
+ const path = (_a = activity == null ? void 0 : activity.path) != null ? _a : currentPath2;
829
+ const showBack = activity ? activity.index > 0 : canGoBack;
789
830
  return /* @__PURE__ */ jsxs2(StyledHeader, { children: [
790
- canGoBack && /* @__PURE__ */ jsx3(back_router_default, {}),
791
- /* @__PURE__ */ jsx3(StyledTitle, { children: title != null ? title : currentPath2 })
831
+ showBack && /* @__PURE__ */ jsx3(back_router_default, {}),
832
+ /* @__PURE__ */ jsx3(StyledTitle, { children: title != null ? title : path })
792
833
  ] });
793
834
  };
794
835
  var StyledHeader = styled.header`
836
+ position: sticky;
837
+ top: 0;
838
+ z-index: 1;
795
839
  width: 100%;
796
840
  height: 64px;
797
841
  flex-shrink: 0;
@@ -804,27 +848,32 @@ var StyledHeader = styled.header`
804
848
  `;
805
849
  var StyledTitle = styled.h1`
806
850
  margin: 0;
851
+ min-width: 0;
807
852
  font-size: 1.25rem;
808
853
  font-weight: 600;
809
854
  color: var(--wvk-header-color, #111827);
855
+ white-space: nowrap;
856
+ overflow: hidden;
857
+ text-overflow: ellipsis;
810
858
  `;
811
859
  var header_default = Header;
812
860
 
813
861
  // src/stack-router/_components/stack-page.tsx
814
862
  import { css, keyframes } from "@emotion/react";
815
863
  import styled2 from "@emotion/styled";
816
- import { useLayoutEffect, useRef as useRef2 } from "react";
864
+ import { useLayoutEffect, useRef as useRef3 } from "react";
817
865
  import { jsx as jsx4 } from "@emotion/react/jsx-runtime";
818
866
  var IOS_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
819
867
  var FADE_EASING = "ease";
820
868
  var PARALLAX_OFFSET = "-30%";
869
+ var EDGE_SHADOW = "var(--wvk-edge-shadow, -6px 0 24px rgba(0, 0, 0, 0.12))";
821
870
  var coverIn = keyframes`
822
- from { transform: translate3d(100%, 0, 0); }
823
- to { transform: translate3d(0, 0, 0); }
871
+ from { transform: translate3d(100%, 0, 0); box-shadow: none; }
872
+ to { transform: translate3d(0, 0, 0); box-shadow: ${EDGE_SHADOW}; }
824
873
  `;
825
874
  var coverOut = keyframes`
826
- from { transform: translate3d(0, 0, 0); }
827
- to { transform: translate3d(100%, 0, 0); }
875
+ from { transform: translate3d(0, 0, 0); box-shadow: ${EDGE_SHADOW}; }
876
+ to { transform: translate3d(100%, 0, 0); box-shadow: none; }
828
877
  `;
829
878
  var parallaxOut = keyframes`
830
879
  from { transform: translate3d(0, 0, 0); }
@@ -856,7 +905,6 @@ function slideStyles(direction, pageState, duration) {
856
905
  if (layerOf(direction, pageState) === "upper") {
857
906
  return css`
858
907
  animation: ${direction === "push" ? coverIn : coverOut} ${timing};
859
- box-shadow: var(--wvk-edge-shadow, -6px 0 24px rgba(0, 0, 0, 0.12));
860
908
  `;
861
909
  }
862
910
  return css`
@@ -879,7 +927,7 @@ var PageWrapper = styled2.div`
879
927
  -webkit-overflow-scrolling: touch;
880
928
  background: var(--wvk-page-bg, #ffffff);
881
929
  z-index: ${({ zIndex }) => zIndex};
882
- visibility: ${({ pageState }) => pageState === "covered" ? "hidden" : "visible"};
930
+ visibility: ${({ pageState }) => pageState === "covered" || pageState === "cached" ? "hidden" : "visible"};
883
931
  ${({ pageState, direction, transitionType, duration }) => {
884
932
  if (pageState !== "enter" && pageState !== "exit") return "";
885
933
  if (transitionType === "none" || duration === 0) return "";
@@ -902,7 +950,7 @@ function StackPage({
902
950
  children,
903
951
  ...wrapperProps
904
952
  }) {
905
- const ref = useRef2(null);
953
+ const ref = useRef3(null);
906
954
  useLayoutEffect(() => {
907
955
  var _a;
908
956
  (_a = ref.current) == null ? void 0 : _a.toggleAttribute("inert", !isTop);
@@ -924,32 +972,117 @@ function StackPage({
924
972
  );
925
973
  }
926
974
 
927
- // src/stack-router/activity-context.tsx
928
- import { createContext, useContext } from "react";
929
- var ActivityContext = createContext(null);
930
- function useActivity() {
931
- const context = useContext(ActivityContext);
932
- if (!context) {
933
- throw new Error(
934
- "useActivity must be used inside a StackRouterProvider page"
935
- );
936
- }
937
- return context;
938
- }
939
- function useParams() {
940
- return useActivity().params;
941
- }
942
-
943
975
  // src/stack-router/stack-reducer.ts
944
976
  var topOf = (entries) => entries[entries.length - 1];
945
- function createStackState(entry) {
946
- return { entries: [entry], leaving: [], transition: null };
977
+ var bumpRevision = (entry) => {
978
+ var _a;
979
+ return {
980
+ ...entry,
981
+ revision: ((_a = entry.revision) != null ? _a : 0) + 1
982
+ };
983
+ };
984
+ function createStackState(entry, cacheLimit = 0) {
985
+ return {
986
+ entries: [entry],
987
+ leaving: [],
988
+ transition: null,
989
+ cached: [],
990
+ cacheLimit
991
+ };
992
+ }
993
+ function cacheEntries(state, removed) {
994
+ const { entries, cached, cacheLimit } = state;
995
+ const incoming = removed.filter((entry) => entry.keepAlive);
996
+ if (cacheLimit <= 0 || incoming.length === 0) return cached;
997
+ const incomingPaths = new Set(incoming.map((entry) => entry.path));
998
+ const merged = [
999
+ ...cached.filter((entry) => !incomingPaths.has(entry.path)),
1000
+ ...incoming
1001
+ ];
1002
+ const capacity = cacheLimit - entries.filter((e) => e.keepAlive).length;
1003
+ return capacity > 0 ? merged.slice(-capacity) : [];
1004
+ }
1005
+ function withoutPath(list, path) {
1006
+ return list.filter((entry) => entry.path !== path);
1007
+ }
1008
+ function flushLeaving(state, nextEntries) {
1009
+ const alive = new Set(nextEntries.map((entry) => entry.key));
1010
+ const removed = state.leaving.filter((entry) => !alive.has(entry.key));
1011
+ return cacheEntries({ ...state, entries: nextEntries }, removed);
1012
+ }
1013
+ function revive(state, action) {
1014
+ const { direction, index, animate } = action;
1015
+ const entry = bumpRevision(action.entry);
1016
+ const previousTop = topOf(state.entries);
1017
+ const entries = [...state.entries.slice(0, index), entry];
1018
+ const removed = state.entries.slice(index);
1019
+ const cached = state.cached.filter((item) => item.key !== entry.key);
1020
+ const base = { ...state, entries, cached };
1021
+ if (!animate || direction === "pop" && removed.length === 0) {
1022
+ return {
1023
+ ...base,
1024
+ cached: cacheEntries(base, removed),
1025
+ leaving: [],
1026
+ transition: null
1027
+ };
1028
+ }
1029
+ if (direction === "push") {
1030
+ const stillInStack = !removed.includes(previousTop);
1031
+ return {
1032
+ ...base,
1033
+ cached: cacheEntries(
1034
+ base,
1035
+ removed.filter((item) => item !== previousTop)
1036
+ ),
1037
+ leaving: stillInStack ? [] : [previousTop],
1038
+ transition: {
1039
+ type: "push",
1040
+ enterKey: entry.key,
1041
+ exitKey: previousTop.key
1042
+ }
1043
+ };
1044
+ }
1045
+ return {
1046
+ ...base,
1047
+ leaving: removed,
1048
+ transition: {
1049
+ type: "pop",
1050
+ enterKey: entry.key,
1051
+ exitKeys: removed.map((item) => item.key)
1052
+ }
1053
+ };
947
1054
  }
1055
+ function findRevivable(state, path, opts = {}) {
1056
+ const matches = (entry) => entry.path === path;
1057
+ const fromStack = opts.includeRemovable ? [...state.entries.slice(1)].reverse().find((entry) => entry.keepAlive && matches(entry)) : void 0;
1058
+ return fromStack != null ? fromStack : [...state.cached].reverse().find((entry) => matches(entry));
1059
+ }
1060
+ var OVERWRITES_LEAVING = /* @__PURE__ */ new Set([
1061
+ "PUSH",
1062
+ "POP_TO",
1063
+ "RESET",
1064
+ "RESET_PUSH",
1065
+ "RESTORE",
1066
+ "REVIVE"
1067
+ ]);
948
1068
  function stackReducer(state, action) {
1069
+ if (!OVERWRITES_LEAVING.has(action.type) || state.leaving.length === 0) {
1070
+ return reduce(state, action);
1071
+ }
1072
+ const settled = { ...state, leaving: [] };
1073
+ const next = reduce(settled, action);
1074
+ if (next === settled) return state;
1075
+ return reduce(
1076
+ { ...settled, cached: flushLeaving(state, next.entries) },
1077
+ action
1078
+ );
1079
+ }
1080
+ function reduce(state, action) {
949
1081
  switch (action.type) {
950
1082
  case "PUSH": {
951
1083
  const previousTop = topOf(state.entries);
952
1084
  return {
1085
+ ...state,
953
1086
  entries: [...state.entries, action.entry],
954
1087
  leaving: [],
955
1088
  transition: action.animate ? {
@@ -959,20 +1092,34 @@ function stackReducer(state, action) {
959
1092
  } : null
960
1093
  };
961
1094
  }
962
- case "REPLACE":
1095
+ case "REPLACE": {
1096
+ const entries = [...state.entries.slice(0, -1), action.entry];
1097
+ const { path } = action.entry;
963
1098
  return {
964
1099
  ...state,
965
- entries: [...state.entries.slice(0, -1), action.entry]
1100
+ entries,
1101
+ cached: cacheEntries(
1102
+ { ...state, entries, cached: withoutPath(state.cached, path) },
1103
+ withoutPath([topOf(state.entries)], path)
1104
+ )
966
1105
  };
1106
+ }
967
1107
  case "POP_TO": {
968
1108
  const index = Math.max(0, action.index);
969
1109
  if (index >= state.entries.length - 1) return state;
970
1110
  const entries = state.entries.slice(0, index + 1);
971
1111
  const leaving = state.entries.slice(index + 1);
972
1112
  if (!action.animate) {
973
- return { entries, leaving: [], transition: null };
1113
+ return {
1114
+ ...state,
1115
+ entries,
1116
+ leaving: [],
1117
+ transition: null,
1118
+ cached: cacheEntries({ ...state, entries }, leaving)
1119
+ };
974
1120
  }
975
1121
  return {
1122
+ ...state,
976
1123
  entries,
977
1124
  leaving,
978
1125
  transition: {
@@ -982,21 +1129,80 @@ function stackReducer(state, action) {
982
1129
  }
983
1130
  };
984
1131
  }
985
- case "RESET":
986
- return createStackState(action.entry);
1132
+ case "RESET": {
1133
+ const next = createStackState(action.entry, state.cacheLimit);
1134
+ const { path } = action.entry;
1135
+ return {
1136
+ ...next,
1137
+ cached: cacheEntries(
1138
+ {
1139
+ ...state,
1140
+ entries: next.entries,
1141
+ cached: withoutPath(state.cached, path)
1142
+ },
1143
+ withoutPath(state.entries, path)
1144
+ )
1145
+ };
1146
+ }
1147
+ case "REVIVE":
1148
+ return revive(state, action);
1149
+ case "RESET_PUSH": {
1150
+ const [root] = state.entries;
1151
+ const previousTop = topOf(state.entries);
1152
+ const reusedKey = action.entry.key;
1153
+ const isReused = [...state.entries, ...state.cached].some(
1154
+ (entry) => entry.key === reusedKey
1155
+ );
1156
+ const entries = [
1157
+ root,
1158
+ isReused ? bumpRevision(action.entry) : action.entry
1159
+ ];
1160
+ const sameTop = previousTop.key === reusedKey;
1161
+ const dropped = (action.animate ? state.entries.slice(1, -1) : state.entries.slice(1)).filter((entry) => entry.key !== reusedKey);
1162
+ const cached = state.cached.filter((entry) => entry.key !== reusedKey);
1163
+ const exiting = action.animate && !sameTop;
1164
+ return {
1165
+ ...state,
1166
+ entries,
1167
+ cached: cacheEntries({ ...state, entries, cached }, dropped),
1168
+ leaving: exiting ? [previousTop] : [],
1169
+ transition: exiting ? {
1170
+ type: "push",
1171
+ enterKey: action.entry.key,
1172
+ exitKey: previousTop.key
1173
+ } : null
1174
+ };
1175
+ }
987
1176
  case "RESTORE":
988
- return { entries: action.entries, leaving: [], transition: null };
989
- case "END_TRANSITION":
1177
+ return {
1178
+ ...state,
1179
+ entries: action.entries,
1180
+ leaving: [],
1181
+ transition: null
1182
+ };
1183
+ case "END_TRANSITION": {
990
1184
  if (!state.transition && state.leaving.length === 0) return state;
991
- return { ...state, leaving: [], transition: null };
1185
+ return {
1186
+ ...state,
1187
+ leaving: [],
1188
+ transition: null,
1189
+ cached: flushLeaving(state, state.entries)
1190
+ };
1191
+ }
992
1192
  default:
993
1193
  return state;
994
1194
  }
995
1195
  }
996
1196
  function getRenderedPages(state) {
997
- const { entries, leaving, transition } = state;
1197
+ const { entries, leaving, transition, cached } = state;
998
1198
  const top = topOf(entries);
999
- return [...entries, ...leaving].map((entry) => {
1199
+ const ordered = (transition == null ? void 0 : transition.type) === "push" ? [...entries.slice(0, -1), ...leaving, top] : [...entries, ...leaving];
1200
+ const cachedPages = cached.map((entry) => ({
1201
+ entry,
1202
+ pageState: "cached",
1203
+ isTop: false
1204
+ }));
1205
+ const stackPages = ordered.map((entry) => {
1000
1206
  const isTop = entry.key === top.key;
1001
1207
  let pageState = isTop ? "idle" : "covered";
1002
1208
  if ((transition == null ? void 0 : transition.type) === "push") {
@@ -1009,10 +1215,11 @@ function getRenderedPages(state) {
1009
1215
  }
1010
1216
  return { entry, pageState, isTop };
1011
1217
  });
1218
+ return [...cachedPages, ...stackPages];
1012
1219
  }
1013
1220
 
1014
1221
  // src/stack-router/use-prefers-reduced-motion.ts
1015
- import { useEffect as useEffect2, useState } from "react";
1222
+ import { useEffect as useEffect3, useState } from "react";
1016
1223
  var QUERY = "(prefers-reduced-motion: reduce)";
1017
1224
  var getMediaQuery = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null;
1018
1225
  function usePrefersReducedMotion() {
@@ -1022,7 +1229,7 @@ function usePrefersReducedMotion() {
1022
1229
  return (_b = (_a = getMediaQuery()) == null ? void 0 : _a.matches) != null ? _b : false;
1023
1230
  }
1024
1231
  );
1025
- useEffect2(() => {
1232
+ useEffect3(() => {
1026
1233
  var _a;
1027
1234
  const mediaQuery = getMediaQuery();
1028
1235
  if (!mediaQuery) return;
@@ -1037,13 +1244,13 @@ function usePrefersReducedMotion() {
1037
1244
  }
1038
1245
 
1039
1246
  // src/stack-router/use-stack-bridge-sync.ts
1040
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
1247
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
1041
1248
  function useStackBridgeSync(bridge, router, onBackAtRoot) {
1042
1249
  const enabled = Boolean(bridge);
1043
1250
  const options = typeof bridge === "object" ? bridge : {};
1044
- const latest = useRef3({ router, options, onBackAtRoot });
1251
+ const latest = useRef4({ router, options, onBackAtRoot });
1045
1252
  latest.current = { router, options, onBackAtRoot };
1046
- useEffect3(() => {
1253
+ useEffect4(() => {
1047
1254
  if (!enabled) return;
1048
1255
  return installBridgeHost({
1049
1256
  allowUrl: latest.current.options.allowUrl,
@@ -1068,7 +1275,7 @@ function useStackBridgeSync(bridge, router, onBackAtRoot) {
1068
1275
  }, [enabled]);
1069
1276
  const { depth, currentPath: currentPath2 } = router;
1070
1277
  const emit = enabled && options.emitStackChanged !== false;
1071
- useEffect3(() => {
1278
+ useEffect4(() => {
1072
1279
  if (!emit || !isNativeBridgeHost(detectBridgeHost())) return;
1073
1280
  createWebViewBridgeClient().notifyStackChanged({ depth, path: currentPath2 }).catch((error) => {
1074
1281
  var _a, _b;
@@ -1078,7 +1285,7 @@ function useStackBridgeSync(bridge, router, onBackAtRoot) {
1078
1285
  }
1079
1286
 
1080
1287
  // src/stack-router/use-stack-navigation.ts
1081
- import { useCallback, useEffect as useEffect4, useReducer, useRef as useRef4 } from "react";
1288
+ import { useCallback, useEffect as useEffect5, useReducer, useRef as useRef5 } from "react";
1082
1289
 
1083
1290
  // src/stack-router/match-path.ts
1084
1291
  var splitSegments = (pathname) => pathname.split("/").filter((segment) => segment.length > 0);
@@ -1129,32 +1336,52 @@ var isDev2 = () => {
1129
1336
  var _a;
1130
1337
  return typeof process !== "undefined" && ((_a = process.env) == null ? void 0 : _a.NODE_ENV) !== "production";
1131
1338
  };
1132
- function createEntry(hit, path, data, key = createBridgeId()) {
1339
+ function createEntry(hit, path, cacheLimit, data, key = createBridgeId()) {
1133
1340
  return {
1134
1341
  key,
1135
1342
  path,
1136
1343
  pattern: hit.activity.path,
1137
1344
  params: hit.match.params,
1138
1345
  query: hit.match.query,
1139
- data
1346
+ data,
1347
+ keepAlive: hit.activity.keepAlive === true && cacheLimit > 0
1140
1348
  };
1141
1349
  }
1142
1350
  var currentUrl = () => window.location.pathname + window.location.search;
1143
1351
  function useStackNavigation(options) {
1144
- const optionsRef = useRef4(options);
1352
+ const optionsRef = useRef5(options);
1145
1353
  optionsRef.current = options;
1146
1354
  const [state, dispatch] = useReducer(stackReducer, void 0, () => {
1355
+ var _a;
1147
1356
  const { initialActivity } = options;
1357
+ const cacheLimit = (_a = options.keepAliveLimit) != null ? _a : KEEP_ALIVE_LIMIT;
1148
1358
  return createStackState(
1149
1359
  createEntry(
1150
1360
  { activity: initialActivity, match: { params: {}, query: {} } },
1151
- initialActivity.path
1152
- )
1361
+ initialActivity.path,
1362
+ cacheLimit
1363
+ ),
1364
+ cacheLimit
1153
1365
  );
1154
1366
  });
1155
- const stateRef = useRef4(state);
1367
+ const stateRef = useRef5(state);
1156
1368
  stateRef.current = state;
1157
- const busyRef = useRef4(false);
1369
+ const newEntry = (hit, path, data, key) => createEntry(hit, path, stateRef.current.cacheLimit, data, key);
1370
+ const busyRef = useRef5(false);
1371
+ const ignorePopRef = useRef5(0);
1372
+ const pendingTravelRef = useRef5(0);
1373
+ const isLocked = () => busyRef.current || pendingTravelRef.current > 0;
1374
+ const travel = (delta, ignorePop) => {
1375
+ pendingTravelRef.current += 1;
1376
+ if (ignorePop) ignorePopRef.current += 1;
1377
+ return history.travel(delta).then((popped) => {
1378
+ pendingTravelRef.current -= 1;
1379
+ if (!popped && ignorePop) {
1380
+ ignorePopRef.current = Math.max(0, ignorePopRef.current - 1);
1381
+ }
1382
+ return popped;
1383
+ });
1384
+ };
1158
1385
  const resolve = useCallback((path) => {
1159
1386
  const { activities, initialActivity } = optionsRef.current;
1160
1387
  return findActivity([...activities, initialActivity], path);
@@ -1176,27 +1403,33 @@ function useStackNavigation(options) {
1176
1403
  const animate = () => optionsRef.current.duration > 0;
1177
1404
  const push = useCallback(
1178
1405
  (path, data) => {
1179
- if (busyRef.current) return false;
1406
+ if (isLocked()) return false;
1180
1407
  const hit = resolveOrNotify(path);
1181
1408
  if (!hit) return false;
1182
- const entry = createEntry(hit, path, data);
1409
+ const { entries } = stateRef.current;
1410
+ const revived = findRevivable(stateRef.current, path);
1411
+ const entry = revived ? { ...revived, data } : newEntry(hit, path, data);
1183
1412
  busyRef.current = true;
1184
- history.push(path, {
1185
- key: entry.key,
1186
- index: stateRef.current.entries.length,
1187
- data
1188
- });
1189
- dispatch({ type: "PUSH", entry, animate: animate() });
1413
+ history.push(path, { key: entry.key, index: entries.length, data });
1414
+ dispatch(
1415
+ revived ? {
1416
+ type: "REVIVE",
1417
+ entry,
1418
+ direction: "push",
1419
+ index: entries.length,
1420
+ animate: animate()
1421
+ } : { type: "PUSH", entry, animate: animate() }
1422
+ );
1190
1423
  return true;
1191
1424
  },
1192
1425
  [resolveOrNotify]
1193
1426
  );
1194
1427
  const replace = useCallback(
1195
1428
  (path, data) => {
1196
- if (busyRef.current) return false;
1429
+ if (isLocked()) return false;
1197
1430
  const hit = resolveOrNotify(path);
1198
1431
  if (!hit) return false;
1199
- const entry = createEntry(hit, path, data);
1432
+ const entry = newEntry(hit, path, data);
1200
1433
  history.replace(path, {
1201
1434
  key: entry.key,
1202
1435
  index: stateRef.current.entries.length - 1,
@@ -1209,19 +1442,44 @@ function useStackNavigation(options) {
1209
1442
  );
1210
1443
  const reset = useCallback(
1211
1444
  (path, data) => {
1212
- if (busyRef.current) return false;
1445
+ if (isLocked()) return false;
1213
1446
  const hit = resolveOrNotify(path);
1214
1447
  if (!hit) return false;
1215
- const entry = createEntry(hit, path, data);
1216
- history.replace(path, { key: entry.key, index: 0, data });
1448
+ const entry = newEntry(hit, path, data);
1449
+ const depth = stateRef.current.entries.length;
1217
1450
  dispatch({ type: "RESET", entry });
1451
+ const commit = () => {
1452
+ history.replace(path, { key: entry.key, index: 0, data });
1453
+ };
1454
+ if (depth === 1) commit();
1455
+ else void travel(-(depth - 1), true).then(commit);
1218
1456
  return true;
1219
1457
  },
1220
1458
  [resolveOrNotify]
1221
1459
  );
1460
+ const resetPush = useCallback(
1461
+ (path, data) => {
1462
+ const { entries } = stateRef.current;
1463
+ if (entries.length === 1) return push(path, data);
1464
+ if (isLocked()) return false;
1465
+ const hit = resolveOrNotify(path);
1466
+ if (!hit) return false;
1467
+ const revivable = findRevivable(stateRef.current, path, {
1468
+ includeRemovable: true
1469
+ });
1470
+ const entry = revivable ? { ...revivable, data } : newEntry(hit, path, data);
1471
+ busyRef.current = true;
1472
+ dispatch({ type: "RESET_PUSH", entry, animate: animate() });
1473
+ void travel(-(entries.length - 1), true).then(() => {
1474
+ history.push(path, { key: entry.key, index: 1, data });
1475
+ });
1476
+ return true;
1477
+ },
1478
+ [push, resolveOrNotify]
1479
+ );
1222
1480
  const pop = useCallback((count = 1) => {
1223
1481
  var _a, _b;
1224
- if (busyRef.current) return false;
1482
+ if (isLocked()) return false;
1225
1483
  const { entries } = stateRef.current;
1226
1484
  if (entries.length <= 1) {
1227
1485
  (_b = (_a = optionsRef.current).onBackAtRoot) == null ? void 0 : _b.call(_a);
@@ -1231,7 +1489,7 @@ function useStackNavigation(options) {
1231
1489
  const targetIndex = entries.length - 1 - delta;
1232
1490
  busyRef.current = true;
1233
1491
  dispatch({ type: "POP_TO", index: targetIndex, animate: animate() });
1234
- void history.travel(-delta);
1492
+ void travel(-delta, false);
1235
1493
  return true;
1236
1494
  }, []);
1237
1495
  const back = useCallback(() => pop(1), [pop]);
@@ -1250,8 +1508,12 @@ function useStackNavigation(options) {
1250
1508
  const endTransition = useCallback(() => {
1251
1509
  dispatch({ type: "END_TRANSITION" });
1252
1510
  }, []);
1253
- useEffect4(() => {
1511
+ useEffect5(() => {
1254
1512
  const onPop = (historyState) => {
1513
+ if (ignorePopRef.current > 0) {
1514
+ ignorePopRef.current -= 1;
1515
+ return;
1516
+ }
1255
1517
  const { entries } = stateRef.current;
1256
1518
  const index = historyState.key ? entries.findIndex((entry2) => entry2.key === historyState.key) : -1;
1257
1519
  if (index !== -1) {
@@ -1261,12 +1523,26 @@ function useStackNavigation(options) {
1261
1523
  }
1262
1524
  return;
1263
1525
  }
1526
+ const cachedEntry = historyState.key ? stateRef.current.cached.find((item) => item.key === historyState.key) : void 0;
1527
+ const targetIndex = historyState.index;
1528
+ if (cachedEntry && typeof targetIndex === "number" && // 루트 자리(0)는 되살리지 않는다. 아래 일반 처리(reset)로 넘긴다
1529
+ targetIndex >= 1 && targetIndex <= entries.length) {
1530
+ busyRef.current = true;
1531
+ dispatch({
1532
+ type: "REVIVE",
1533
+ entry: { ...cachedEntry, data: historyState.data },
1534
+ direction: targetIndex === entries.length ? "push" : "pop",
1535
+ index: targetIndex,
1536
+ animate: animate()
1537
+ });
1538
+ return;
1539
+ }
1264
1540
  const hit = resolveOrNotify(historyState.path);
1265
1541
  if (!hit) {
1266
1542
  busyRef.current = false;
1267
1543
  return;
1268
1544
  }
1269
- const entry = createEntry(
1545
+ const entry = newEntry(
1270
1546
  hit,
1271
1547
  historyState.path,
1272
1548
  historyState.data,
@@ -1284,8 +1560,8 @@ function useStackNavigation(options) {
1284
1560
  removeListener();
1285
1561
  };
1286
1562
  }, [resolveOrNotify]);
1287
- const restoredRef = useRef4(false);
1288
- useEffect4(() => {
1563
+ const restoredRef = useRef5(false);
1564
+ useEffect5(() => {
1289
1565
  if (restoredRef.current) return;
1290
1566
  restoredRef.current = true;
1291
1567
  const root = stateRef.current.entries[0];
@@ -1296,7 +1572,7 @@ function useStackNavigation(options) {
1296
1572
  return;
1297
1573
  }
1298
1574
  const savedData = history.getCurrentState().data;
1299
- const top = createEntry(hit, url, savedData);
1575
+ const top = newEntry(hit, url, savedData);
1300
1576
  if (optionsRef.current.restoreStack === "top-only") {
1301
1577
  history.replace(url, { key: top.key, index: 0, data: savedData });
1302
1578
  dispatch({ type: "RESTORE", entries: [top] });
@@ -1306,7 +1582,7 @@ function useStackNavigation(options) {
1306
1582
  history.push(url, { key: top.key, index: 1, data: savedData });
1307
1583
  dispatch({ type: "RESTORE", entries: [root, top] });
1308
1584
  }, [resolve]);
1309
- useEffect4(() => {
1585
+ useEffect5(() => {
1310
1586
  if (!state.transition) {
1311
1587
  busyRef.current = false;
1312
1588
  return;
@@ -1317,14 +1593,24 @@ function useStackNavigation(options) {
1317
1593
  );
1318
1594
  return () => clearTimeout(timer);
1319
1595
  }, [state, endTransition]);
1320
- return { state, push, replace, reset, back, pop, popTo, endTransition };
1596
+ return {
1597
+ state,
1598
+ push,
1599
+ replace,
1600
+ reset,
1601
+ resetPush,
1602
+ back,
1603
+ pop,
1604
+ popTo,
1605
+ endTransition
1606
+ };
1321
1607
  }
1322
1608
 
1323
1609
  // src/stack-router/stack-router-provider.tsx
1324
1610
  import { jsx as jsx5, jsxs as jsxs3 } from "@emotion/react/jsx-runtime";
1325
1611
  var StackRouterContext = createContext2(null);
1326
1612
  var useStackRouter = () => {
1327
- const context = useContext2(StackRouterContext);
1613
+ const context = useContext3(StackRouterContext);
1328
1614
  if (!context) {
1329
1615
  throw new Error("useStackRouter must be used within a StackRouterProvider");
1330
1616
  }
@@ -1334,6 +1620,8 @@ var isDevEnv = () => {
1334
1620
  var _a;
1335
1621
  return typeof process !== "undefined" && ((_a = process.env) == null ? void 0 : _a.NODE_ENV) === "development";
1336
1622
  };
1623
+ var MIN_MOUNTED_PAGES = 1;
1624
+ var CACHED_ACTIVITY_INDEX = -1;
1337
1625
  var StackRouterProvider = ({
1338
1626
  initialActivity,
1339
1627
  Activities = [],
@@ -1343,6 +1631,8 @@ var StackRouterProvider = ({
1343
1631
  transition = "slide",
1344
1632
  duration = STACK_TRANSITION_MS,
1345
1633
  restoreStack = "with-root",
1634
+ keepAliveLimit = KEEP_ALIVE_LIMIT,
1635
+ maxMountedPages,
1346
1636
  safeArea = false,
1347
1637
  onBackAtRoot,
1348
1638
  onNotFound,
@@ -1356,6 +1646,7 @@ var StackRouterProvider = ({
1356
1646
  activities: Activities,
1357
1647
  duration: effectiveDuration,
1358
1648
  restoreStack,
1649
+ keepAliveLimit,
1359
1650
  onBackAtRoot,
1360
1651
  onNotFound
1361
1652
  });
@@ -1387,6 +1678,7 @@ var StackRouterProvider = ({
1387
1678
  push: navigation.push,
1388
1679
  replace: navigation.replace,
1389
1680
  reset: navigation.reset,
1681
+ resetPush: navigation.resetPush,
1390
1682
  back: navigation.back,
1391
1683
  pop: navigation.pop,
1392
1684
  popTo: navigation.popTo,
@@ -1397,11 +1689,60 @@ var StackRouterProvider = ({
1397
1689
  useStackBridgeSync(bridge, value, onBackAtRoot);
1398
1690
  const direction = (_b = (_a = state.transition) == null ? void 0 : _a.type) != null ? _b : "push";
1399
1691
  const pages = getRenderedPages(state);
1692
+ const stackIndexByKey = new Map(
1693
+ state.entries.map((entry, index) => [entry.key, index])
1694
+ );
1695
+ const activityIndexOf = (entry) => {
1696
+ const stackIndex = stackIndexByKey.get(entry.key);
1697
+ if (stackIndex !== void 0) return stackIndex;
1698
+ const leavingIndex = state.leaving.findIndex(
1699
+ (item) => item.key === entry.key
1700
+ );
1701
+ return leavingIndex === -1 ? CACHED_ACTIVITY_INDEX : state.entries.length + leavingIndex;
1702
+ };
1703
+ const mountLimit = maxMountedPages === void 0 || !Number.isFinite(maxMountedPages) ? Infinity : Math.max(MIN_MOUNTED_PAGES, maxMountedPages);
1704
+ const mountedKeysRef = useRef6(/* @__PURE__ */ new Set());
1705
+ const wasMounted = mountedKeysRef.current;
1706
+ const shouldMount = (page) => {
1707
+ const { entry, pageState } = page;
1708
+ if (pageState === "cached" || entry.keepAlive) return true;
1709
+ const stackIndex = stackIndexByKey.get(entry.key);
1710
+ if (stackIndex === void 0) return wasMounted.has(entry.key);
1711
+ if (pageState === "enter" || pageState === "exit") return true;
1712
+ return state.entries.length - stackIndex <= mountLimit;
1713
+ };
1714
+ const mountedKeys = new Set(
1715
+ pages.filter(shouldMount).map((page) => page.entry.key)
1716
+ );
1717
+ mountedKeysRef.current = mountedKeys;
1718
+ const mountedCount = mountedKeys.size;
1719
+ const domOrderRef = useRef6([]);
1720
+ const currentKeys = new Set(pages.map((page) => page.entry.key));
1721
+ const knownKeys = new Set(domOrderRef.current);
1722
+ domOrderRef.current = [
1723
+ ...domOrderRef.current.filter((key) => currentKeys.has(key)),
1724
+ ...pages.map((page) => page.entry.key).filter((key) => !knownKeys.has(key))
1725
+ ];
1726
+ const domOrder = domOrderRef.current;
1727
+ const domPages = pages.map((page, index) => ({ page, index })).sort(
1728
+ (a, b) => domOrder.indexOf(a.page.entry.key) - domOrder.indexOf(b.page.entry.key)
1729
+ );
1730
+ const hasWarnedRef = useRef6(false);
1731
+ useEffect6(() => {
1732
+ if (hasWarnedRef.current || !isDevEnv() || mountedCount <= DEV_MOUNTED_PAGES_WARN) {
1733
+ return;
1734
+ }
1735
+ hasWarnedRef.current = true;
1736
+ console.warn(
1737
+ `[webviewkit] \uB9C8\uC6B4\uD2B8\uB41C \uD398\uC774\uC9C0\uAC00 ${mountedCount}\uAC1C\uC785\uB2C8\uB2E4. \uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uC774 \uCEE4\uC9C8 \uC218 \uC788\uC73C\uB2C8 maxMountedPages \uB85C \uC0C1\uD55C\uC744 \uB450\uC138\uC694.`
1738
+ );
1739
+ }, [mountedCount]);
1400
1740
  const showDevTools = enableDevTools != null ? enableDevTools : isDevEnv();
1401
1741
  return /* @__PURE__ */ jsxs3(StackRouterContext.Provider, { value, children: [
1402
- /* @__PURE__ */ jsx5(LayoutContainer, { safeArea, children: /* @__PURE__ */ jsxs3(Layout, { maxWidth, "data-testid": "layout", children: [
1403
- isShowHeader && /* @__PURE__ */ jsx5(header_default, {}),
1404
- /* @__PURE__ */ jsx5(PageContainer, { children: pages.map(({ entry, pageState, isTop }, index) => /* @__PURE__ */ jsx5(
1742
+ /* @__PURE__ */ jsx5(LayoutContainer, { safeArea, children: /* @__PURE__ */ jsx5(Layout, { maxWidth, "data-testid": "layout", children: /* @__PURE__ */ jsx5(PageContainer, { children: domPages.map(({ page, index }) => {
1743
+ var _a2;
1744
+ const { entry, pageState, isTop } = page;
1745
+ return /* @__PURE__ */ jsx5(
1405
1746
  ActivityContext.Provider,
1406
1747
  {
1407
1748
  value: {
@@ -1411,10 +1752,12 @@ var StackRouterProvider = ({
1411
1752
  params: entry.params,
1412
1753
  query: entry.query,
1413
1754
  data: entry.data,
1414
- index,
1415
- isTop
1755
+ index: activityIndexOf(entry),
1756
+ isTop,
1757
+ isCached: pageState === "cached",
1758
+ revision: (_a2 = entry.revision) != null ? _a2 : 0
1416
1759
  },
1417
- children: /* @__PURE__ */ jsx5(
1760
+ children: /* @__PURE__ */ jsxs3(
1418
1761
  StackPage,
1419
1762
  {
1420
1763
  pageState,
@@ -1424,13 +1767,16 @@ var StackRouterProvider = ({
1424
1767
  zIndex: index,
1425
1768
  isTop,
1426
1769
  onAnimationEnd: pageState === "enter" ? endTransition : void 0,
1427
- children: elementFor(entry.pattern)
1770
+ children: [
1771
+ mountedKeys.has(entry.key) && isShowHeader && /* @__PURE__ */ jsx5(header_default, {}),
1772
+ mountedKeys.has(entry.key) ? elementFor(entry.pattern) : null
1773
+ ]
1428
1774
  }
1429
1775
  )
1430
1776
  },
1431
1777
  entry.key
1432
- )) })
1433
- ] }) }),
1778
+ );
1779
+ }) }) }) }),
1434
1780
  /* @__PURE__ */ jsx5(
1435
1781
  StackDevRoadmap,
1436
1782
  {
@@ -1534,6 +1880,7 @@ export {
1534
1880
  FLUTTER_READY_EVENT,
1535
1881
  FLUTTER_UNIFIED_HANDLER,
1536
1882
  HistoryAction,
1883
+ KEEP_ALIVE_LIMIT,
1537
1884
  RN_BRIDGE_HANDLER,
1538
1885
  STACK_BRIDGE_METHODS,
1539
1886
  STACK_TRANSITION_MS,
@@ -1567,6 +1914,7 @@ export {
1567
1914
  serializeBridgeMessage,
1568
1915
  toStackPath,
1569
1916
  useActivity,
1917
+ useOnRevive,
1570
1918
  useParams,
1571
1919
  useStackRouter,
1572
1920
  waitForBridgeHost