@ecopages/browser-router 0.2.0-alpha.6 → 0.2.0-alpha.8

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/CHANGELOG.md CHANGED
@@ -10,12 +10,15 @@ All notable changes to `@ecopages/browser-router` are documented here.
10
10
 
11
11
  - Added cross-router navigation handoff and document adoption hooks so browser-router can exchange control with other runtimes without forcing a reload.
12
12
  - Improved head swaps to execute newly introduced scripts and preserve light-DOM custom elements during page transitions.
13
+ - Added `documentElementAttributesToSync` so apps can explicitly control which root `<html>` attributes browser-router synchronizes during navigation.
14
+ - Added public document sync helpers so advanced setups can reuse browser-router's root `<html>` attribute synchronization logic without overriding the router pipeline.
13
15
 
14
16
  ### Bug Fixes
15
17
 
16
18
  - Fixed stale delegated navigations and rapid-click races so only the latest browser-router navigation can commit a swap.
17
19
  - Re-executed `data-eco-rerun` scripts after body replacement and forced fresh module URLs for external rerun scripts so page bootstraps rebind correctly on every navigation.
18
20
  - Synced rendered document ownership markers and live `<html>` attributes during swaps so mixed browser-router and React-router pages preserve the correct hydration owner.
21
+ - Preserved client-managed `<html>` state such as theme classes and data attributes while still syncing router-owned document metadata during swaps.
19
22
  - Prevented duplicate head-script execution, duplicate `/_hmr_runtime.js` injection, and listener accumulation across repeated navigations.
20
23
  - Reset hydrated custom elements from incoming HTML and ignored superseded navigation fetch failures so cross-runtime handoffs no longer leave blank or mixed DOM state behind.
21
24
 
package/README.md CHANGED
@@ -49,6 +49,28 @@ import { createRouter } from '@ecopages/browser-router/client';
49
49
  const router = createRouter({
50
50
  viewTransitions: true,
51
51
  scrollBehavior: 'auto',
52
+ documentElementAttributesToSync: ['lang', 'dir', 'data-theme'],
53
+ });
54
+ ```
55
+
56
+ By default, browser-router only syncs root `<html>` metadata it owns. Client-managed attributes and classes such as theme state are preserved unless you explicitly include them in `documentElementAttributesToSync`.
57
+
58
+ For advanced cases, browser-router also exports low-level document sync tooling without changing the router instance API:
59
+
60
+ ```ts
61
+ import {
62
+ createRouter,
63
+ defaultDocumentElementAttributesToSync,
64
+ syncDocumentElementAttributes,
65
+ } from '@ecopages/browser-router';
66
+
67
+ const router = createRouter();
68
+
69
+ document.addEventListener('eco:before-swap', (event) => {
70
+ syncDocumentElementAttributes(document, event.detail.newDocument, [
71
+ ...defaultDocumentElementAttributesToSync,
72
+ 'data-theme',
73
+ ]);
52
74
  });
53
75
  ```
54
76
 
@@ -56,15 +78,16 @@ Loading the router script is the opt-in point for browser-router-managed navigat
56
78
 
57
79
  ## Configuration
58
80
 
59
- | Option | Type | Default | Description |
60
- | :----------------- | :------------------------------ | :------------------- | :--------------------------------------------- |
61
- | `linkSelector` | `string` | `'a[href]'` | Selector for links to intercept |
62
- | `persistAttribute` | `string` | `'data-eco-persist'` | Attribute to mark elements for DOM persistence |
63
- | `reloadAttribute` | `string` | `'data-eco-reload'` | Attribute to force full page reload |
64
- | `updateHistory` | `boolean` | `true` | Whether to update browser history |
65
- | `scrollBehavior` | `'top' \| 'preserve' \| 'auto'` | `'top'` | Scroll behavior after navigation |
66
- | `viewTransitions` | `boolean` | `false` | Use View Transition API for animations |
67
- | `smoothScroll` | `boolean` | `false` | Use smooth scrolling during navigation |
81
+ | Option | Type | Default | Description |
82
+ | :-------------------------------- | :------------------------------ | :------------------------------------------- | :------------------------------------------------------------------------------------------ |
83
+ | `linkSelector` | `string` | `'a[href]'` | Selector for links to intercept |
84
+ | `documentElementAttributesToSync` | `string[]` | `['lang', 'dir', 'data-eco-document-owner']` | `<html>` attributes to sync from the incoming document; other root attributes are preserved |
85
+ | `persistAttribute` | `string` | `'data-eco-persist'` | Attribute to mark elements for DOM persistence |
86
+ | `reloadAttribute` | `string` | `'data-eco-reload'` | Attribute to force full page reload |
87
+ | `updateHistory` | `boolean` | `true` | Whether to update browser history |
88
+ | `scrollBehavior` | `'top' \| 'preserve' \| 'auto'` | `'top'` | Scroll behavior after navigation |
89
+ | `viewTransitions` | `boolean` | `false` | Use View Transition API for animations |
90
+ | `smoothScroll` | `boolean` | `false` | Use smooth scrolling during navigation |
68
91
 
69
92
  ## Persistence
70
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/browser-router",
3
- "version": "0.2.0-alpha.6",
3
+ "version": "0.2.0-alpha.8",
4
4
  "description": "Client-side router for Ecopages with view transitions support",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -32,7 +32,7 @@
32
32
  "directory": "packages/browser-router"
33
33
  },
34
34
  "dependencies": {
35
- "@ecopages/core": "0.2.0-alpha.6",
35
+ "@ecopages/core": "0.2.0-alpha.8",
36
36
  "morphdom": "^2.7.8"
37
37
  },
38
38
  "types": "./src/index.d.ts"
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Helpers for synchronizing selected root `<html>` attributes during client-side navigation.
3
+ * @module
4
+ */
5
+ /**
6
+ * Default root `<html>` attributes that browser-router treats as document-owned.
7
+ *
8
+ * These attributes are synchronized from the incoming document during navigation.
9
+ * Other root attributes are preserved unless explicitly included.
10
+ */
11
+ export declare const defaultDocumentElementAttributesToSync: string[];
12
+ /**
13
+ * Synchronizes a selected set of root `<html>` attributes from an incoming document
14
+ * onto the current live document.
15
+ *
16
+ * Attributes listed here are treated as document-owned metadata. Attributes not
17
+ * listed remain untouched on the live document so client-managed state can survive
18
+ * across navigation swaps.
19
+ *
20
+ * @param currentDocument - The live document being updated
21
+ * @param newDocument - The parsed incoming document for the next page
22
+ * @param attributes - Root `<html>` attributes to synchronize
23
+ */
24
+ export declare function syncDocumentElementAttributes(currentDocument: Document, newDocument: Document, attributes: readonly string[]): void;
@@ -0,0 +1,20 @@
1
+ import { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC } from "./types.js";
2
+ const defaultDocumentElementAttributesToSync = DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC;
3
+ function syncDocumentElementAttributes(currentDocument, newDocument, attributes) {
4
+ const currentHtml = currentDocument.documentElement;
5
+ const nextHtml = newDocument.documentElement;
6
+ for (const attributeName of attributes) {
7
+ const nextValue = nextHtml.getAttribute(attributeName);
8
+ if (nextValue === null) {
9
+ currentHtml.removeAttribute(attributeName);
10
+ continue;
11
+ }
12
+ if (currentHtml.getAttribute(attributeName) !== nextValue) {
13
+ currentHtml.setAttribute(attributeName, nextValue);
14
+ }
15
+ }
16
+ }
17
+ export {
18
+ defaultDocumentElementAttributesToSync,
19
+ syncDocumentElementAttributes
20
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Helpers for synchronizing selected root `<html>` attributes during client-side navigation.
3
+ * @module
4
+ */
5
+
6
+ import { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC } from './types.ts';
7
+
8
+ /**
9
+ * Default root `<html>` attributes that browser-router treats as document-owned.
10
+ *
11
+ * These attributes are synchronized from the incoming document during navigation.
12
+ * Other root attributes are preserved unless explicitly included.
13
+ */
14
+ export const defaultDocumentElementAttributesToSync = DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC;
15
+
16
+ /**
17
+ * Synchronizes a selected set of root `<html>` attributes from an incoming document
18
+ * onto the current live document.
19
+ *
20
+ * Attributes listed here are treated as document-owned metadata. Attributes not
21
+ * listed remain untouched on the live document so client-managed state can survive
22
+ * across navigation swaps.
23
+ *
24
+ * @param currentDocument - The live document being updated
25
+ * @param newDocument - The parsed incoming document for the next page
26
+ * @param attributes - Root `<html>` attributes to synchronize
27
+ */
28
+ export function syncDocumentElementAttributes(
29
+ currentDocument: Document,
30
+ newDocument: Document,
31
+ attributes: readonly string[],
32
+ ): void {
33
+ const currentHtml = currentDocument.documentElement;
34
+ const nextHtml = newDocument.documentElement;
35
+
36
+ for (const attributeName of attributes) {
37
+ const nextValue = nextHtml.getAttribute(attributeName);
38
+
39
+ if (nextValue === null) {
40
+ currentHtml.removeAttribute(attributeName);
41
+ continue;
42
+ }
43
+
44
+ if (currentHtml.getAttribute(attributeName) !== nextValue) {
45
+ currentHtml.setAttribute(attributeName, nextValue);
46
+ }
47
+ }
48
+ }
@@ -3,7 +3,8 @@ import {
3
3
  getAnchorFromNavigationEvent,
4
4
  recoverPendingNavigationHref
5
5
  } from "@ecopages/core/router/link-intent";
6
- import { DEFAULT_OPTIONS } from "./types.js";
6
+ import { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from "./types.js";
7
+ import { syncDocumentElementAttributes } from "./document-element-sync.js";
7
8
  import { DomSwapper, ScrollManager, ViewTransitionManager, PrefetchManager } from "./services/index.js";
8
9
  class EcoRouter {
9
10
  options;
@@ -18,7 +19,13 @@ class EcoRouter {
18
19
  viewTransitionManager;
19
20
  prefetchManager = null;
20
21
  constructor(options = {}) {
21
- this.options = { ...DEFAULT_OPTIONS, ...options };
22
+ this.options = {
23
+ ...DEFAULT_OPTIONS,
24
+ ...options,
25
+ documentElementAttributesToSync: [
26
+ ...options.documentElementAttributesToSync ?? DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC
27
+ ]
28
+ };
22
29
  this.domSwapper = new DomSwapper(this.options.persistAttribute);
23
30
  this.scrollManager = new ScrollManager(this.options.scrollBehavior, this.options.smoothScroll);
24
31
  this.viewTransitionManager = new ViewTransitionManager(this.options.viewTransitions);
@@ -84,18 +91,7 @@ class EcoRouter {
84
91
  getEcoNavigationRuntime(window).adoptDocumentOwner(doc, "browser-router");
85
92
  }
86
93
  syncDocumentElementAttributes(newDocument) {
87
- const currentHtml = document.documentElement;
88
- const nextHtml = newDocument.documentElement;
89
- for (const attribute of Array.from(currentHtml.attributes)) {
90
- if (!nextHtml.hasAttribute(attribute.name)) {
91
- currentHtml.removeAttribute(attribute.name);
92
- }
93
- }
94
- for (const attribute of Array.from(nextHtml.attributes)) {
95
- if (currentHtml.getAttribute(attribute.name) !== attribute.value) {
96
- currentHtml.setAttribute(attribute.name, attribute.value);
97
- }
98
- }
94
+ syncDocumentElementAttributes(document, newDocument, this.options.documentElementAttributesToSync);
99
95
  }
100
96
  reloadDocument(url) {
101
97
  window.location.assign(url.href);
@@ -4,13 +4,14 @@
4
4
  */
5
5
 
6
6
  import type { EcoRouterOptions, EcoNavigationEvent, EcoBeforeSwapEvent, EcoAfterSwapEvent } from './types.ts';
7
- import { getEcoNavigationRuntime } from '@ecopages/core/router/navigation-coordinator';
7
+ import { ECO_DOCUMENT_OWNER_ATTRIBUTE, getEcoNavigationRuntime } from '@ecopages/core/router/navigation-coordinator';
8
8
  import {
9
9
  getAnchorFromNavigationEvent,
10
10
  recoverPendingNavigationHref,
11
11
  type EcoPendingNavigationIntent,
12
12
  } from '@ecopages/core/router/link-intent';
13
- import { DEFAULT_OPTIONS } from './types.ts';
13
+ import { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from './types.ts';
14
+ import { syncDocumentElementAttributes } from './document-element-sync.ts';
14
15
  import { DomSwapper, ScrollManager, ViewTransitionManager, PrefetchManager } from './services/index.ts';
15
16
 
16
17
  /**
@@ -32,7 +33,13 @@ export class EcoRouter {
32
33
  private prefetchManager: PrefetchManager | null = null;
33
34
 
34
35
  constructor(options: EcoRouterOptions = {}) {
35
- this.options = { ...DEFAULT_OPTIONS, ...options };
36
+ this.options = {
37
+ ...DEFAULT_OPTIONS,
38
+ ...options,
39
+ documentElementAttributesToSync: [
40
+ ...(options.documentElementAttributesToSync ?? DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC),
41
+ ],
42
+ };
36
43
 
37
44
  this.domSwapper = new DomSwapper(this.options.persistAttribute);
38
45
  this.scrollManager = new ScrollManager(this.options.scrollBehavior, this.options.smoothScroll);
@@ -121,20 +128,7 @@ export class EcoRouter {
121
128
  }
122
129
 
123
130
  private syncDocumentElementAttributes(newDocument: Document): void {
124
- const currentHtml = document.documentElement;
125
- const nextHtml = newDocument.documentElement;
126
-
127
- for (const attribute of Array.from(currentHtml.attributes)) {
128
- if (!nextHtml.hasAttribute(attribute.name)) {
129
- currentHtml.removeAttribute(attribute.name);
130
- }
131
- }
132
-
133
- for (const attribute of Array.from(nextHtml.attributes)) {
134
- if (currentHtml.getAttribute(attribute.name) !== attribute.value) {
135
- currentHtml.setAttribute(attribute.name, attribute.value);
136
- }
137
- }
131
+ syncDocumentElementAttributes(document, newDocument, this.options.documentElementAttributesToSync);
138
132
  }
139
133
 
140
134
  private reloadDocument(url: URL): void {
@@ -36,6 +36,13 @@ export interface PrefetchConfig {
36
36
  export interface EcoRouterOptions {
37
37
  /** Selector for links to intercept. @default 'a[href]' */
38
38
  linkSelector?: string;
39
+ /**
40
+ * Document-level `<html>` attributes to sync from the incoming document during navigation.
41
+ * Attributes not listed here are preserved on the live document element so client-managed
42
+ * state such as theme classes or data attributes is not clobbered during swaps.
43
+ * @default ['lang', 'dir', 'data-eco-document-owner']
44
+ */
45
+ documentElementAttributesToSync?: string[];
39
46
  /** Attribute to mark elements for DOM persistence. @default 'data-eco-persist' */
40
47
  persistAttribute?: string;
41
48
  /** Attribute to force full page reload. @default 'data-eco-reload' */
@@ -83,5 +90,7 @@ export interface EcoBeforeSwapEvent extends EcoNavigationEvent {
83
90
  /** Event fired after the DOM swap completes */
84
91
  export interface EcoAfterSwapEvent extends EcoNavigationEvent {
85
92
  }
93
+ /** Default document-level `<html>` attributes synchronized during navigation swaps. */
94
+ export declare const DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC: string[];
86
95
  /** Default configuration options */
87
96
  export declare const DEFAULT_OPTIONS: Required<EcoRouterOptions>;
@@ -1,11 +1,14 @@
1
+ import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from "@ecopages/core/router/navigation-coordinator";
1
2
  const DEFAULT_PREFETCH_CONFIG = {
2
3
  strategy: "intent",
3
4
  delay: 65,
4
5
  noPrefetchAttribute: "data-eco-no-prefetch",
5
6
  respectDataSaver: true
6
7
  };
8
+ const DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC = ["lang", "dir", ECO_DOCUMENT_OWNER_ATTRIBUTE];
7
9
  const DEFAULT_OPTIONS = {
8
10
  linkSelector: "a[href]",
11
+ documentElementAttributesToSync: DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
9
12
  persistAttribute: "data-eco-persist",
10
13
  reloadAttribute: "data-eco-reload",
11
14
  updateHistory: true,
@@ -15,5 +18,6 @@ const DEFAULT_OPTIONS = {
15
18
  prefetch: DEFAULT_PREFETCH_CONFIG
16
19
  };
17
20
  export {
21
+ DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
18
22
  DEFAULT_OPTIONS
19
23
  };
@@ -1,3 +1,5 @@
1
+ import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from '@ecopages/core/router/navigation-coordinator';
2
+
1
3
  /**
2
4
  * Shared types for the EcoPages transitions package
3
5
  * @module
@@ -41,6 +43,13 @@ export interface PrefetchConfig {
41
43
  export interface EcoRouterOptions {
42
44
  /** Selector for links to intercept. @default 'a[href]' */
43
45
  linkSelector?: string;
46
+ /**
47
+ * Document-level `<html>` attributes to sync from the incoming document during navigation.
48
+ * Attributes not listed here are preserved on the live document element so client-managed
49
+ * state such as theme classes or data attributes is not clobbered during swaps.
50
+ * @default ['lang', 'dir', 'data-eco-document-owner']
51
+ */
52
+ documentElementAttributesToSync?: string[];
44
53
  /** Attribute to mark elements for DOM persistence. @default 'data-eco-persist' */
45
54
  persistAttribute?: string;
46
55
  /** Attribute to force full page reload. @default 'data-eco-reload' */
@@ -100,9 +109,13 @@ const DEFAULT_PREFETCH_CONFIG: Required<PrefetchConfig> = {
100
109
  respectDataSaver: true,
101
110
  };
102
111
 
112
+ /** Default document-level `<html>` attributes synchronized during navigation swaps. */
113
+ export const DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC = ['lang', 'dir', ECO_DOCUMENT_OWNER_ATTRIBUTE];
114
+
103
115
  /** Default configuration options */
104
116
  export const DEFAULT_OPTIONS: Required<EcoRouterOptions> = {
105
117
  linkSelector: 'a[href]',
118
+ documentElementAttributesToSync: DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
106
119
  persistAttribute: 'data-eco-persist',
107
120
  reloadAttribute: 'data-eco-reload',
108
121
  updateHistory: true,
package/src/index.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * @module
5
5
  */
6
6
  export type { EcoRouterOptions, EcoNavigationEvent, EcoBeforeSwapEvent, EcoAfterSwapEvent, EcoRouterEventMap, } from './types.js';
7
- export { DEFAULT_OPTIONS } from './types.js';
7
+ export { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from './types.js';
8
+ export { defaultDocumentElementAttributesToSync, syncDocumentElementAttributes, } from './client/document-element-sync.js';
8
9
  export { EcoRouter, createRouter } from './client/eco-router.js';
9
10
  export { DomSwapper, ScrollManager, ViewTransitionManager } from './client/services/index.js';
package/src/index.js CHANGED
@@ -1,11 +1,18 @@
1
- import { DEFAULT_OPTIONS } from "./types.js";
1
+ import { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from "./types.js";
2
+ import {
3
+ defaultDocumentElementAttributesToSync,
4
+ syncDocumentElementAttributes
5
+ } from "./client/document-element-sync.js";
2
6
  import { EcoRouter, createRouter } from "./client/eco-router.js";
3
7
  import { DomSwapper, ScrollManager, ViewTransitionManager } from "./client/services/index.js";
4
8
  export {
9
+ DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
5
10
  DEFAULT_OPTIONS,
6
11
  DomSwapper,
7
12
  EcoRouter,
8
13
  ScrollManager,
9
14
  ViewTransitionManager,
10
- createRouter
15
+ createRouter,
16
+ defaultDocumentElementAttributesToSync,
17
+ syncDocumentElementAttributes
11
18
  };
package/src/index.ts CHANGED
@@ -12,7 +12,11 @@ export type {
12
12
  EcoRouterEventMap,
13
13
  } from './types.ts';
14
14
 
15
- export { DEFAULT_OPTIONS } from './types.ts';
15
+ export { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from './types.ts';
16
+ export {
17
+ defaultDocumentElementAttributesToSync,
18
+ syncDocumentElementAttributes,
19
+ } from './client/document-element-sync.ts';
16
20
 
17
21
  export { EcoRouter, createRouter } from './client/eco-router.ts';
18
22
 
package/src/types.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { EcoNavigationEvent, EcoBeforeSwapEvent, EcoAfterSwapEvent } from './client/types';
6
6
  export type { EcoRouterOptions, EcoNavigationEvent, EcoBeforeSwapEvent, EcoAfterSwapEvent } from './client/types';
7
- export { DEFAULT_OPTIONS } from './client/types';
7
+ export { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from './client/types';
8
8
  /**
9
9
  * Custom event map for navigation lifecycle
10
10
  */
package/src/types.js CHANGED
@@ -1,4 +1,5 @@
1
- import { DEFAULT_OPTIONS } from "./client/types";
1
+ import { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from "./client/types";
2
2
  export {
3
+ DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
3
4
  DEFAULT_OPTIONS
4
5
  };
package/src/types.ts CHANGED
@@ -7,7 +7,7 @@ import type { EcoNavigationEvent, EcoBeforeSwapEvent, EcoAfterSwapEvent } from '
7
7
 
8
8
  export type { EcoRouterOptions, EcoNavigationEvent, EcoBeforeSwapEvent, EcoAfterSwapEvent } from './client/types';
9
9
 
10
- export { DEFAULT_OPTIONS } from './client/types';
10
+ export { DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC, DEFAULT_OPTIONS } from './client/types';
11
11
 
12
12
  /**
13
13
  * Custom event map for navigation lifecycle