@webjsdev/core 0.7.22 → 0.7.23
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 +0 -1
- package/dist/webjs-core-browser.js +3 -3
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +2 -2
- package/index-browser.js +9 -0
- package/package.json +1 -1
- package/src/component.d.ts +14 -13
- package/src/router-client.js +95 -27
- package/src/webjs-config.d.ts +11 -1
package/index-browser.js
CHANGED
|
@@ -38,6 +38,15 @@ export {
|
|
|
38
38
|
stringify, parse,
|
|
39
39
|
serialize, deserialize,
|
|
40
40
|
} from './src/serialize.js';
|
|
41
|
+
// The client router auto-enables on bundle load: `router-client.js` calls
|
|
42
|
+
// `enableClientRouter()` at module end (idempotent). This is INTENTIONAL, so
|
|
43
|
+
// any page that loads `@webjsdev/core` (i.e. ships any component) gets client
|
|
44
|
+
// navigation with no per-app opt-in. A layout no longer needs to
|
|
45
|
+
// `import '@webjsdev/core/client-router'`. The bare side-effect import below
|
|
46
|
+
// pins that contract even if the named re-export is later refactored away;
|
|
47
|
+
// the re-export still also evaluates the module, so the two are belt-and-
|
|
48
|
+
// suspenders, not a double-enable (ESM evaluates once, enable is idempotent).
|
|
49
|
+
import './src/router-client.js';
|
|
41
50
|
export { enableClientRouter, disableClientRouter, navigate, revalidate } from './src/router-client.js';
|
|
42
51
|
export { WebjsFrame } from './src/webjs-frame.js';
|
|
43
52
|
export { WebjsStream, renderStream } from './src/webjs-stream.js';
|
package/package.json
CHANGED
package/src/component.d.ts
CHANGED
|
@@ -146,10 +146,14 @@ export type InferProps<S> = {
|
|
|
146
146
|
[K in keyof S]: S[K] extends PropertyDeclaration<infer T> ? T : Infer<S[K]>
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
/**
|
|
150
|
+
* The static surface every `WebComponent` constructor carries (the bare base,
|
|
151
|
+
* a factory-produced base, and a registered subclass alike). Declared ONCE
|
|
152
|
+
* here and referenced from both `WebComponentConstructor` and the factory-call
|
|
153
|
+
* return type below, so adding or changing a static can never let the two
|
|
154
|
+
* positions drift. Mirrors the `static` members on `WebComponentBase`.
|
|
155
|
+
*/
|
|
156
|
+
interface WebComponentStatics {
|
|
153
157
|
shadow: boolean;
|
|
154
158
|
hydrate: 'visible' | undefined;
|
|
155
159
|
properties: Record<string, PropertyDeclaration>;
|
|
@@ -157,18 +161,15 @@ export interface WebComponentConstructor {
|
|
|
157
161
|
lazy?: boolean;
|
|
158
162
|
register(tag: string): void;
|
|
159
163
|
readonly observedAttributes: string[];
|
|
164
|
+
}
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
export interface WebComponentConstructor extends WebComponentStatics {
|
|
167
|
+
new (): WebComponentBase;
|
|
168
|
+
prototype: WebComponentBase;
|
|
169
|
+
|
|
170
|
+
<S extends Record<string, any>>(shape: S): WebComponentStatics & {
|
|
162
171
|
new (): WebComponentBase & InferProps<S>;
|
|
163
172
|
prototype: WebComponentBase & InferProps<S>;
|
|
164
|
-
|
|
165
|
-
shadow: boolean;
|
|
166
|
-
hydrate: 'visible' | undefined;
|
|
167
|
-
properties: Record<string, PropertyDeclaration>;
|
|
168
|
-
styles: CSSResult | CSSResult[] | null;
|
|
169
|
-
lazy?: boolean;
|
|
170
|
-
register(tag: string): void;
|
|
171
|
-
readonly observedAttributes: string[];
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
|
package/src/router-client.js
CHANGED
|
@@ -27,11 +27,11 @@ const STREAM_MIME = 'text/vnd.webjs-stream.html';
|
|
|
27
27
|
* children of that boundary. Outer layout DOM (header, sidenav, footer)
|
|
28
28
|
* stays mounted: no re-render, no flicker, scroll positions preserved.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* Enablement is automatic: this module calls `enableClientRouter()` at its
|
|
31
|
+
* end (idempotent), and the `@webjsdev/core` browser entry loads it, so any
|
|
32
|
+
* page that ships a component gets the router with no import to add. Call
|
|
33
|
+
* `disableClientRouter()` to opt out, or `enableClientRouter()` for
|
|
34
|
+
* programmatic control.
|
|
35
35
|
*
|
|
36
36
|
* Mechanism: auto-derived from folder structure:
|
|
37
37
|
* 1. SSR injects `<!--wj:children:<segment-path>-->...<!--/wj:children-->`
|
|
@@ -606,6 +606,64 @@ function warnOnce(key, message) {
|
|
|
606
606
|
if (typeof console !== 'undefined' && console.warn) console.warn(message);
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Dev-only, fire-once hint: the router forces an INSTANT scroll-to-top on a
|
|
611
|
+
* forward navigation (matching a native page load), so an app-level
|
|
612
|
+
* `scroll-behavior: smooth` on <html> does not affect route transitions (it
|
|
613
|
+
* still applies to in-page #anchor links via `scrollIntoView`). A developer
|
|
614
|
+
* who set smooth expecting smooth nav scrolling would otherwise be puzzled.
|
|
615
|
+
* Also flags the iOS sticky-`backdrop-filter` flash this combination can
|
|
616
|
+
* cause (#610). Never warns in production, never throws.
|
|
617
|
+
*
|
|
618
|
+
* The `smoothScrollChecked` flag gates the `getComputedStyle` read (a forced
|
|
619
|
+
* style flush) to AT MOST ONCE per page, so a dev session does not pay a
|
|
620
|
+
* per-navigation reflow after the first forward nav.
|
|
621
|
+
*/
|
|
622
|
+
let smoothScrollChecked = false;
|
|
623
|
+
function warnIfSmoothScrollOnHtml() {
|
|
624
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') return;
|
|
625
|
+
if (smoothScrollChecked) return;
|
|
626
|
+
if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') return;
|
|
627
|
+
const root = document.documentElement;
|
|
628
|
+
if (!root) return;
|
|
629
|
+
smoothScrollChecked = true;
|
|
630
|
+
let behavior;
|
|
631
|
+
try { behavior = getComputedStyle(root).scrollBehavior; } catch { return; }
|
|
632
|
+
if (behavior !== 'smooth') return;
|
|
633
|
+
warnOnce(
|
|
634
|
+
'scroll-behavior-smooth-html',
|
|
635
|
+
'[webjs] Detected `scroll-behavior: smooth` on <html>. The client router scrolls ' +
|
|
636
|
+
'to the top instantly on navigation (like a native page load), so route transitions ' +
|
|
637
|
+
'are not affected by it. It still applies to in-page #anchor links. Pairing it with a ' +
|
|
638
|
+
'sticky `backdrop-filter` header can also flash on iOS during navigation.'
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Nav-in-flight signalling. The router can expose `data-navigating` on <html>
|
|
644
|
+
* so an app may style a loading indicator with `html[data-navigating] { … }`.
|
|
645
|
+
*
|
|
646
|
+
* This is OPT-IN, set only when the app marks `<html data-webjs-nav-progress>`.
|
|
647
|
+
* The reason it is not unconditional: toggling ANY attribute on the root
|
|
648
|
+
* re-runs global style resolution, and on WebKit (so every iOS browser, since
|
|
649
|
+
* they all use it) that re-resolves `oklch()` / `color-mix(in oklch, …)` token
|
|
650
|
+
* values to an equivalent oklab representation and repaints them for one frame.
|
|
651
|
+
* On a token-driven theme that is a visible background flash on navigation
|
|
652
|
+
* (#610). The flash only shows on a nav slow enough to reach the deferred set
|
|
653
|
+
* below, which a desktop nav rarely is but a mobile forward fetch routinely is,
|
|
654
|
+
* so the symptom is iOS-and-forward-only. With no opt-in the attribute is never
|
|
655
|
+
* written, so the re-resolution never happens and the flash cannot occur.
|
|
656
|
+
*/
|
|
657
|
+
function setNavigating(on) {
|
|
658
|
+
if (typeof document === 'undefined') return;
|
|
659
|
+
const root = document.documentElement;
|
|
660
|
+
if (!root || !root.hasAttribute('data-webjs-nav-progress')) return;
|
|
661
|
+
try {
|
|
662
|
+
if (on) root.setAttribute('data-navigating', '');
|
|
663
|
+
else root.removeAttribute('data-navigating');
|
|
664
|
+
} catch { /* non-DOM environment */ }
|
|
665
|
+
}
|
|
666
|
+
|
|
609
667
|
/* ====================================================================
|
|
610
668
|
* Marker discovery (the heart of the partial-swap mechanism)
|
|
611
669
|
* ==================================================================== */
|
|
@@ -772,17 +830,11 @@ async function performNavigation(href, isPopState, frameId) {
|
|
|
772
830
|
// about to read in the popstate-restore branch below.
|
|
773
831
|
if (currentPageUrl) snapshotCurrent(currentPageUrl);
|
|
774
832
|
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
// `color-mix(in oklch, …)` to re-resolve. For values that use
|
|
779
|
-
// wide-gamut color spaces the re-resolution can switch between
|
|
780
|
-
// equivalent representations (oklch ↔ oklab) and fire any
|
|
781
|
-
// `transition` rules listening on that property, producing a
|
|
782
|
-
// visible flash on every nav. Defer the attribute set so quick
|
|
783
|
-
// navs (sub-150ms) never set it at all.
|
|
833
|
+
// Expose the opt-in `data-navigating` loading-indicator hook (see
|
|
834
|
+
// setNavigating), but only if the nav takes long enough to be worth showing
|
|
835
|
+
// one. Deferred so quick navs (sub-150ms) never set it at all.
|
|
784
836
|
let navigatingFlagTimer = setTimeout(() => {
|
|
785
|
-
|
|
837
|
+
setNavigating(true);
|
|
786
838
|
navigatingFlagTimer = null;
|
|
787
839
|
}, 150);
|
|
788
840
|
|
|
@@ -801,9 +853,11 @@ async function performNavigation(href, isPopState, frameId) {
|
|
|
801
853
|
const cachedDoc = parseHTML(cached.html);
|
|
802
854
|
if (cachedDoc) {
|
|
803
855
|
applySwap(cachedDoc, frameId, /* revalidating */ true, /* href */ null);
|
|
804
|
-
// Restore window scroll to where the user left it.
|
|
856
|
+
// Restore window scroll to where the user left it. Use
|
|
857
|
+
// behavior:'instant' so an app-level `scroll-behavior: smooth`
|
|
858
|
+
// stylesheet does not animate the restore (native nav jumps).
|
|
805
859
|
if (typeof window !== 'undefined') {
|
|
806
|
-
window.scrollTo(cached.scrollX, cached.scrollY);
|
|
860
|
+
window.scrollTo({ left: cached.scrollX, top: cached.scrollY, behavior: 'instant' });
|
|
807
861
|
}
|
|
808
862
|
// Fire-and-forget revalidation. Uses a fresh AbortController
|
|
809
863
|
// since this background fetch is allowed to overlap with the
|
|
@@ -819,7 +873,7 @@ async function performNavigation(href, isPopState, frameId) {
|
|
|
819
873
|
// on the page they popped FROM. Scroll to top as the reasonable
|
|
820
874
|
// default; fetchAndApply skips its own scroll handling when
|
|
821
875
|
// recordHistory=false (which is the case here).
|
|
822
|
-
if (typeof window !== 'undefined') window.scrollTo(0, 0);
|
|
876
|
+
if (typeof window !== 'undefined') window.scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
|
823
877
|
}
|
|
824
878
|
|
|
825
879
|
await fetchAndApply(href, frameId, !isPopState, optimisticState, 'GET', null, signal, myToken);
|
|
@@ -828,7 +882,7 @@ async function performNavigation(href, isPopState, frameId) {
|
|
|
828
882
|
// Only clear the navigating flag if WE are still the active nav.
|
|
829
883
|
// A newer nav has its own flag lifecycle.
|
|
830
884
|
if (myToken === currentNavigationToken) {
|
|
831
|
-
|
|
885
|
+
setNavigating(false);
|
|
832
886
|
// Record where the user is NOW so the next navigation can
|
|
833
887
|
// snapshot under the right URL key.
|
|
834
888
|
if (typeof location !== 'undefined') currentPageUrl = location.href;
|
|
@@ -895,7 +949,7 @@ async function performSubmission(href, method, body, frameId, form) {
|
|
|
895
949
|
if (currentPageUrl) snapshotCurrent(currentPageUrl);
|
|
896
950
|
|
|
897
951
|
let navigatingFlagTimer = setTimeout(() => {
|
|
898
|
-
|
|
952
|
+
setNavigating(true);
|
|
899
953
|
navigatingFlagTimer = null;
|
|
900
954
|
}, 150);
|
|
901
955
|
|
|
@@ -935,7 +989,7 @@ async function performSubmission(href, method, body, frameId, form) {
|
|
|
935
989
|
if (busyForm) clearFormBusy(busyForm, myToken, url.href, outcomeOk);
|
|
936
990
|
if (navigatingFlagTimer) clearTimeout(navigatingFlagTimer);
|
|
937
991
|
if (myToken === currentNavigationToken) {
|
|
938
|
-
|
|
992
|
+
setNavigating(false);
|
|
939
993
|
if (typeof location !== 'undefined') currentPageUrl = location.href;
|
|
940
994
|
}
|
|
941
995
|
}
|
|
@@ -1719,10 +1773,16 @@ async function fetchAndApply(href, frameId, recordHistory, optimisticState, meth
|
|
|
1719
1773
|
const url = new URL(finalUrl);
|
|
1720
1774
|
if (url.hash) {
|
|
1721
1775
|
const t = document.getElementById(url.hash.slice(1));
|
|
1776
|
+
// A hash anchor is the one nav scroll we DON'T force instant: a
|
|
1777
|
+
// `#section` link is exactly where an app's `scroll-behavior: smooth`
|
|
1778
|
+
// is wanted, and native browsers animate it too.
|
|
1722
1779
|
if (t) t.scrollIntoView();
|
|
1723
|
-
else window.scrollTo(0, 0);
|
|
1780
|
+
else { warnIfSmoothScrollOnHtml(); window.scrollTo({ left: 0, top: 0, behavior: 'instant' }); }
|
|
1724
1781
|
} else {
|
|
1725
|
-
|
|
1782
|
+
// Scroll-to-top on a forward nav. behavior:'instant' so an app-level
|
|
1783
|
+
// `scroll-behavior: smooth` does not animate it (match native nav).
|
|
1784
|
+
warnIfSmoothScrollOnHtml();
|
|
1785
|
+
window.scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
|
1726
1786
|
}
|
|
1727
1787
|
}
|
|
1728
1788
|
|
|
@@ -3199,6 +3259,8 @@ export function _bumpNavToken() { return ++currentNavigationToken; }
|
|
|
3199
3259
|
export function _currentPageUrl() { return currentPageUrl; }
|
|
3200
3260
|
/** Test-only: set the tracker (simulates being on a specific page). */
|
|
3201
3261
|
export function _setCurrentPageUrl(u) { currentPageUrl = u; }
|
|
3262
|
+
/** Test-only: clear the fire-once warning guards so a case can be re-exercised. */
|
|
3263
|
+
export function _resetWarnOnce() { warnedKeys.clear(); smoothScrollChecked = false; }
|
|
3202
3264
|
|
|
3203
3265
|
/**
|
|
3204
3266
|
* Predicate used by the onClick handler to decide whether a same-origin
|
|
@@ -3211,7 +3273,13 @@ export function _isNonHtmlPath(pathname) {
|
|
|
3211
3273
|
return NON_HTML_EXTENSIONS.test(pathname);
|
|
3212
3274
|
}
|
|
3213
3275
|
|
|
3214
|
-
// Auto-enable on import (standard Turbo-Drive convention)
|
|
3215
|
-
//
|
|
3216
|
-
//
|
|
3217
|
-
|
|
3276
|
+
// Auto-enable on import (standard Turbo-Drive convention) UNLESS the app opted
|
|
3277
|
+
// out with `webjs.clientRouter: false` (#629), which the server signals by
|
|
3278
|
+
// setting `window.__WEBJS_CLIENT_ROUTER__ = false` in an inline script emitted
|
|
3279
|
+
// BEFORE this (deferred) bundle runs. On the server `window` is undefined, so
|
|
3280
|
+
// the call still runs and no-ops behind its own `typeof document` guard, as
|
|
3281
|
+
// before. Placed last so every top-level binding the router touches (notably
|
|
3282
|
+
// the prefetch state) is initialised before enableClientRouter() runs.
|
|
3283
|
+
if (typeof window === 'undefined' || window.__WEBJS_CLIENT_ROUTER__ !== false) {
|
|
3284
|
+
enableClientRouter();
|
|
3285
|
+
}
|
package/src/webjs-config.d.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* LOCKSTEP: this file, the JSON Schema at
|
|
16
16
|
* packages/server/webjs-config.schema.json, and the server reader
|
|
17
17
|
* functions MUST stay in sync. The readers are: readElideEnabled
|
|
18
|
-
* (dev.js, elide),
|
|
18
|
+
* (dev.js, elide), readSeedEnabled (dev.js, seed), readClientRouterEnabled
|
|
19
|
+
* (dev.js, clientRouter), compileHeaderRules (headers.js, headers),
|
|
19
20
|
* compileRedirectRules / readTrailingSlashPolicy (redirects.js,
|
|
20
21
|
* redirects / trailingSlash), readBasePath (base-path.js, basePath),
|
|
21
22
|
* readCspConfig (csp.js, csp), and
|
|
@@ -144,6 +145,15 @@ export interface WebjsConfig {
|
|
|
144
145
|
*/
|
|
145
146
|
seed?: boolean;
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Client-router switch (#629). Default `true`: the client router auto-enables
|
|
150
|
+
* in the browser whenever `@webjsdev/core` loads, so any page that ships a
|
|
151
|
+
* component gets SPA-style navigation with no import. Set to `false` to opt
|
|
152
|
+
* the whole app out (pure MPA, full-page navigation; `disableClientRouter()`
|
|
153
|
+
* stays the programmatic escape hatch).
|
|
154
|
+
*/
|
|
155
|
+
clientRouter?: boolean;
|
|
156
|
+
|
|
147
157
|
/**
|
|
148
158
|
* Dev/start task orchestration (#550). `webjs dev` / `webjs start` run these
|
|
149
159
|
* so a bare CLI invocation matches `npm run dev` / `start`. Read by the CLI
|