@moku-labs/web 2.4.1 → 2.4.3

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/README.md CHANGED
@@ -140,7 +140,7 @@ route("/board/{id}/issue/{issueId}")
140
140
 
141
141
  - **`viewTransitions`** accepts `boolean` (`true` → `"crossfade"`, `false` → `"none"`) **or** a named `TransitionMode` (`"none" | "crossfade" | "slide" | "morph"`). A named transition tags the swap so your CSS can target it two ways — `:root[data-view-transition~="slide"]` (every View-Transitions engine) and the standards-track `:active-view-transition-type(slide)` — and a shared `view-transition-name` left across the swap morphs one element into another. Reduced-motion and unsupported browsers fall back to an instant swap.
142
142
  - **`scrollRestoration`** is `"top"` (reset, the default) or `"preserve"` (keep the current scroll — e.g. opening an overlay route over content that must stay still). Back/forward always restores its saved position.
143
- - **`ctx.navigate(path, options?)`** — every island can navigate programmatically with no `app` handle (an always-present context member, like `ctx.set`/`ctx.url`). `app.spa.navigate` takes the same optional `{ scroll }`.
143
+ - **`ctx.navigate(path, options?)`** — every island can navigate programmatically with no `app` handle (an always-present context member, like `ctx.set`/`ctx.url`). `app.spa.navigate` takes the same optional `{ scroll, replace }`: `replace: true` swaps the current history entry instead of adding one, so Back skips the page navigated from (e.g. the same page in another language).
144
144
  - **`navigate(path, options?)` / `hardNavigate(url)`** — importable from `@moku-labs/web/browser` for module-scope callers (shared chrome/nav helpers with no `app` or island `ctx`); they bind to the single booted app and no-op before `app.start()`. `hardNavigate` (also `app.spa.hardNavigate`) does a REAL full-page load for crossing a boundary the SPA can't swap — a different layout or an auth split — by detaching the SPA's own interceptor first, so `location.assign` isn't caught and converted to a region swap.
145
145
 
146
146
  ```ts
@@ -920,6 +920,11 @@ declare namespace types_d_exports$5 {
920
920
  interface NavigateOptions {
921
921
  /** Override the scroll behaviour for this single navigation (else the route/app default). */
922
922
  scroll?: ScrollMode;
923
+ /**
924
+ * Replace the current history entry instead of adding one (`history.replaceState`), so Back
925
+ * skips the page navigated from; e.g. the same page in another language.
926
+ */
927
+ replace?: boolean;
923
928
  }
924
929
  /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
925
930
  type SpaEvents = {
package/dist/browser.mjs CHANGED
@@ -3867,6 +3867,17 @@ function createSpaKernel(state, config, emit, deps) {
3867
3867
  let pendingScrollToTop = true;
3868
3868
  let pendingTransition = resolveTransition(resolved.defaultTransition);
3869
3869
  let pushingOwnUrl = false;
3870
+ let programmatic;
3871
+ /**
3872
+ * Aborts the programmatic navigation still loading, if any.
3873
+ *
3874
+ * @example
3875
+ * supersedeProgrammatic(); // a link click or back/forward starts
3876
+ */
3877
+ const supersedeProgrammatic = () => {
3878
+ programmatic?.abort();
3879
+ programmatic = void 0;
3880
+ };
3870
3881
  /**
3871
3882
  * Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
3872
3883
  * consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
@@ -4181,25 +4192,35 @@ function createSpaKernel(state, config, emit, deps) {
4181
4192
  * swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
4182
4193
  * Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
4183
4194
  * (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.
4195
+ * navigation a second time. The current scroll is saved first so a later back restores it. It
4196
+ * carries its own abort signal: any later navigation aborts it before it can swap.
4197
+ *
4198
+ * `options.replace` swaps the current history entry (`replaceState`) instead of adding one.
4185
4199
  *
4186
4200
  * @param path - The destination path (pathname + optional search).
4187
- * @param scroll - The optional per-call scroll override.
4201
+ * @param options - Optional per-call overrides: `scroll`, `replace`.
4188
4202
  * @example
4189
- * navigateProgrammatic("/board/abc/issue/1", "preserve");
4203
+ * navigateProgrammatic("/board/abc/issue/1", { scroll: "preserve" });
4190
4204
  */
4191
- const navigateProgrammatic = (path, scroll) => {
4205
+ const navigateProgrammatic = (path, options = {}) => {
4192
4206
  if (typeof document === "undefined") return;
4207
+ const { scroll, replace = false } = options;
4193
4208
  if (path !== currentLocationUrl()) {
4194
4209
  saveScrollPosition(currentLocationUrl());
4195
4210
  pushingOwnUrl = true;
4196
4211
  try {
4197
- history.pushState({}, "", path);
4212
+ if (replace) history.replaceState({}, "", path);
4213
+ else history.pushState({}, "", path);
4198
4214
  } finally {
4199
4215
  pushingOwnUrl = false;
4200
4216
  }
4201
4217
  }
4202
- navigate(path, true, void 0, scroll).catch(() => {});
4218
+ supersedeProgrammatic();
4219
+ const controller = new AbortController();
4220
+ programmatic = controller;
4221
+ navigate(path, true, controller.signal, scroll).catch(() => {}).finally(() => {
4222
+ if (programmatic === controller) programmatic = void 0;
4223
+ });
4203
4224
  };
4204
4225
  return {
4205
4226
  /**
@@ -4212,7 +4233,7 @@ function createSpaKernel(state, config, emit, deps) {
4212
4233
  for (const island of resolved.islands) registerIsland(state, island);
4213
4234
  state.currentUrl = currentLocationUrl();
4214
4235
  state.navigate = (path, options) => {
4215
- navigateProgrammatic(path, options?.scroll);
4236
+ navigateProgrammatic(path, options);
4216
4237
  };
4217
4238
  },
4218
4239
  /**
@@ -4226,7 +4247,10 @@ function createSpaKernel(state, config, emit, deps) {
4226
4247
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
4227
4248
  progress = createProgressBar(resolved.progressBar);
4228
4249
  state.currentUrl = currentLocationUrl();
4229
- state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
4250
+ state.destroyRouter = attachRouter(handlers, (pathname, scrollToTop, signal, scrollOverride) => {
4251
+ supersedeProgrammatic();
4252
+ return navigate(pathname, scrollToTop, signal, scrollOverride);
4253
+ }, () => pushingOwnUrl);
4230
4254
  const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
4231
4255
  const hit = deps.router.match(matchPath);
4232
4256
  if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
@@ -4244,16 +4268,16 @@ function createSpaKernel(state, config, emit, deps) {
4244
4268
  registerIsland(state, island);
4245
4269
  },
4246
4270
  /**
4247
- * Process a navigation to `path` (fetch then swap; full reload on error). An optional
4248
- * per-call `scroll` override flows through to the swap's scroll intent.
4271
+ * Process a navigation to `path` (fetch then swap; full reload on error). Optional per-call
4272
+ * overrides: `scroll` for the swap's scroll intent, `replace` to replace the history entry.
4249
4273
  *
4250
4274
  * @param path - The target path to navigate to.
4251
- * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
4275
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve", replace: true }`).
4252
4276
  * @example
4253
4277
  * kernel.processNav("/about");
4254
4278
  */
4255
4279
  processNav(path, options) {
4256
- navigateProgrammatic(path, options?.scroll);
4280
+ navigateProgrammatic(path, options);
4257
4281
  },
4258
4282
  /**
4259
4283
  * Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
package/dist/index.cjs CHANGED
@@ -10961,6 +10961,17 @@ function createSpaKernel(state, config, emit, deps) {
10961
10961
  let pendingScrollToTop = true;
10962
10962
  let pendingTransition = resolveTransition(resolved.defaultTransition);
10963
10963
  let pushingOwnUrl = false;
10964
+ let programmatic;
10965
+ /**
10966
+ * Aborts the programmatic navigation still loading, if any.
10967
+ *
10968
+ * @example
10969
+ * supersedeProgrammatic(); // a link click or back/forward starts
10970
+ */
10971
+ const supersedeProgrammatic = () => {
10972
+ programmatic?.abort();
10973
+ programmatic = void 0;
10974
+ };
10964
10975
  /**
10965
10976
  * Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
10966
10977
  * consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
@@ -11275,25 +11286,35 @@ function createSpaKernel(state, config, emit, deps) {
11275
11286
  * swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
11276
11287
  * Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
11277
11288
  * (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.
11289
+ * navigation a second time. The current scroll is saved first so a later back restores it. It
11290
+ * carries its own abort signal: any later navigation aborts it before it can swap.
11291
+ *
11292
+ * `options.replace` swaps the current history entry (`replaceState`) instead of adding one.
11279
11293
  *
11280
11294
  * @param path - The destination path (pathname + optional search).
11281
- * @param scroll - The optional per-call scroll override.
11295
+ * @param options - Optional per-call overrides: `scroll`, `replace`.
11282
11296
  * @example
11283
- * navigateProgrammatic("/board/abc/issue/1", "preserve");
11297
+ * navigateProgrammatic("/board/abc/issue/1", { scroll: "preserve" });
11284
11298
  */
11285
- const navigateProgrammatic = (path, scroll) => {
11299
+ const navigateProgrammatic = (path, options = {}) => {
11286
11300
  if (typeof document === "undefined") return;
11301
+ const { scroll, replace = false } = options;
11287
11302
  if (path !== currentLocationUrl()) {
11288
11303
  saveScrollPosition(currentLocationUrl());
11289
11304
  pushingOwnUrl = true;
11290
11305
  try {
11291
- history.pushState({}, "", path);
11306
+ if (replace) history.replaceState({}, "", path);
11307
+ else history.pushState({}, "", path);
11292
11308
  } finally {
11293
11309
  pushingOwnUrl = false;
11294
11310
  }
11295
11311
  }
11296
- navigate(path, true, void 0, scroll).catch(() => {});
11312
+ supersedeProgrammatic();
11313
+ const controller = new AbortController();
11314
+ programmatic = controller;
11315
+ navigate(path, true, controller.signal, scroll).catch(() => {}).finally(() => {
11316
+ if (programmatic === controller) programmatic = void 0;
11317
+ });
11297
11318
  };
11298
11319
  return {
11299
11320
  /**
@@ -11306,7 +11327,7 @@ function createSpaKernel(state, config, emit, deps) {
11306
11327
  for (const island of resolved.islands) registerIsland(state, island);
11307
11328
  state.currentUrl = currentLocationUrl();
11308
11329
  state.navigate = (path, options) => {
11309
- navigateProgrammatic(path, options?.scroll);
11330
+ navigateProgrammatic(path, options);
11310
11331
  };
11311
11332
  },
11312
11333
  /**
@@ -11320,7 +11341,10 @@ function createSpaKernel(state, config, emit, deps) {
11320
11341
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
11321
11342
  progress = createProgressBar(resolved.progressBar);
11322
11343
  state.currentUrl = currentLocationUrl();
11323
- state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
11344
+ state.destroyRouter = attachRouter(handlers, (pathname, scrollToTop, signal, scrollOverride) => {
11345
+ supersedeProgrammatic();
11346
+ return navigate(pathname, scrollToTop, signal, scrollOverride);
11347
+ }, () => pushingOwnUrl);
11324
11348
  const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
11325
11349
  const hit = deps.router.match(matchPath);
11326
11350
  if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
@@ -11338,16 +11362,16 @@ function createSpaKernel(state, config, emit, deps) {
11338
11362
  registerIsland(state, island);
11339
11363
  },
11340
11364
  /**
11341
- * Process a navigation to `path` (fetch then swap; full reload on error). An optional
11342
- * per-call `scroll` override flows through to the swap's scroll intent.
11365
+ * Process a navigation to `path` (fetch then swap; full reload on error). Optional per-call
11366
+ * overrides: `scroll` for the swap's scroll intent, `replace` to replace the history entry.
11343
11367
  *
11344
11368
  * @param path - The target path to navigate to.
11345
- * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
11369
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve", replace: true }`).
11346
11370
  * @example
11347
11371
  * kernel.processNav("/about");
11348
11372
  */
11349
11373
  processNav(path, options) {
11350
- navigateProgrammatic(path, options?.scroll);
11374
+ navigateProgrammatic(path, options);
11351
11375
  },
11352
11376
  /**
11353
11377
  * Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
package/dist/index.d.cts CHANGED
@@ -921,6 +921,11 @@ declare namespace types_d_exports$8 {
921
921
  interface NavigateOptions {
922
922
  /** Override the scroll behaviour for this single navigation (else the route/app default). */
923
923
  scroll?: ScrollMode;
924
+ /**
925
+ * Replace the current history entry instead of adding one (`history.replaceState`), so Back
926
+ * skips the page navigated from; e.g. the same page in another language.
927
+ */
928
+ replace?: boolean;
924
929
  }
925
930
  /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
926
931
  type SpaEvents = {
package/dist/index.d.mts CHANGED
@@ -919,6 +919,11 @@ declare namespace types_d_exports$8 {
919
919
  interface NavigateOptions {
920
920
  /** Override the scroll behaviour for this single navigation (else the route/app default). */
921
921
  scroll?: ScrollMode;
922
+ /**
923
+ * Replace the current history entry instead of adding one (`history.replaceState`), so Back
924
+ * skips the page navigated from; e.g. the same page in another language.
925
+ */
926
+ replace?: boolean;
922
927
  }
923
928
  /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
924
929
  type SpaEvents = {
package/dist/index.mjs CHANGED
@@ -10917,6 +10917,17 @@ function createSpaKernel(state, config, emit, deps) {
10917
10917
  let pendingScrollToTop = true;
10918
10918
  let pendingTransition = resolveTransition(resolved.defaultTransition);
10919
10919
  let pushingOwnUrl = false;
10920
+ let programmatic;
10921
+ /**
10922
+ * Aborts the programmatic navigation still loading, if any.
10923
+ *
10924
+ * @example
10925
+ * supersedeProgrammatic(); // a link click or back/forward starts
10926
+ */
10927
+ const supersedeProgrammatic = () => {
10928
+ programmatic?.abort();
10929
+ programmatic = void 0;
10930
+ };
10920
10931
  /**
10921
10932
  * Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
10922
10933
  * consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
@@ -11231,25 +11242,35 @@ function createSpaKernel(state, config, emit, deps) {
11231
11242
  * swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
11232
11243
  * Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
11233
11244
  * (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.
11245
+ * navigation a second time. The current scroll is saved first so a later back restores it. It
11246
+ * carries its own abort signal: any later navigation aborts it before it can swap.
11247
+ *
11248
+ * `options.replace` swaps the current history entry (`replaceState`) instead of adding one.
11235
11249
  *
11236
11250
  * @param path - The destination path (pathname + optional search).
11237
- * @param scroll - The optional per-call scroll override.
11251
+ * @param options - Optional per-call overrides: `scroll`, `replace`.
11238
11252
  * @example
11239
- * navigateProgrammatic("/board/abc/issue/1", "preserve");
11253
+ * navigateProgrammatic("/board/abc/issue/1", { scroll: "preserve" });
11240
11254
  */
11241
- const navigateProgrammatic = (path, scroll) => {
11255
+ const navigateProgrammatic = (path, options = {}) => {
11242
11256
  if (typeof document === "undefined") return;
11257
+ const { scroll, replace = false } = options;
11243
11258
  if (path !== currentLocationUrl()) {
11244
11259
  saveScrollPosition(currentLocationUrl());
11245
11260
  pushingOwnUrl = true;
11246
11261
  try {
11247
- history.pushState({}, "", path);
11262
+ if (replace) history.replaceState({}, "", path);
11263
+ else history.pushState({}, "", path);
11248
11264
  } finally {
11249
11265
  pushingOwnUrl = false;
11250
11266
  }
11251
11267
  }
11252
- navigate(path, true, void 0, scroll).catch(() => {});
11268
+ supersedeProgrammatic();
11269
+ const controller = new AbortController();
11270
+ programmatic = controller;
11271
+ navigate(path, true, controller.signal, scroll).catch(() => {}).finally(() => {
11272
+ if (programmatic === controller) programmatic = void 0;
11273
+ });
11253
11274
  };
11254
11275
  return {
11255
11276
  /**
@@ -11262,7 +11283,7 @@ function createSpaKernel(state, config, emit, deps) {
11262
11283
  for (const island of resolved.islands) registerIsland(state, island);
11263
11284
  state.currentUrl = currentLocationUrl();
11264
11285
  state.navigate = (path, options) => {
11265
- navigateProgrammatic(path, options?.scroll);
11286
+ navigateProgrammatic(path, options);
11266
11287
  };
11267
11288
  },
11268
11289
  /**
@@ -11276,7 +11297,10 @@ function createSpaKernel(state, config, emit, deps) {
11276
11297
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
11277
11298
  progress = createProgressBar(resolved.progressBar);
11278
11299
  state.currentUrl = currentLocationUrl();
11279
- state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
11300
+ state.destroyRouter = attachRouter(handlers, (pathname, scrollToTop, signal, scrollOverride) => {
11301
+ supersedeProgrammatic();
11302
+ return navigate(pathname, scrollToTop, signal, scrollOverride);
11303
+ }, () => pushingOwnUrl);
11280
11304
  const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
11281
11305
  const hit = deps.router.match(matchPath);
11282
11306
  if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
@@ -11294,16 +11318,16 @@ function createSpaKernel(state, config, emit, deps) {
11294
11318
  registerIsland(state, island);
11295
11319
  },
11296
11320
  /**
11297
- * Process a navigation to `path` (fetch then swap; full reload on error). An optional
11298
- * per-call `scroll` override flows through to the swap's scroll intent.
11321
+ * Process a navigation to `path` (fetch then swap; full reload on error). Optional per-call
11322
+ * overrides: `scroll` for the swap's scroll intent, `replace` to replace the history entry.
11299
11323
  *
11300
11324
  * @param path - The target path to navigate to.
11301
- * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
11325
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve", replace: true }`).
11302
11326
  * @example
11303
11327
  * kernel.processNav("/about");
11304
11328
  */
11305
11329
  processNav(path, options) {
11306
- navigateProgrammatic(path, options?.scroll);
11330
+ navigateProgrammatic(path, options);
11307
11331
  },
11308
11332
  /**
11309
11333
  * Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
@@ -13,6 +13,11 @@ type ScrollMode = "top" | "preserve";
13
13
  interface NavigateOptions {
14
14
  /** Override the scroll behaviour for this single navigation (else the route/app default). */
15
15
  scroll?: ScrollMode;
16
+ /**
17
+ * Replace the current history entry instead of adding one (`history.replaceState`), so Back
18
+ * skips the page navigated from; e.g. the same page in another language.
19
+ */
20
+ replace?: boolean;
16
21
  }
17
22
  /**
18
23
  * What a island's `render` may return:
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.1"
140
+ "version": "2.4.3"
141
141
  }