@moku-labs/web 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/README.md +1 -0
- package/dist/browser.d.mts +47 -1
- package/dist/browser.mjs +96 -4
- package/dist/index.cjs +97 -3
- package/dist/index.d.cts +47 -1
- package/dist/index.d.mts +47 -1
- package/dist/index.mjs +96 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,7 @@ route("/board/{id}/issue/{issueId}")
|
|
|
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
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 }`.
|
|
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.
|
|
144
145
|
|
|
145
146
|
```ts
|
|
146
147
|
createIsland("issue", {
|
package/dist/browser.d.mts
CHANGED
|
@@ -1395,6 +1395,16 @@ interface SpaKernel {
|
|
|
1395
1395
|
* kernel.processNav("/about");
|
|
1396
1396
|
*/
|
|
1397
1397
|
processNav(path: string, options?: NavigateOptions): void;
|
|
1398
|
+
/**
|
|
1399
|
+
* Cross a boundary the SPA cannot swap (different layout / auth split) with a REAL
|
|
1400
|
+
* full-page load — detaches the interceptor first so the navigation isn't caught.
|
|
1401
|
+
*
|
|
1402
|
+
* @param url - The destination URL.
|
|
1403
|
+
* @returns void
|
|
1404
|
+
* @example
|
|
1405
|
+
* kernel.hardNavigate("/signin/");
|
|
1406
|
+
*/
|
|
1407
|
+
hardNavigate(url: string): void;
|
|
1398
1408
|
/**
|
|
1399
1409
|
* Query the swap region and mount islands for matching elements.
|
|
1400
1410
|
*
|
|
@@ -1456,6 +1466,17 @@ type SpaApi = {
|
|
|
1456
1466
|
* app.spa.navigate("/about");
|
|
1457
1467
|
*/
|
|
1458
1468
|
navigate(path: string, options?: NavigateOptions): void;
|
|
1469
|
+
/**
|
|
1470
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
1471
|
+
* full-page navigation. Detaches the SPA interceptor first, so the navigation is a true
|
|
1472
|
+
* document load (rendering the destination's own layout) rather than an intercepted swap.
|
|
1473
|
+
*
|
|
1474
|
+
* @param url - The destination URL (internal path or absolute).
|
|
1475
|
+
* @returns void
|
|
1476
|
+
* @example
|
|
1477
|
+
* app.spa.hardNavigate("/signin/"); // sign-out / 401: leave the app chrome for the auth split
|
|
1478
|
+
*/
|
|
1479
|
+
hardNavigate(url: string): void;
|
|
1459
1480
|
/**
|
|
1460
1481
|
* Read the current resolved URL.
|
|
1461
1482
|
*
|
|
@@ -1827,6 +1848,31 @@ declare function createIsland<S extends object = object, A = unknown>(name: stri
|
|
|
1827
1848
|
*/
|
|
1828
1849
|
declare const lazyEmbed: IslandDef;
|
|
1829
1850
|
//#endregion
|
|
1851
|
+
//#region src/plugins/spa/lifecycle.d.ts
|
|
1852
|
+
/**
|
|
1853
|
+
* Navigate the booted SPA to an internal path — the SAME swap pipeline as a link click,
|
|
1854
|
+
* `ctx.navigate`, or `app.spa.navigate`, for module-scope callers with no `app`/island `ctx`.
|
|
1855
|
+
* No-op before `app.start()`. Build `path` from the route map's `urls`, never a literal.
|
|
1856
|
+
*
|
|
1857
|
+
* @param path - The internal destination path (e.g. `urls.toUrl("board", { id })`).
|
|
1858
|
+
* @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
|
|
1859
|
+
* @example
|
|
1860
|
+
* import { navigate } from "@moku-labs/web/browser";
|
|
1861
|
+
* navigate(urls.toUrl("issue", { id, issueId }));
|
|
1862
|
+
*/
|
|
1863
|
+
declare function navigate(path: string, options?: NavigateOptions): void;
|
|
1864
|
+
/**
|
|
1865
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
1866
|
+
* full-page load — detaches the SPA interceptor first so the navigation isn't caught and
|
|
1867
|
+
* converted to a swap. For module-scope callers with no `app`/island `ctx`. No-op before boot.
|
|
1868
|
+
*
|
|
1869
|
+
* @param url - The destination URL (internal path or absolute).
|
|
1870
|
+
* @example
|
|
1871
|
+
* import { hardNavigate } from "@moku-labs/web/browser";
|
|
1872
|
+
* hardNavigate(urls.toUrl("signin", {})); // sign-out / 401: leave the app chrome for the auth split
|
|
1873
|
+
*/
|
|
1874
|
+
declare function hardNavigate(url: string): void;
|
|
1875
|
+
//#endregion
|
|
1830
1876
|
//#region src/plugins/spa/index.d.ts
|
|
1831
1877
|
/**
|
|
1832
1878
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -2614,4 +2660,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
2614
2660
|
*/
|
|
2615
2661
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$5, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>]>>;
|
|
2616
2662
|
//#endregion
|
|
2617
|
-
export { type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports as Content, types_d_exports$1 as Data, type Env, types_d_exports$2 as Head, type Log, types_d_exports$3 as Router, types_d_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
2663
|
+
export { type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports as Content, types_d_exports$1 as Data, type Env, types_d_exports$2 as Head, type Log, types_d_exports$3 as Router, types_d_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, navigate, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/browser.mjs
CHANGED
|
@@ -2259,6 +2259,17 @@ function createApi(ctx) {
|
|
|
2259
2259
|
ctx.state.kernel?.processNav(path, options);
|
|
2260
2260
|
},
|
|
2261
2261
|
/**
|
|
2262
|
+
* Cross a boundary the SPA cannot swap (different layout / auth split) with a REAL
|
|
2263
|
+
* full-page load — detaches the interceptor first so the navigation isn't intercepted.
|
|
2264
|
+
*
|
|
2265
|
+
* @param url - The destination URL.
|
|
2266
|
+
* @example
|
|
2267
|
+
* app.spa.hardNavigate("/signin/");
|
|
2268
|
+
*/
|
|
2269
|
+
hardNavigate(url) {
|
|
2270
|
+
ctx.state.kernel?.hardNavigate(url);
|
|
2271
|
+
},
|
|
2272
|
+
/**
|
|
2262
2273
|
* Read the current resolved URL.
|
|
2263
2274
|
*
|
|
2264
2275
|
* @returns The current pathname + search.
|
|
@@ -4156,6 +4167,27 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4156
4167
|
if (signal?.aborted) return;
|
|
4157
4168
|
await performNavigation(pathname, handlers, signal);
|
|
4158
4169
|
};
|
|
4170
|
+
/**
|
|
4171
|
+
* Programmatic navigation entry (`app.spa.navigate` / `ctx.navigate`). A link click or the
|
|
4172
|
+
* Navigation API commits the address bar before the interceptor calls {@link navigate}; a
|
|
4173
|
+
* programmatic call does NOT, so it must commit the URL itself first — otherwise the content
|
|
4174
|
+
* swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
|
|
4175
|
+
* `history.pushState` updates the URL WITHOUT firing a `navigate` event, so it can't
|
|
4176
|
+
* double-trigger the interceptor. The current scroll is saved first so a later back restores it.
|
|
4177
|
+
*
|
|
4178
|
+
* @param path - The destination path (pathname + optional search).
|
|
4179
|
+
* @param scroll - The optional per-call scroll override.
|
|
4180
|
+
* @example
|
|
4181
|
+
* navigateProgrammatic("/board/abc/issue/1", "preserve");
|
|
4182
|
+
*/
|
|
4183
|
+
const navigateProgrammatic = (path, scroll) => {
|
|
4184
|
+
if (typeof document === "undefined") return;
|
|
4185
|
+
if (path !== currentLocationUrl()) {
|
|
4186
|
+
saveScrollPosition(currentLocationUrl());
|
|
4187
|
+
history.pushState({}, "", path);
|
|
4188
|
+
}
|
|
4189
|
+
navigate(path, true, void 0, scroll).catch(() => {});
|
|
4190
|
+
};
|
|
4159
4191
|
return {
|
|
4160
4192
|
/**
|
|
4161
4193
|
* Register config islands and seed currentUrl from the document.
|
|
@@ -4167,8 +4199,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4167
4199
|
for (const island of resolved.islands) registerIsland(state, island);
|
|
4168
4200
|
state.currentUrl = currentLocationUrl();
|
|
4169
4201
|
state.navigate = (path, options) => {
|
|
4170
|
-
|
|
4171
|
-
navigate(path, true, void 0, options?.scroll).catch(() => {});
|
|
4202
|
+
navigateProgrammatic(path, options?.scroll);
|
|
4172
4203
|
};
|
|
4173
4204
|
},
|
|
4174
4205
|
/**
|
|
@@ -4209,8 +4240,24 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4209
4240
|
* kernel.processNav("/about");
|
|
4210
4241
|
*/
|
|
4211
4242
|
processNav(path, options) {
|
|
4243
|
+
navigateProgrammatic(path, options?.scroll);
|
|
4244
|
+
},
|
|
4245
|
+
/**
|
|
4246
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
4247
|
+
* full-page navigation. The click / Navigation-API interceptor catches even
|
|
4248
|
+
* `location.assign`, converting it to a region swap — so this DETACHES that interceptor
|
|
4249
|
+
* first (the same teardown `dispose` runs) and only THEN assigns, guaranteeing a true
|
|
4250
|
+
* document load that renders the destination's own layout. No-op headless.
|
|
4251
|
+
*
|
|
4252
|
+
* @param url - The destination URL (internal path or absolute).
|
|
4253
|
+
* @example
|
|
4254
|
+
* kernel.hardNavigate("/signin/");
|
|
4255
|
+
*/
|
|
4256
|
+
hardNavigate(url) {
|
|
4212
4257
|
if (typeof document === "undefined") return;
|
|
4213
|
-
|
|
4258
|
+
state.destroyRouter?.();
|
|
4259
|
+
state.destroyRouter = null;
|
|
4260
|
+
location.assign(url);
|
|
4214
4261
|
},
|
|
4215
4262
|
/**
|
|
4216
4263
|
* Scan the swap region and mount islands for matching elements.
|
|
@@ -4267,6 +4314,49 @@ function initSpa(ctx) {
|
|
|
4267
4314
|
let teardown;
|
|
4268
4315
|
/** Captured log ref — onStop has no `ctx.log` (spec/08 §4). */
|
|
4269
4316
|
let logRef;
|
|
4317
|
+
/** The booted app's navigators, or undefined before boot / after stop (single-app-per-document). */
|
|
4318
|
+
let boundNavigators;
|
|
4319
|
+
/**
|
|
4320
|
+
* Bind the running app's navigators from its kernel — called by the plugin's `onStart` after
|
|
4321
|
+
* `kernel.boot()`, so the module-level {@link navigate}/{@link hardNavigate} delegate to it.
|
|
4322
|
+
*
|
|
4323
|
+
* @param kernel - The booted spa kernel.
|
|
4324
|
+
* @example
|
|
4325
|
+
* bindKernelNavigators(ctx.state.kernel);
|
|
4326
|
+
*/
|
|
4327
|
+
function bindKernelNavigators(kernel) {
|
|
4328
|
+
boundNavigators = {
|
|
4329
|
+
navigate: (path, options) => kernel.processNav(path, options),
|
|
4330
|
+
hardNavigate: (url) => kernel.hardNavigate(url)
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Navigate the booted SPA to an internal path — the SAME swap pipeline as a link click,
|
|
4335
|
+
* `ctx.navigate`, or `app.spa.navigate`, for module-scope callers with no `app`/island `ctx`.
|
|
4336
|
+
* No-op before `app.start()`. Build `path` from the route map's `urls`, never a literal.
|
|
4337
|
+
*
|
|
4338
|
+
* @param path - The internal destination path (e.g. `urls.toUrl("board", { id })`).
|
|
4339
|
+
* @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
|
|
4340
|
+
* @example
|
|
4341
|
+
* import { navigate } from "@moku-labs/web/browser";
|
|
4342
|
+
* navigate(urls.toUrl("issue", { id, issueId }));
|
|
4343
|
+
*/
|
|
4344
|
+
function navigate(path, options) {
|
|
4345
|
+
boundNavigators?.navigate(path, options);
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
4349
|
+
* full-page load — detaches the SPA interceptor first so the navigation isn't caught and
|
|
4350
|
+
* converted to a swap. For module-scope callers with no `app`/island `ctx`. No-op before boot.
|
|
4351
|
+
*
|
|
4352
|
+
* @param url - The destination URL (internal path or absolute).
|
|
4353
|
+
* @example
|
|
4354
|
+
* import { hardNavigate } from "@moku-labs/web/browser";
|
|
4355
|
+
* hardNavigate(urls.toUrl("signin", {})); // sign-out / 401: leave the app chrome for the auth split
|
|
4356
|
+
*/
|
|
4357
|
+
function hardNavigate(url) {
|
|
4358
|
+
boundNavigators?.hardNavigate(url);
|
|
4359
|
+
}
|
|
4270
4360
|
/**
|
|
4271
4361
|
* Capture the teardown + log handles during `onStart` (no-op without a DOM —
|
|
4272
4362
|
* the SSR/build guard, so onStop has nothing to release). The kernel built in
|
|
@@ -4299,6 +4389,7 @@ function disposeSpa() {
|
|
|
4299
4389
|
} finally {
|
|
4300
4390
|
teardown = void 0;
|
|
4301
4391
|
logRef = void 0;
|
|
4392
|
+
boundNavigators = void 0;
|
|
4302
4393
|
}
|
|
4303
4394
|
}
|
|
4304
4395
|
//#endregion
|
|
@@ -4673,6 +4764,7 @@ const spaPlugin = createPlugin$1("spa", {
|
|
|
4673
4764
|
onStart(ctx) {
|
|
4674
4765
|
captureTeardown(ctx);
|
|
4675
4766
|
ctx.state.kernel?.boot();
|
|
4767
|
+
if (ctx.state.kernel) bindKernelNavigators(ctx.state.kernel);
|
|
4676
4768
|
},
|
|
4677
4769
|
onStop: disposeSpa
|
|
4678
4770
|
});
|
|
@@ -5316,4 +5408,4 @@ const createApp = core.createApp;
|
|
|
5316
5408
|
*/
|
|
5317
5409
|
const createPlugin = core.createPlugin;
|
|
5318
5410
|
//#endregion
|
|
5319
|
-
export { types_exports as Content, types_exports$1 as Data, types_exports$2 as Head, types_exports$3 as Router, types_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
5411
|
+
export { types_exports as Content, types_exports$1 as Data, types_exports$2 as Head, types_exports$3 as Router, types_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, navigate, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.cjs
CHANGED
|
@@ -9217,6 +9217,17 @@ function createApi(ctx) {
|
|
|
9217
9217
|
ctx.state.kernel?.processNav(path, options);
|
|
9218
9218
|
},
|
|
9219
9219
|
/**
|
|
9220
|
+
* Cross a boundary the SPA cannot swap (different layout / auth split) with a REAL
|
|
9221
|
+
* full-page load — detaches the interceptor first so the navigation isn't intercepted.
|
|
9222
|
+
*
|
|
9223
|
+
* @param url - The destination URL.
|
|
9224
|
+
* @example
|
|
9225
|
+
* app.spa.hardNavigate("/signin/");
|
|
9226
|
+
*/
|
|
9227
|
+
hardNavigate(url) {
|
|
9228
|
+
ctx.state.kernel?.hardNavigate(url);
|
|
9229
|
+
},
|
|
9230
|
+
/**
|
|
9220
9231
|
* Read the current resolved URL.
|
|
9221
9232
|
*
|
|
9222
9233
|
* @returns The current pathname + search.
|
|
@@ -10862,6 +10873,27 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10862
10873
|
if (signal?.aborted) return;
|
|
10863
10874
|
await performNavigation(pathname, handlers, signal);
|
|
10864
10875
|
};
|
|
10876
|
+
/**
|
|
10877
|
+
* Programmatic navigation entry (`app.spa.navigate` / `ctx.navigate`). A link click or the
|
|
10878
|
+
* Navigation API commits the address bar before the interceptor calls {@link navigate}; a
|
|
10879
|
+
* programmatic call does NOT, so it must commit the URL itself first — otherwise the content
|
|
10880
|
+
* swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
|
|
10881
|
+
* `history.pushState` updates the URL WITHOUT firing a `navigate` event, so it can't
|
|
10882
|
+
* double-trigger the interceptor. The current scroll is saved first so a later back restores it.
|
|
10883
|
+
*
|
|
10884
|
+
* @param path - The destination path (pathname + optional search).
|
|
10885
|
+
* @param scroll - The optional per-call scroll override.
|
|
10886
|
+
* @example
|
|
10887
|
+
* navigateProgrammatic("/board/abc/issue/1", "preserve");
|
|
10888
|
+
*/
|
|
10889
|
+
const navigateProgrammatic = (path, scroll) => {
|
|
10890
|
+
if (typeof document === "undefined") return;
|
|
10891
|
+
if (path !== currentLocationUrl()) {
|
|
10892
|
+
saveScrollPosition(currentLocationUrl());
|
|
10893
|
+
history.pushState({}, "", path);
|
|
10894
|
+
}
|
|
10895
|
+
navigate(path, true, void 0, scroll).catch(() => {});
|
|
10896
|
+
};
|
|
10865
10897
|
return {
|
|
10866
10898
|
/**
|
|
10867
10899
|
* Register config islands and seed currentUrl from the document.
|
|
@@ -10873,8 +10905,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10873
10905
|
for (const island of resolved.islands) registerIsland(state, island);
|
|
10874
10906
|
state.currentUrl = currentLocationUrl();
|
|
10875
10907
|
state.navigate = (path, options) => {
|
|
10876
|
-
|
|
10877
|
-
navigate(path, true, void 0, options?.scroll).catch(() => {});
|
|
10908
|
+
navigateProgrammatic(path, options?.scroll);
|
|
10878
10909
|
};
|
|
10879
10910
|
},
|
|
10880
10911
|
/**
|
|
@@ -10915,8 +10946,24 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10915
10946
|
* kernel.processNav("/about");
|
|
10916
10947
|
*/
|
|
10917
10948
|
processNav(path, options) {
|
|
10949
|
+
navigateProgrammatic(path, options?.scroll);
|
|
10950
|
+
},
|
|
10951
|
+
/**
|
|
10952
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
10953
|
+
* full-page navigation. The click / Navigation-API interceptor catches even
|
|
10954
|
+
* `location.assign`, converting it to a region swap — so this DETACHES that interceptor
|
|
10955
|
+
* first (the same teardown `dispose` runs) and only THEN assigns, guaranteeing a true
|
|
10956
|
+
* document load that renders the destination's own layout. No-op headless.
|
|
10957
|
+
*
|
|
10958
|
+
* @param url - The destination URL (internal path or absolute).
|
|
10959
|
+
* @example
|
|
10960
|
+
* kernel.hardNavigate("/signin/");
|
|
10961
|
+
*/
|
|
10962
|
+
hardNavigate(url) {
|
|
10918
10963
|
if (typeof document === "undefined") return;
|
|
10919
|
-
|
|
10964
|
+
state.destroyRouter?.();
|
|
10965
|
+
state.destroyRouter = null;
|
|
10966
|
+
location.assign(url);
|
|
10920
10967
|
},
|
|
10921
10968
|
/**
|
|
10922
10969
|
* Scan the swap region and mount islands for matching elements.
|
|
@@ -10973,6 +11020,49 @@ function initSpa(ctx) {
|
|
|
10973
11020
|
let teardown;
|
|
10974
11021
|
/** Captured log ref — onStop has no `ctx.log` (spec/08 §4). */
|
|
10975
11022
|
let logRef;
|
|
11023
|
+
/** The booted app's navigators, or undefined before boot / after stop (single-app-per-document). */
|
|
11024
|
+
let boundNavigators;
|
|
11025
|
+
/**
|
|
11026
|
+
* Bind the running app's navigators from its kernel — called by the plugin's `onStart` after
|
|
11027
|
+
* `kernel.boot()`, so the module-level {@link navigate}/{@link hardNavigate} delegate to it.
|
|
11028
|
+
*
|
|
11029
|
+
* @param kernel - The booted spa kernel.
|
|
11030
|
+
* @example
|
|
11031
|
+
* bindKernelNavigators(ctx.state.kernel);
|
|
11032
|
+
*/
|
|
11033
|
+
function bindKernelNavigators(kernel) {
|
|
11034
|
+
boundNavigators = {
|
|
11035
|
+
navigate: (path, options) => kernel.processNav(path, options),
|
|
11036
|
+
hardNavigate: (url) => kernel.hardNavigate(url)
|
|
11037
|
+
};
|
|
11038
|
+
}
|
|
11039
|
+
/**
|
|
11040
|
+
* Navigate the booted SPA to an internal path — the SAME swap pipeline as a link click,
|
|
11041
|
+
* `ctx.navigate`, or `app.spa.navigate`, for module-scope callers with no `app`/island `ctx`.
|
|
11042
|
+
* No-op before `app.start()`. Build `path` from the route map's `urls`, never a literal.
|
|
11043
|
+
*
|
|
11044
|
+
* @param path - The internal destination path (e.g. `urls.toUrl("board", { id })`).
|
|
11045
|
+
* @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
|
|
11046
|
+
* @example
|
|
11047
|
+
* import { navigate } from "@moku-labs/web/browser";
|
|
11048
|
+
* navigate(urls.toUrl("issue", { id, issueId }));
|
|
11049
|
+
*/
|
|
11050
|
+
function navigate(path, options) {
|
|
11051
|
+
boundNavigators?.navigate(path, options);
|
|
11052
|
+
}
|
|
11053
|
+
/**
|
|
11054
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
11055
|
+
* full-page load — detaches the SPA interceptor first so the navigation isn't caught and
|
|
11056
|
+
* converted to a swap. For module-scope callers with no `app`/island `ctx`. No-op before boot.
|
|
11057
|
+
*
|
|
11058
|
+
* @param url - The destination URL (internal path or absolute).
|
|
11059
|
+
* @example
|
|
11060
|
+
* import { hardNavigate } from "@moku-labs/web/browser";
|
|
11061
|
+
* hardNavigate(urls.toUrl("signin", {})); // sign-out / 401: leave the app chrome for the auth split
|
|
11062
|
+
*/
|
|
11063
|
+
function hardNavigate(url) {
|
|
11064
|
+
boundNavigators?.hardNavigate(url);
|
|
11065
|
+
}
|
|
10976
11066
|
/**
|
|
10977
11067
|
* Capture the teardown + log handles during `onStart` (no-op without a DOM —
|
|
10978
11068
|
* the SSR/build guard, so onStop has nothing to release). The kernel built in
|
|
@@ -11005,6 +11095,7 @@ function disposeSpa() {
|
|
|
11005
11095
|
} finally {
|
|
11006
11096
|
teardown = void 0;
|
|
11007
11097
|
logRef = void 0;
|
|
11098
|
+
boundNavigators = void 0;
|
|
11008
11099
|
}
|
|
11009
11100
|
}
|
|
11010
11101
|
//#endregion
|
|
@@ -11379,6 +11470,7 @@ const spaPlugin = createPlugin$1("spa", {
|
|
|
11379
11470
|
onStart(ctx) {
|
|
11380
11471
|
captureTeardown(ctx);
|
|
11381
11472
|
ctx.state.kernel?.boot();
|
|
11473
|
+
if (ctx.state.kernel) bindKernelNavigators(ctx.state.kernel);
|
|
11382
11474
|
},
|
|
11383
11475
|
onStop: disposeSpa
|
|
11384
11476
|
});
|
|
@@ -13691,6 +13783,7 @@ Object.defineProperty(exports, "envPlugin", {
|
|
|
13691
13783
|
});
|
|
13692
13784
|
exports.feedLink = feedLink;
|
|
13693
13785
|
exports.fileSystemContent = fileSystemContent;
|
|
13786
|
+
exports.hardNavigate = hardNavigate;
|
|
13694
13787
|
exports.headPlugin = headPlugin;
|
|
13695
13788
|
exports.hreflang = hreflang;
|
|
13696
13789
|
exports.i18nPlugin = i18nPlugin;
|
|
@@ -13703,6 +13796,7 @@ Object.defineProperty(exports, "logPlugin", {
|
|
|
13703
13796
|
}
|
|
13704
13797
|
});
|
|
13705
13798
|
exports.meta = meta;
|
|
13799
|
+
exports.navigate = navigate;
|
|
13706
13800
|
exports.og = og;
|
|
13707
13801
|
Object.defineProperty(exports, "processEnv", {
|
|
13708
13802
|
enumerable: true,
|
package/dist/index.d.cts
CHANGED
|
@@ -1396,6 +1396,16 @@ interface SpaKernel {
|
|
|
1396
1396
|
* kernel.processNav("/about");
|
|
1397
1397
|
*/
|
|
1398
1398
|
processNav(path: string, options?: NavigateOptions): void;
|
|
1399
|
+
/**
|
|
1400
|
+
* Cross a boundary the SPA cannot swap (different layout / auth split) with a REAL
|
|
1401
|
+
* full-page load — detaches the interceptor first so the navigation isn't caught.
|
|
1402
|
+
*
|
|
1403
|
+
* @param url - The destination URL.
|
|
1404
|
+
* @returns void
|
|
1405
|
+
* @example
|
|
1406
|
+
* kernel.hardNavigate("/signin/");
|
|
1407
|
+
*/
|
|
1408
|
+
hardNavigate(url: string): void;
|
|
1399
1409
|
/**
|
|
1400
1410
|
* Query the swap region and mount islands for matching elements.
|
|
1401
1411
|
*
|
|
@@ -1457,6 +1467,17 @@ type SpaApi = {
|
|
|
1457
1467
|
* app.spa.navigate("/about");
|
|
1458
1468
|
*/
|
|
1459
1469
|
navigate(path: string, options?: NavigateOptions): void;
|
|
1470
|
+
/**
|
|
1471
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
1472
|
+
* full-page navigation. Detaches the SPA interceptor first, so the navigation is a true
|
|
1473
|
+
* document load (rendering the destination's own layout) rather than an intercepted swap.
|
|
1474
|
+
*
|
|
1475
|
+
* @param url - The destination URL (internal path or absolute).
|
|
1476
|
+
* @returns void
|
|
1477
|
+
* @example
|
|
1478
|
+
* app.spa.hardNavigate("/signin/"); // sign-out / 401: leave the app chrome for the auth split
|
|
1479
|
+
*/
|
|
1480
|
+
hardNavigate(url: string): void;
|
|
1460
1481
|
/**
|
|
1461
1482
|
* Read the current resolved URL.
|
|
1462
1483
|
*
|
|
@@ -3747,6 +3768,31 @@ declare function createIsland<S extends object = object, A = unknown>(name: stri
|
|
|
3747
3768
|
*/
|
|
3748
3769
|
declare const lazyEmbed: IslandDef;
|
|
3749
3770
|
//#endregion
|
|
3771
|
+
//#region src/plugins/spa/lifecycle.d.ts
|
|
3772
|
+
/**
|
|
3773
|
+
* Navigate the booted SPA to an internal path — the SAME swap pipeline as a link click,
|
|
3774
|
+
* `ctx.navigate`, or `app.spa.navigate`, for module-scope callers with no `app`/island `ctx`.
|
|
3775
|
+
* No-op before `app.start()`. Build `path` from the route map's `urls`, never a literal.
|
|
3776
|
+
*
|
|
3777
|
+
* @param path - The internal destination path (e.g. `urls.toUrl("board", { id })`).
|
|
3778
|
+
* @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
|
|
3779
|
+
* @example
|
|
3780
|
+
* import { navigate } from "@moku-labs/web/browser";
|
|
3781
|
+
* navigate(urls.toUrl("issue", { id, issueId }));
|
|
3782
|
+
*/
|
|
3783
|
+
declare function navigate(path: string, options?: NavigateOptions): void;
|
|
3784
|
+
/**
|
|
3785
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
3786
|
+
* full-page load — detaches the SPA interceptor first so the navigation isn't caught and
|
|
3787
|
+
* converted to a swap. For module-scope callers with no `app`/island `ctx`. No-op before boot.
|
|
3788
|
+
*
|
|
3789
|
+
* @param url - The destination URL (internal path or absolute).
|
|
3790
|
+
* @example
|
|
3791
|
+
* import { hardNavigate } from "@moku-labs/web/browser";
|
|
3792
|
+
* hardNavigate(urls.toUrl("signin", {})); // sign-out / 401: leave the app chrome for the auth split
|
|
3793
|
+
*/
|
|
3794
|
+
declare function hardNavigate(url: string): void;
|
|
3795
|
+
//#endregion
|
|
3750
3796
|
//#region src/plugins/spa/index.d.ts
|
|
3751
3797
|
/**
|
|
3752
3798
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -3946,4 +3992,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3946
3992
|
*/
|
|
3947
3993
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$8, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>]>>;
|
|
3948
3994
|
//#endregion
|
|
3949
|
-
export { types_d_exports as Build, type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, type Env, type GalleryComponent, type GalleryOptions, type GalleryProps, type GallerySlide, GalleryTrack, types_d_exports$5 as Head, type Log, types_d_exports$6 as Router, types_d_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
3995
|
+
export { types_d_exports as Build, type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, type Env, type GalleryComponent, type GalleryOptions, type GalleryProps, type GallerySlide, GalleryTrack, types_d_exports$5 as Head, type Log, types_d_exports$6 as Router, types_d_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, navigate, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.d.mts
CHANGED
|
@@ -1394,6 +1394,16 @@ interface SpaKernel {
|
|
|
1394
1394
|
* kernel.processNav("/about");
|
|
1395
1395
|
*/
|
|
1396
1396
|
processNav(path: string, options?: NavigateOptions): void;
|
|
1397
|
+
/**
|
|
1398
|
+
* Cross a boundary the SPA cannot swap (different layout / auth split) with a REAL
|
|
1399
|
+
* full-page load — detaches the interceptor first so the navigation isn't caught.
|
|
1400
|
+
*
|
|
1401
|
+
* @param url - The destination URL.
|
|
1402
|
+
* @returns void
|
|
1403
|
+
* @example
|
|
1404
|
+
* kernel.hardNavigate("/signin/");
|
|
1405
|
+
*/
|
|
1406
|
+
hardNavigate(url: string): void;
|
|
1397
1407
|
/**
|
|
1398
1408
|
* Query the swap region and mount islands for matching elements.
|
|
1399
1409
|
*
|
|
@@ -1455,6 +1465,17 @@ type SpaApi = {
|
|
|
1455
1465
|
* app.spa.navigate("/about");
|
|
1456
1466
|
*/
|
|
1457
1467
|
navigate(path: string, options?: NavigateOptions): void;
|
|
1468
|
+
/**
|
|
1469
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
1470
|
+
* full-page navigation. Detaches the SPA interceptor first, so the navigation is a true
|
|
1471
|
+
* document load (rendering the destination's own layout) rather than an intercepted swap.
|
|
1472
|
+
*
|
|
1473
|
+
* @param url - The destination URL (internal path or absolute).
|
|
1474
|
+
* @returns void
|
|
1475
|
+
* @example
|
|
1476
|
+
* app.spa.hardNavigate("/signin/"); // sign-out / 401: leave the app chrome for the auth split
|
|
1477
|
+
*/
|
|
1478
|
+
hardNavigate(url: string): void;
|
|
1458
1479
|
/**
|
|
1459
1480
|
* Read the current resolved URL.
|
|
1460
1481
|
*
|
|
@@ -3745,6 +3766,31 @@ declare function createIsland<S extends object = object, A = unknown>(name: stri
|
|
|
3745
3766
|
*/
|
|
3746
3767
|
declare const lazyEmbed: IslandDef;
|
|
3747
3768
|
//#endregion
|
|
3769
|
+
//#region src/plugins/spa/lifecycle.d.ts
|
|
3770
|
+
/**
|
|
3771
|
+
* Navigate the booted SPA to an internal path — the SAME swap pipeline as a link click,
|
|
3772
|
+
* `ctx.navigate`, or `app.spa.navigate`, for module-scope callers with no `app`/island `ctx`.
|
|
3773
|
+
* No-op before `app.start()`. Build `path` from the route map's `urls`, never a literal.
|
|
3774
|
+
*
|
|
3775
|
+
* @param path - The internal destination path (e.g. `urls.toUrl("board", { id })`).
|
|
3776
|
+
* @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
|
|
3777
|
+
* @example
|
|
3778
|
+
* import { navigate } from "@moku-labs/web/browser";
|
|
3779
|
+
* navigate(urls.toUrl("issue", { id, issueId }));
|
|
3780
|
+
*/
|
|
3781
|
+
declare function navigate(path: string, options?: NavigateOptions): void;
|
|
3782
|
+
/**
|
|
3783
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
3784
|
+
* full-page load — detaches the SPA interceptor first so the navigation isn't caught and
|
|
3785
|
+
* converted to a swap. For module-scope callers with no `app`/island `ctx`. No-op before boot.
|
|
3786
|
+
*
|
|
3787
|
+
* @param url - The destination URL (internal path or absolute).
|
|
3788
|
+
* @example
|
|
3789
|
+
* import { hardNavigate } from "@moku-labs/web/browser";
|
|
3790
|
+
* hardNavigate(urls.toUrl("signin", {})); // sign-out / 401: leave the app chrome for the auth split
|
|
3791
|
+
*/
|
|
3792
|
+
declare function hardNavigate(url: string): void;
|
|
3793
|
+
//#endregion
|
|
3748
3794
|
//#region src/plugins/spa/index.d.ts
|
|
3749
3795
|
/**
|
|
3750
3796
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -3944,4 +3990,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3944
3990
|
*/
|
|
3945
3991
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$8, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>]>>;
|
|
3946
3992
|
//#endregion
|
|
3947
|
-
export { types_d_exports as Build, type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, type Env, type GalleryComponent, type GalleryOptions, type GalleryProps, type GallerySlide, GalleryTrack, types_d_exports$5 as Head, type Log, types_d_exports$6 as Router, types_d_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
3993
|
+
export { types_d_exports as Build, type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, type Env, type GalleryComponent, type GalleryOptions, type GalleryProps, type GallerySlide, GalleryTrack, types_d_exports$5 as Head, type Log, types_d_exports$6 as Router, types_d_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, navigate, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.mjs
CHANGED
|
@@ -9204,6 +9204,17 @@ function createApi(ctx) {
|
|
|
9204
9204
|
ctx.state.kernel?.processNav(path, options);
|
|
9205
9205
|
},
|
|
9206
9206
|
/**
|
|
9207
|
+
* Cross a boundary the SPA cannot swap (different layout / auth split) with a REAL
|
|
9208
|
+
* full-page load — detaches the interceptor first so the navigation isn't intercepted.
|
|
9209
|
+
*
|
|
9210
|
+
* @param url - The destination URL.
|
|
9211
|
+
* @example
|
|
9212
|
+
* app.spa.hardNavigate("/signin/");
|
|
9213
|
+
*/
|
|
9214
|
+
hardNavigate(url) {
|
|
9215
|
+
ctx.state.kernel?.hardNavigate(url);
|
|
9216
|
+
},
|
|
9217
|
+
/**
|
|
9207
9218
|
* Read the current resolved URL.
|
|
9208
9219
|
*
|
|
9209
9220
|
* @returns The current pathname + search.
|
|
@@ -10849,6 +10860,27 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10849
10860
|
if (signal?.aborted) return;
|
|
10850
10861
|
await performNavigation(pathname, handlers, signal);
|
|
10851
10862
|
};
|
|
10863
|
+
/**
|
|
10864
|
+
* Programmatic navigation entry (`app.spa.navigate` / `ctx.navigate`). A link click or the
|
|
10865
|
+
* Navigation API commits the address bar before the interceptor calls {@link navigate}; a
|
|
10866
|
+
* programmatic call does NOT, so it must commit the URL itself first — otherwise the content
|
|
10867
|
+
* swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
|
|
10868
|
+
* `history.pushState` updates the URL WITHOUT firing a `navigate` event, so it can't
|
|
10869
|
+
* double-trigger the interceptor. The current scroll is saved first so a later back restores it.
|
|
10870
|
+
*
|
|
10871
|
+
* @param path - The destination path (pathname + optional search).
|
|
10872
|
+
* @param scroll - The optional per-call scroll override.
|
|
10873
|
+
* @example
|
|
10874
|
+
* navigateProgrammatic("/board/abc/issue/1", "preserve");
|
|
10875
|
+
*/
|
|
10876
|
+
const navigateProgrammatic = (path, scroll) => {
|
|
10877
|
+
if (typeof document === "undefined") return;
|
|
10878
|
+
if (path !== currentLocationUrl()) {
|
|
10879
|
+
saveScrollPosition(currentLocationUrl());
|
|
10880
|
+
history.pushState({}, "", path);
|
|
10881
|
+
}
|
|
10882
|
+
navigate(path, true, void 0, scroll).catch(() => {});
|
|
10883
|
+
};
|
|
10852
10884
|
return {
|
|
10853
10885
|
/**
|
|
10854
10886
|
* Register config islands and seed currentUrl from the document.
|
|
@@ -10860,8 +10892,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10860
10892
|
for (const island of resolved.islands) registerIsland(state, island);
|
|
10861
10893
|
state.currentUrl = currentLocationUrl();
|
|
10862
10894
|
state.navigate = (path, options) => {
|
|
10863
|
-
|
|
10864
|
-
navigate(path, true, void 0, options?.scroll).catch(() => {});
|
|
10895
|
+
navigateProgrammatic(path, options?.scroll);
|
|
10865
10896
|
};
|
|
10866
10897
|
},
|
|
10867
10898
|
/**
|
|
@@ -10902,8 +10933,24 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10902
10933
|
* kernel.processNav("/about");
|
|
10903
10934
|
*/
|
|
10904
10935
|
processNav(path, options) {
|
|
10936
|
+
navigateProgrammatic(path, options?.scroll);
|
|
10937
|
+
},
|
|
10938
|
+
/**
|
|
10939
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
10940
|
+
* full-page navigation. The click / Navigation-API interceptor catches even
|
|
10941
|
+
* `location.assign`, converting it to a region swap — so this DETACHES that interceptor
|
|
10942
|
+
* first (the same teardown `dispose` runs) and only THEN assigns, guaranteeing a true
|
|
10943
|
+
* document load that renders the destination's own layout. No-op headless.
|
|
10944
|
+
*
|
|
10945
|
+
* @param url - The destination URL (internal path or absolute).
|
|
10946
|
+
* @example
|
|
10947
|
+
* kernel.hardNavigate("/signin/");
|
|
10948
|
+
*/
|
|
10949
|
+
hardNavigate(url) {
|
|
10905
10950
|
if (typeof document === "undefined") return;
|
|
10906
|
-
|
|
10951
|
+
state.destroyRouter?.();
|
|
10952
|
+
state.destroyRouter = null;
|
|
10953
|
+
location.assign(url);
|
|
10907
10954
|
},
|
|
10908
10955
|
/**
|
|
10909
10956
|
* Scan the swap region and mount islands for matching elements.
|
|
@@ -10960,6 +11007,49 @@ function initSpa(ctx) {
|
|
|
10960
11007
|
let teardown;
|
|
10961
11008
|
/** Captured log ref — onStop has no `ctx.log` (spec/08 §4). */
|
|
10962
11009
|
let logRef;
|
|
11010
|
+
/** The booted app's navigators, or undefined before boot / after stop (single-app-per-document). */
|
|
11011
|
+
let boundNavigators;
|
|
11012
|
+
/**
|
|
11013
|
+
* Bind the running app's navigators from its kernel — called by the plugin's `onStart` after
|
|
11014
|
+
* `kernel.boot()`, so the module-level {@link navigate}/{@link hardNavigate} delegate to it.
|
|
11015
|
+
*
|
|
11016
|
+
* @param kernel - The booted spa kernel.
|
|
11017
|
+
* @example
|
|
11018
|
+
* bindKernelNavigators(ctx.state.kernel);
|
|
11019
|
+
*/
|
|
11020
|
+
function bindKernelNavigators(kernel) {
|
|
11021
|
+
boundNavigators = {
|
|
11022
|
+
navigate: (path, options) => kernel.processNav(path, options),
|
|
11023
|
+
hardNavigate: (url) => kernel.hardNavigate(url)
|
|
11024
|
+
};
|
|
11025
|
+
}
|
|
11026
|
+
/**
|
|
11027
|
+
* Navigate the booted SPA to an internal path — the SAME swap pipeline as a link click,
|
|
11028
|
+
* `ctx.navigate`, or `app.spa.navigate`, for module-scope callers with no `app`/island `ctx`.
|
|
11029
|
+
* No-op before `app.start()`. Build `path` from the route map's `urls`, never a literal.
|
|
11030
|
+
*
|
|
11031
|
+
* @param path - The internal destination path (e.g. `urls.toUrl("board", { id })`).
|
|
11032
|
+
* @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
|
|
11033
|
+
* @example
|
|
11034
|
+
* import { navigate } from "@moku-labs/web/browser";
|
|
11035
|
+
* navigate(urls.toUrl("issue", { id, issueId }));
|
|
11036
|
+
*/
|
|
11037
|
+
function navigate(path, options) {
|
|
11038
|
+
boundNavigators?.navigate(path, options);
|
|
11039
|
+
}
|
|
11040
|
+
/**
|
|
11041
|
+
* Cross a boundary the SPA cannot swap (a different layout, the auth split) with a REAL
|
|
11042
|
+
* full-page load — detaches the SPA interceptor first so the navigation isn't caught and
|
|
11043
|
+
* converted to a swap. For module-scope callers with no `app`/island `ctx`. No-op before boot.
|
|
11044
|
+
*
|
|
11045
|
+
* @param url - The destination URL (internal path or absolute).
|
|
11046
|
+
* @example
|
|
11047
|
+
* import { hardNavigate } from "@moku-labs/web/browser";
|
|
11048
|
+
* hardNavigate(urls.toUrl("signin", {})); // sign-out / 401: leave the app chrome for the auth split
|
|
11049
|
+
*/
|
|
11050
|
+
function hardNavigate(url) {
|
|
11051
|
+
boundNavigators?.hardNavigate(url);
|
|
11052
|
+
}
|
|
10963
11053
|
/**
|
|
10964
11054
|
* Capture the teardown + log handles during `onStart` (no-op without a DOM —
|
|
10965
11055
|
* the SSR/build guard, so onStop has nothing to release). The kernel built in
|
|
@@ -10992,6 +11082,7 @@ function disposeSpa() {
|
|
|
10992
11082
|
} finally {
|
|
10993
11083
|
teardown = void 0;
|
|
10994
11084
|
logRef = void 0;
|
|
11085
|
+
boundNavigators = void 0;
|
|
10995
11086
|
}
|
|
10996
11087
|
}
|
|
10997
11088
|
//#endregion
|
|
@@ -11366,6 +11457,7 @@ const spaPlugin = createPlugin$1("spa", {
|
|
|
11366
11457
|
onStart(ctx) {
|
|
11367
11458
|
captureTeardown(ctx);
|
|
11368
11459
|
ctx.state.kernel?.boot();
|
|
11460
|
+
if (ctx.state.kernel) bindKernelNavigators(ctx.state.kernel);
|
|
11369
11461
|
},
|
|
11370
11462
|
onStop: disposeSpa
|
|
11371
11463
|
});
|
|
@@ -13589,4 +13681,4 @@ const createApp = core.createApp;
|
|
|
13589
13681
|
*/
|
|
13590
13682
|
const createPlugin = core.createPlugin;
|
|
13591
13683
|
//#endregion
|
|
13592
|
-
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, GalleryTrack, types_exports$5 as Head, types_exports$6 as Router, types_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
13684
|
+
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, GalleryTrack, types_exports$5 as Head, types_exports$6 as Router, types_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, navigate, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/package.json
CHANGED