@moku-labs/web 2.4.0 → 2.4.1

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/browser.mjs CHANGED
@@ -3645,11 +3645,13 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
3645
3645
  * @param navigation - The Navigation API object to attach the listener to.
3646
3646
  * @param handlers - The navigation lifecycle callbacks.
3647
3647
  * @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
3648
+ * @param isOwnPush - True while the kernel's programmatic navigation runs its own
3649
+ * `history.pushState`: the `navigate` event it fires is left alone, the kernel runs it once.
3648
3650
  * @returns A teardown that removes the `navigate` listener.
3649
3651
  * @example
3650
3652
  * const dispose = attachNavigationApi(navigation, handlers);
3651
3653
  */
3652
- function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal)) {
3654
+ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal), isOwnPush = () => false) {
3653
3655
  /**
3654
3656
  * Handle a `navigate` event: classify, then intercept with fetch-and-swap.
3655
3657
  *
@@ -3658,6 +3660,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
3658
3660
  * navigation.addEventListener("navigate", onNavigate);
3659
3661
  */
3660
3662
  const onNavigate = (navEvent) => {
3663
+ if (isOwnPush()) return;
3661
3664
  const url = new URL(navEvent.destination.url);
3662
3665
  if (!navEvent.canIntercept || navEvent.hashChange || navEvent.downloadRequest) return;
3663
3666
  if (!isInternalLink(url)) return;
@@ -3690,13 +3693,15 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
3690
3693
  *
3691
3694
  * @param handlers - The navigation lifecycle callbacks the kernel supplies.
3692
3695
  * @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
3696
+ * @param isOwnPush - True while the kernel runs its own `history.pushState` (Navigation API path
3697
+ * only; the History fallback never sees an event for it).
3693
3698
  * @returns A teardown removing all attached listeners.
3694
3699
  * @example
3695
- * const dispose = attachRouter(handlers, navigate);
3700
+ * const dispose = attachRouter(handlers, navigate, () => pushingOwnUrl);
3696
3701
  */
3697
- function attachRouter(handlers, navigate) {
3702
+ function attachRouter(handlers, navigate, isOwnPush) {
3698
3703
  const navigation = getNavigation();
3699
- return navigation ? attachNavigationApi(navigation, handlers, navigate) : attachHistoryFallback(handlers, navigate);
3704
+ return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
3700
3705
  }
3701
3706
  //#endregion
3702
3707
  //#region src/plugins/spa/state.ts
@@ -3861,6 +3866,7 @@ function createSpaKernel(state, config, emit, deps) {
3861
3866
  let progress;
3862
3867
  let pendingScrollToTop = true;
3863
3868
  let pendingTransition = resolveTransition(resolved.defaultTransition);
3869
+ let pushingOwnUrl = false;
3864
3870
  /**
3865
3871
  * Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
3866
3872
  * consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
@@ -4173,8 +4179,9 @@ function createSpaKernel(state, config, emit, deps) {
4173
4179
  * Navigation API commits the address bar before the interceptor calls {@link navigate}; a
4174
4180
  * programmatic call does NOT, so it must commit the URL itself first — otherwise the content
4175
4181
  * swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
4176
- * `history.pushState` updates the URL WITHOUT firing a `navigate` event, so it can't
4177
- * double-trigger the interceptor. The current scroll is saved first so a later back restores it.
4182
+ * Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
4183
+ * (Chrome, Edge, Safari 26): `pushingOwnUrl` marks it, so the interceptor does not run this
4184
+ * navigation a second time. The current scroll is saved first so a later back restores it.
4178
4185
  *
4179
4186
  * @param path - The destination path (pathname + optional search).
4180
4187
  * @param scroll - The optional per-call scroll override.
@@ -4185,7 +4192,12 @@ function createSpaKernel(state, config, emit, deps) {
4185
4192
  if (typeof document === "undefined") return;
4186
4193
  if (path !== currentLocationUrl()) {
4187
4194
  saveScrollPosition(currentLocationUrl());
4188
- history.pushState({}, "", path);
4195
+ pushingOwnUrl = true;
4196
+ try {
4197
+ history.pushState({}, "", path);
4198
+ } finally {
4199
+ pushingOwnUrl = false;
4200
+ }
4189
4201
  }
4190
4202
  navigate(path, true, void 0, scroll).catch(() => {});
4191
4203
  };
@@ -4214,7 +4226,7 @@ function createSpaKernel(state, config, emit, deps) {
4214
4226
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
4215
4227
  progress = createProgressBar(resolved.progressBar);
4216
4228
  state.currentUrl = currentLocationUrl();
4217
- state.destroyRouter = attachRouter(handlers, navigate);
4229
+ state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
4218
4230
  const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
4219
4231
  const hit = deps.router.match(matchPath);
4220
4232
  if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
package/dist/index.cjs CHANGED
@@ -10739,11 +10739,13 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
10739
10739
  * @param navigation - The Navigation API object to attach the listener to.
10740
10740
  * @param handlers - The navigation lifecycle callbacks.
10741
10741
  * @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
10742
+ * @param isOwnPush - True while the kernel's programmatic navigation runs its own
10743
+ * `history.pushState`: the `navigate` event it fires is left alone, the kernel runs it once.
10742
10744
  * @returns A teardown that removes the `navigate` listener.
10743
10745
  * @example
10744
10746
  * const dispose = attachNavigationApi(navigation, handlers);
10745
10747
  */
10746
- function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal)) {
10748
+ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal), isOwnPush = () => false) {
10747
10749
  /**
10748
10750
  * Handle a `navigate` event: classify, then intercept with fetch-and-swap.
10749
10751
  *
@@ -10752,6 +10754,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
10752
10754
  * navigation.addEventListener("navigate", onNavigate);
10753
10755
  */
10754
10756
  const onNavigate = (navEvent) => {
10757
+ if (isOwnPush()) return;
10755
10758
  const url = new URL(navEvent.destination.url);
10756
10759
  if (!navEvent.canIntercept || navEvent.hashChange || navEvent.downloadRequest) return;
10757
10760
  if (!isInternalLink(url)) return;
@@ -10784,13 +10787,15 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
10784
10787
  *
10785
10788
  * @param handlers - The navigation lifecycle callbacks the kernel supplies.
10786
10789
  * @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
10790
+ * @param isOwnPush - True while the kernel runs its own `history.pushState` (Navigation API path
10791
+ * only; the History fallback never sees an event for it).
10787
10792
  * @returns A teardown removing all attached listeners.
10788
10793
  * @example
10789
- * const dispose = attachRouter(handlers, navigate);
10794
+ * const dispose = attachRouter(handlers, navigate, () => pushingOwnUrl);
10790
10795
  */
10791
- function attachRouter(handlers, navigate) {
10796
+ function attachRouter(handlers, navigate, isOwnPush) {
10792
10797
  const navigation = getNavigation();
10793
- return navigation ? attachNavigationApi(navigation, handlers, navigate) : attachHistoryFallback(handlers, navigate);
10798
+ return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
10794
10799
  }
10795
10800
  //#endregion
10796
10801
  //#region src/plugins/spa/state.ts
@@ -10955,6 +10960,7 @@ function createSpaKernel(state, config, emit, deps) {
10955
10960
  let progress;
10956
10961
  let pendingScrollToTop = true;
10957
10962
  let pendingTransition = resolveTransition(resolved.defaultTransition);
10963
+ let pushingOwnUrl = false;
10958
10964
  /**
10959
10965
  * Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
10960
10966
  * consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
@@ -11267,8 +11273,9 @@ function createSpaKernel(state, config, emit, deps) {
11267
11273
  * Navigation API commits the address bar before the interceptor calls {@link navigate}; a
11268
11274
  * programmatic call does NOT, so it must commit the URL itself first — otherwise the content
11269
11275
  * swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
11270
- * `history.pushState` updates the URL WITHOUT firing a `navigate` event, so it can't
11271
- * double-trigger the interceptor. The current scroll is saved first so a later back restores it.
11276
+ * Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
11277
+ * (Chrome, Edge, Safari 26): `pushingOwnUrl` marks it, so the interceptor does not run this
11278
+ * navigation a second time. The current scroll is saved first so a later back restores it.
11272
11279
  *
11273
11280
  * @param path - The destination path (pathname + optional search).
11274
11281
  * @param scroll - The optional per-call scroll override.
@@ -11279,7 +11286,12 @@ function createSpaKernel(state, config, emit, deps) {
11279
11286
  if (typeof document === "undefined") return;
11280
11287
  if (path !== currentLocationUrl()) {
11281
11288
  saveScrollPosition(currentLocationUrl());
11282
- history.pushState({}, "", path);
11289
+ pushingOwnUrl = true;
11290
+ try {
11291
+ history.pushState({}, "", path);
11292
+ } finally {
11293
+ pushingOwnUrl = false;
11294
+ }
11283
11295
  }
11284
11296
  navigate(path, true, void 0, scroll).catch(() => {});
11285
11297
  };
@@ -11308,7 +11320,7 @@ function createSpaKernel(state, config, emit, deps) {
11308
11320
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
11309
11321
  progress = createProgressBar(resolved.progressBar);
11310
11322
  state.currentUrl = currentLocationUrl();
11311
- state.destroyRouter = attachRouter(handlers, navigate);
11323
+ state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
11312
11324
  const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
11313
11325
  const hit = deps.router.match(matchPath);
11314
11326
  if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
package/dist/index.mjs CHANGED
@@ -10695,11 +10695,13 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
10695
10695
  * @param navigation - The Navigation API object to attach the listener to.
10696
10696
  * @param handlers - The navigation lifecycle callbacks.
10697
10697
  * @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
10698
+ * @param isOwnPush - True while the kernel's programmatic navigation runs its own
10699
+ * `history.pushState`: the `navigate` event it fires is left alone, the kernel runs it once.
10698
10700
  * @returns A teardown that removes the `navigate` listener.
10699
10701
  * @example
10700
10702
  * const dispose = attachNavigationApi(navigation, handlers);
10701
10703
  */
10702
- function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal)) {
10704
+ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal), isOwnPush = () => false) {
10703
10705
  /**
10704
10706
  * Handle a `navigate` event: classify, then intercept with fetch-and-swap.
10705
10707
  *
@@ -10708,6 +10710,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
10708
10710
  * navigation.addEventListener("navigate", onNavigate);
10709
10711
  */
10710
10712
  const onNavigate = (navEvent) => {
10713
+ if (isOwnPush()) return;
10711
10714
  const url = new URL(navEvent.destination.url);
10712
10715
  if (!navEvent.canIntercept || navEvent.hashChange || navEvent.downloadRequest) return;
10713
10716
  if (!isInternalLink(url)) return;
@@ -10740,13 +10743,15 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
10740
10743
  *
10741
10744
  * @param handlers - The navigation lifecycle callbacks the kernel supplies.
10742
10745
  * @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
10746
+ * @param isOwnPush - True while the kernel runs its own `history.pushState` (Navigation API path
10747
+ * only; the History fallback never sees an event for it).
10743
10748
  * @returns A teardown removing all attached listeners.
10744
10749
  * @example
10745
- * const dispose = attachRouter(handlers, navigate);
10750
+ * const dispose = attachRouter(handlers, navigate, () => pushingOwnUrl);
10746
10751
  */
10747
- function attachRouter(handlers, navigate) {
10752
+ function attachRouter(handlers, navigate, isOwnPush) {
10748
10753
  const navigation = getNavigation();
10749
- return navigation ? attachNavigationApi(navigation, handlers, navigate) : attachHistoryFallback(handlers, navigate);
10754
+ return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
10750
10755
  }
10751
10756
  //#endregion
10752
10757
  //#region src/plugins/spa/state.ts
@@ -10911,6 +10916,7 @@ function createSpaKernel(state, config, emit, deps) {
10911
10916
  let progress;
10912
10917
  let pendingScrollToTop = true;
10913
10918
  let pendingTransition = resolveTransition(resolved.defaultTransition);
10919
+ let pushingOwnUrl = false;
10914
10920
  /**
10915
10921
  * Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
10916
10922
  * consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
@@ -11223,8 +11229,9 @@ function createSpaKernel(state, config, emit, deps) {
11223
11229
  * Navigation API commits the address bar before the interceptor calls {@link navigate}; a
11224
11230
  * programmatic call does NOT, so it must commit the URL itself first — otherwise the content
11225
11231
  * swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
11226
- * `history.pushState` updates the URL WITHOUT firing a `navigate` event, so it can't
11227
- * double-trigger the interceptor. The current scroll is saved first so a later back restores it.
11232
+ * Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
11233
+ * (Chrome, Edge, Safari 26): `pushingOwnUrl` marks it, so the interceptor does not run this
11234
+ * navigation a second time. The current scroll is saved first so a later back restores it.
11228
11235
  *
11229
11236
  * @param path - The destination path (pathname + optional search).
11230
11237
  * @param scroll - The optional per-call scroll override.
@@ -11235,7 +11242,12 @@ function createSpaKernel(state, config, emit, deps) {
11235
11242
  if (typeof document === "undefined") return;
11236
11243
  if (path !== currentLocationUrl()) {
11237
11244
  saveScrollPosition(currentLocationUrl());
11238
- history.pushState({}, "", path);
11245
+ pushingOwnUrl = true;
11246
+ try {
11247
+ history.pushState({}, "", path);
11248
+ } finally {
11249
+ pushingOwnUrl = false;
11250
+ }
11239
11251
  }
11240
11252
  navigate(path, true, void 0, scroll).catch(() => {});
11241
11253
  };
@@ -11264,7 +11276,7 @@ function createSpaKernel(state, config, emit, deps) {
11264
11276
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
11265
11277
  progress = createProgressBar(resolved.progressBar);
11266
11278
  state.currentUrl = currentLocationUrl();
11267
- state.destroyRouter = attachRouter(handlers, navigate);
11279
+ state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
11268
11280
  const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
11269
11281
  const hit = deps.router.match(matchPath);
11270
11282
  if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
package/package.json CHANGED
@@ -137,5 +137,5 @@
137
137
  "release:doctor": "moku-release doctor",
138
138
  "release": "moku-release"
139
139
  },
140
- "version": "2.4.0"
140
+ "version": "2.4.1"
141
141
  }