@ecopages/browser-router 0.2.0-alpha.1 → 0.2.0-alpha.10
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 +12 -6
- package/README.md +59 -36
- package/package.json +4 -2
- package/src/client/document-element-sync.d.ts +24 -0
- package/src/client/document-element-sync.js +20 -0
- package/src/client/eco-router.d.ts +35 -1
- package/src/client/eco-router.js +334 -68
- package/src/client/services/dom-swapper.d.ts +23 -0
- package/src/client/services/dom-swapper.js +210 -38
- package/src/client/services/prefetch-manager.d.ts +1 -0
- package/src/client/services/prefetch-manager.js +5 -0
- package/src/client/services/view-transition-manager.d.ts +7 -1
- package/src/client/services/view-transition-manager.js +10 -5
- package/src/client/types.d.ts +12 -0
- package/src/client/types.js +4 -0
- package/src/index.d.ts +2 -1
- package/src/index.js +9 -2
- package/src/types.d.ts +1 -1
- package/src/types.js +2 -1
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Initialization-should-create-router-instance-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Initialization-should-start-and-stop-without-errors-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Lifecycle-Events-should-dispatch-eco-before-swap--eco-after-swap--and-eco-page-load-events-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Lifecycle-Events-should-dispatch-eco-page-load-event-after-animation-frame-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Lifecycle-Events-should-provide-event-details-with-url-and-direction-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Custom-Link-Selector-should-work-with-data-attribute-selector-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Custom-Reload-Attribute-should-intercept-links-with-default-reload-attribute-when-custom-is-set-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Custom-link-selector-should-only-intercept-links-matching-custom-selector-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-External-Links--should-NOT-intercept--should-NOT-intercept-external-links-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Internal-Links-should-intercept-clicks-on-relative-path-links-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Internal-Links-should-intercept-clicks-on-same-origin-absolute-URLs-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Link-Attributes--should-NOT-intercept--should-NOT-intercept-links-with-download-attribute-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Link-Attributes--should-NOT-intercept--should-intercept-links-with-target---self--1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Modifier-keys-should-NOT-intercept-alt-click-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Modifier-keys-should-NOT-intercept-ctrl-click-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Modifier-keys-should-NOT-intercept-meta-click--cmd-on-Mac--1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Modifier-keys-should-NOT-intercept-middle-mouse-button-click-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Modifier-keys-should-NOT-intercept-right-mouse-button-click-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Modifier-keys-should-NOT-intercept-shift-click-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-external-links--different-origin--1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-hash-only-links-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-javascript--links-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-with-custom-reload-attribute-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-with-data-eco-reload-attribute-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-with-download-attribute-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-with-empty-href-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-with-target---blank--1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-with-target---parent--1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-NOT-intercept-links-without-href-attribute-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-intercept-internal-links-with-absolute-same-origin-paths-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-intercept-internal-links-with-relative-paths-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-Should-intercept-nested-elements-inside-links-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Link-Interception-should-NOT-intercept-external-links-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Navigation-Abort-should-abort-previous-navigation-when-new-one-starts-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Programmatic-Navigation-should-navigate-and-update-history-with-pushState-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.browser.ts/EcoRouter-Programmatic-Navigation-should-use-replaceState-when-replace-option-is-true-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.ts/EcoRouter-Error-Handling-should-fall-back-to-full-page-navigation-on-fetch-error-1.png +0 -0
- package/src/client/__screenshots__/eco-router.test.ts/EcoRouter-Error-Handling-should-log-error-and-attempt-fallback-navigation-on-fetch-error-1.png +0 -0
- package/src/client/eco-router.ts +0 -290
- package/src/client/services/dom-swapper.ts +0 -325
- package/src/client/services/index.ts +0 -9
- package/src/client/services/prefetch-manager.ts +0 -451
- package/src/client/services/scroll-manager.ts +0 -48
- package/src/client/services/view-transition-manager.ts +0 -75
- package/src/client/types.ts +0 -109
- package/src/client/view-transition-utils.ts +0 -98
- package/src/index.ts +0 -19
- package/src/types.ts +0 -19
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import morphdom from "morphdom";
|
|
2
2
|
const DEFAULT_PERSIST_ATTR = "data-eco-persist";
|
|
3
|
+
const RERUN_SRC_ATTR = "data-eco-rerun-src";
|
|
3
4
|
function isPersisted(element, persistAttribute) {
|
|
4
5
|
return element.hasAttribute(persistAttribute) || element.hasAttribute(DEFAULT_PERSIST_ATTR);
|
|
5
6
|
}
|
|
@@ -8,6 +9,9 @@ function isHydratedCustomElement(element) {
|
|
|
8
9
|
}
|
|
9
10
|
class DomSwapper {
|
|
10
11
|
persistAttribute;
|
|
12
|
+
pendingHeadScripts = [];
|
|
13
|
+
pendingRerunScripts = [];
|
|
14
|
+
rerunNonce = 0;
|
|
11
15
|
constructor(persistAttribute) {
|
|
12
16
|
this.persistAttribute = persistAttribute;
|
|
13
17
|
}
|
|
@@ -78,6 +82,9 @@ class DomSwapper {
|
|
|
78
82
|
* - Injects new scripts from the incoming page that are absent from the current head
|
|
79
83
|
*/
|
|
80
84
|
morphHead(newDocument) {
|
|
85
|
+
this.pendingHeadScripts = [];
|
|
86
|
+
this.pendingRerunScripts = this.collectRerunScripts(newDocument);
|
|
87
|
+
this.removeStaleHeadScripts(newDocument);
|
|
81
88
|
const newTitle = newDocument.head.querySelector("title");
|
|
82
89
|
if (newTitle && document.title !== newTitle.textContent) {
|
|
83
90
|
document.title = newTitle.textContent || "";
|
|
@@ -97,25 +104,6 @@ class DomSwapper {
|
|
|
97
104
|
document.head.appendChild(newMeta.cloneNode(true));
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
|
-
const existingScriptIds = new Set(
|
|
101
|
-
Array.from(document.head.querySelectorAll("script[data-eco-script-id]")).map(
|
|
102
|
-
(s) => s.getAttribute("data-eco-script-id")
|
|
103
|
-
)
|
|
104
|
-
);
|
|
105
|
-
const rerunScripts = newDocument.head.querySelectorAll("script[data-eco-rerun]");
|
|
106
|
-
for (const script of rerunScripts) {
|
|
107
|
-
const scriptId = script.getAttribute("data-eco-script-id");
|
|
108
|
-
if (scriptId && !existingScriptIds.has(scriptId)) {
|
|
109
|
-
const newScript = document.createElement("script");
|
|
110
|
-
for (const attr of script.attributes) {
|
|
111
|
-
if (attr.name !== "data-eco-rerun") {
|
|
112
|
-
newScript.setAttribute(attr.name, attr.value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
newScript.textContent = script.textContent;
|
|
116
|
-
document.head.appendChild(newScript);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
107
|
const existingScriptSrcs = new Set(
|
|
120
108
|
Array.from(document.head.querySelectorAll("script[src]")).map((s) => s.getAttribute("src"))
|
|
121
109
|
);
|
|
@@ -126,27 +114,98 @@ class DomSwapper {
|
|
|
126
114
|
for (const script of allNewHeadScripts) {
|
|
127
115
|
if (script.hasAttribute("data-eco-rerun")) continue;
|
|
128
116
|
const src = script.getAttribute("src");
|
|
117
|
+
const scriptId = script.getAttribute("data-eco-script-id") || script.getAttribute("id");
|
|
118
|
+
const existingScript = this.findExistingHeadScript(script);
|
|
119
|
+
if (scriptId && existingScript) {
|
|
120
|
+
if (!this.isNonExecutableHeadScript(script)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (this.areHeadScriptsEquivalent(script, existingScript)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
this.pendingHeadScripts.push({
|
|
127
|
+
attributes: Array.from(script.attributes).map((attr) => [attr.name, attr.value]),
|
|
128
|
+
textContent: script.textContent ?? "",
|
|
129
|
+
src,
|
|
130
|
+
scriptId,
|
|
131
|
+
replaceExisting: true
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
129
135
|
if (src) {
|
|
130
136
|
if (existingScriptSrcs.has(src)) continue;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
this.pendingHeadScripts.push({
|
|
138
|
+
attributes: Array.from(script.attributes).map((attr) => [attr.name, attr.value]),
|
|
139
|
+
textContent: script.textContent ?? "",
|
|
140
|
+
src,
|
|
141
|
+
scriptId,
|
|
142
|
+
replaceExisting: false
|
|
143
|
+
});
|
|
136
144
|
existingScriptSrcs.add(src);
|
|
137
145
|
} else {
|
|
138
146
|
const content = (script.textContent ?? "").trim();
|
|
139
147
|
if (!content || existingInlineContents.has(content)) continue;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
this.pendingHeadScripts.push({
|
|
149
|
+
attributes: Array.from(script.attributes).map((attr) => [attr.name, attr.value]),
|
|
150
|
+
textContent: script.textContent ?? "",
|
|
151
|
+
src: null,
|
|
152
|
+
scriptId,
|
|
153
|
+
replaceExisting: false
|
|
154
|
+
});
|
|
146
155
|
existingInlineContents.add(content);
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Replays queued `data-eco-rerun` scripts after the body swap completes.
|
|
161
|
+
*
|
|
162
|
+
* Scripts are intentionally flushed after the new body is in place so DOM-
|
|
163
|
+
* dependent bootstraps bind against the incoming page rather than the page
|
|
164
|
+
* being replaced.
|
|
165
|
+
*/
|
|
166
|
+
flushRerunScripts() {
|
|
167
|
+
for (const script of this.pendingHeadScripts) {
|
|
168
|
+
const replacement = document.createElement("script");
|
|
169
|
+
for (const [name, value] of script.attributes) {
|
|
170
|
+
replacement.setAttribute(name, value);
|
|
171
|
+
}
|
|
172
|
+
replacement.textContent = script.textContent;
|
|
173
|
+
const existingScript = this.findExistingHeadScript(script);
|
|
174
|
+
if (script.replaceExisting && existingScript) {
|
|
175
|
+
existingScript.replaceWith(replacement);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
document.head.appendChild(replacement);
|
|
179
|
+
}
|
|
180
|
+
this.pendingHeadScripts = [];
|
|
181
|
+
for (const script of this.pendingRerunScripts) {
|
|
182
|
+
const targetParent = script.parent === "body" ? document.body : document.head;
|
|
183
|
+
const replacement = document.createElement("script");
|
|
184
|
+
const shouldBustModuleSrc = this.isExternalModuleRerunScript(script);
|
|
185
|
+
for (const [name, value] of script.attributes) {
|
|
186
|
+
if (name === "data-eco-rerun") {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (name === "src" && shouldBustModuleSrc) {
|
|
190
|
+
replacement.setAttribute(RERUN_SRC_ATTR, value);
|
|
191
|
+
replacement.setAttribute("src", this.createRerunScriptUrl(value));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
replacement.setAttribute(name, value);
|
|
195
|
+
}
|
|
196
|
+
replacement.textContent = script.textContent;
|
|
197
|
+
const existingScript = this.findExistingRerunScript(targetParent, script);
|
|
198
|
+
if (existingScript) {
|
|
199
|
+
existingScript.replaceWith(replacement);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
targetParent.appendChild(replacement);
|
|
203
|
+
}
|
|
204
|
+
this.pendingRerunScripts = [];
|
|
205
|
+
}
|
|
206
|
+
shouldReplaceBodyForRerunScripts() {
|
|
207
|
+
return this.pendingRerunScripts.length > 0;
|
|
208
|
+
}
|
|
150
209
|
/**
|
|
151
210
|
* Detects custom elements without shadow DOM (light-DOM custom elements).
|
|
152
211
|
* These need full replacement rather than morphing, because morphdom would
|
|
@@ -155,6 +214,15 @@ class DomSwapper {
|
|
|
155
214
|
isLightDomCustomElement(element) {
|
|
156
215
|
return element.localName.includes("-") && element.shadowRoot === null;
|
|
157
216
|
}
|
|
217
|
+
replaceCustomElement(fromEl, toEl) {
|
|
218
|
+
const newEl = document.createElement(toEl.tagName);
|
|
219
|
+
for (const attr of toEl.attributes) {
|
|
220
|
+
newEl.setAttribute(attr.name, attr.value);
|
|
221
|
+
}
|
|
222
|
+
newEl.innerHTML = toEl.innerHTML;
|
|
223
|
+
fromEl.replaceWith(newEl);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
158
226
|
/**
|
|
159
227
|
* Morphs document body using morphdom.
|
|
160
228
|
* Preserves persisted elements and hydrated custom elements.
|
|
@@ -169,16 +237,10 @@ class DomSwapper {
|
|
|
169
237
|
return false;
|
|
170
238
|
}
|
|
171
239
|
if (isHydratedCustomElement(fromEl)) {
|
|
172
|
-
return
|
|
240
|
+
return this.replaceCustomElement(fromEl, toEl);
|
|
173
241
|
}
|
|
174
242
|
if (this.isLightDomCustomElement(fromEl)) {
|
|
175
|
-
|
|
176
|
-
for (const attr of toEl.attributes) {
|
|
177
|
-
newEl.setAttribute(attr.name, attr.value);
|
|
178
|
-
}
|
|
179
|
-
newEl.innerHTML = toEl.innerHTML;
|
|
180
|
-
fromEl.replaceWith(newEl);
|
|
181
|
-
return false;
|
|
243
|
+
return this.replaceCustomElement(fromEl, toEl);
|
|
182
244
|
}
|
|
183
245
|
if (fromEl.isEqualNode(toEl)) {
|
|
184
246
|
return false;
|
|
@@ -214,6 +276,116 @@ class DomSwapper {
|
|
|
214
276
|
document.body.replaceChildren(...newDocument.body.childNodes);
|
|
215
277
|
this.processDeclarativeShadowDOM(document.body);
|
|
216
278
|
}
|
|
279
|
+
collectRerunScripts(newDocument) {
|
|
280
|
+
return Array.from(newDocument.querySelectorAll("script[data-eco-rerun]")).map((script) => ({
|
|
281
|
+
parent: script.closest("body") ? "body" : "head",
|
|
282
|
+
attributes: Array.from(script.attributes).map((attr) => [attr.name, attr.value]),
|
|
283
|
+
textContent: script.textContent ?? "",
|
|
284
|
+
src: script.getAttribute("src"),
|
|
285
|
+
scriptId: script.getAttribute("data-eco-script-id")
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
removeStaleHeadScripts(newDocument) {
|
|
289
|
+
const nextScriptKeys = new Set(
|
|
290
|
+
Array.from(newDocument.head.querySelectorAll("script")).map((script) => this.getHeadScriptKey(script)).filter((key) => key !== null)
|
|
291
|
+
);
|
|
292
|
+
for (const script of Array.from(document.head.querySelectorAll("script"))) {
|
|
293
|
+
const key = this.getHeadScriptKey(script);
|
|
294
|
+
if (!key || nextScriptKeys.has(key)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (this.shouldPersistExecutableInlineHeadScript(script)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
script.remove();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
shouldPersistExecutableInlineHeadScript(script) {
|
|
304
|
+
const scriptId = script.getAttribute("data-eco-script-id") || script.getAttribute("id");
|
|
305
|
+
if (!scriptId) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (script.hasAttribute("data-eco-rerun")) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (script.getAttribute(RERUN_SRC_ATTR) || script.getAttribute("src")) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return !this.isNonExecutableHeadScript(script);
|
|
315
|
+
}
|
|
316
|
+
isNonExecutableHeadScript(script) {
|
|
317
|
+
const type = (script.getAttribute("type") ?? "").trim().toLowerCase();
|
|
318
|
+
if (!type) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return ![
|
|
322
|
+
"application/javascript",
|
|
323
|
+
"application/ecmascript",
|
|
324
|
+
"module",
|
|
325
|
+
"text/ecmascript",
|
|
326
|
+
"text/javascript"
|
|
327
|
+
].includes(type);
|
|
328
|
+
}
|
|
329
|
+
areHeadScriptsEquivalent(nextScript, currentScript) {
|
|
330
|
+
if (this.getHeadScriptKey(nextScript) !== this.getHeadScriptKey(currentScript)) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
if ((nextScript.textContent ?? "") !== (currentScript.textContent ?? "")) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const nextAttributes = Array.from(nextScript.attributes).map((attribute) => [attribute.name, attribute.value]);
|
|
337
|
+
const currentAttributes = Array.from(currentScript.attributes).map((attribute) => [
|
|
338
|
+
attribute.name,
|
|
339
|
+
attribute.value
|
|
340
|
+
]);
|
|
341
|
+
if (nextAttributes.length !== currentAttributes.length) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return nextAttributes.every(
|
|
345
|
+
([name, value], index) => currentAttributes[index]?.[0] === name && currentAttributes[index]?.[1] === value
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
getHeadScriptKey(script) {
|
|
349
|
+
const scriptId = script instanceof HTMLScriptElement ? script.getAttribute("data-eco-script-id") || script.getAttribute("id") : script.scriptId;
|
|
350
|
+
if (scriptId) {
|
|
351
|
+
return `id:${scriptId}`;
|
|
352
|
+
}
|
|
353
|
+
const src = script instanceof HTMLScriptElement ? script.getAttribute(RERUN_SRC_ATTR) || script.getAttribute("src") : script.src;
|
|
354
|
+
if (src) {
|
|
355
|
+
return `src:${src}`;
|
|
356
|
+
}
|
|
357
|
+
const textContent = (script.textContent ?? "").trim();
|
|
358
|
+
return textContent ? `inline:${textContent}` : null;
|
|
359
|
+
}
|
|
360
|
+
findExistingHeadScript(script) {
|
|
361
|
+
const scriptKey = this.getHeadScriptKey(script);
|
|
362
|
+
if (!scriptKey) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return Array.from(document.head.querySelectorAll("script")).find(
|
|
366
|
+
(candidate) => this.getHeadScriptKey(candidate) === scriptKey
|
|
367
|
+
) ?? null;
|
|
368
|
+
}
|
|
369
|
+
findExistingRerunScript(root, script) {
|
|
370
|
+
const scripts = Array.from(root.querySelectorAll("script"));
|
|
371
|
+
if (script.scriptId) {
|
|
372
|
+
return scripts.find((candidate) => candidate.getAttribute("data-eco-script-id") === script.scriptId) ?? null;
|
|
373
|
+
}
|
|
374
|
+
return scripts.find(
|
|
375
|
+
(candidate) => (candidate.getAttribute(RERUN_SRC_ATTR) ?? candidate.getAttribute("src")) === script.src && (candidate.textContent ?? "") === script.textContent
|
|
376
|
+
) ?? null;
|
|
377
|
+
}
|
|
378
|
+
isExternalModuleRerunScript(script) {
|
|
379
|
+
if (!script.src) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
return script.attributes.some(([name, value]) => name === "type" && value === "module");
|
|
383
|
+
}
|
|
384
|
+
createRerunScriptUrl(src) {
|
|
385
|
+
const url = new URL(src, document.baseURI);
|
|
386
|
+
url.searchParams.set("__eco_rerun", String(++this.rerunNonce));
|
|
387
|
+
return url.toString();
|
|
388
|
+
}
|
|
217
389
|
/**
|
|
218
390
|
* Manually attaches declarative shadow DOM templates.
|
|
219
391
|
* Browsers only process `<template shadowrootmode>` during initial parse.
|
|
@@ -52,6 +52,7 @@ export declare class PrefetchManager {
|
|
|
52
52
|
* @returns The cached HTML string, or null if not cached
|
|
53
53
|
*/
|
|
54
54
|
getCachedHtml(href: string): string | null;
|
|
55
|
+
invalidate(href: string): void;
|
|
55
56
|
/**
|
|
56
57
|
* Caches HTML content for a visited page and triggers background revalidation.
|
|
57
58
|
*
|
|
@@ -89,6 +89,11 @@ class PrefetchManager {
|
|
|
89
89
|
const url = new URL(href, window.location.origin);
|
|
90
90
|
return this.htmlCache.get(url.href) ?? null;
|
|
91
91
|
}
|
|
92
|
+
invalidate(href) {
|
|
93
|
+
const url = new URL(href, window.location.origin);
|
|
94
|
+
this.htmlCache.delete(url.href);
|
|
95
|
+
this.prefetched.delete(url.href);
|
|
96
|
+
}
|
|
92
97
|
/**
|
|
93
98
|
* Caches HTML content for a visited page and triggers background revalidation.
|
|
94
99
|
*
|
|
@@ -16,8 +16,14 @@ export declare class ViewTransitionManager {
|
|
|
16
16
|
/**
|
|
17
17
|
* Execute a callback with view transition if available and enabled.
|
|
18
18
|
* Falls back to direct execution if not supported.
|
|
19
|
+
*
|
|
20
|
+
* Navigation correctness depends on the DOM update callback completing, not on
|
|
21
|
+
* the browser finishing the visual transition animation. Awaiting
|
|
22
|
+
* `finished` here can leave router transactions artificially in-flight and
|
|
23
|
+
* block later navigations during rapid repeated clicks, so the router waits
|
|
24
|
+
* for `updateCallbackDone` and lets the animation finish in the background.
|
|
19
25
|
* @param callback - The DOM update callback to execute
|
|
20
|
-
* @returns Promise that resolves when the
|
|
26
|
+
* @returns Promise that resolves when the DOM update has committed
|
|
21
27
|
*/
|
|
22
28
|
transition(callback: () => void | Promise<void>): Promise<void>;
|
|
23
29
|
}
|
|
@@ -13,8 +13,14 @@ class ViewTransitionManager {
|
|
|
13
13
|
/**
|
|
14
14
|
* Execute a callback with view transition if available and enabled.
|
|
15
15
|
* Falls back to direct execution if not supported.
|
|
16
|
+
*
|
|
17
|
+
* Navigation correctness depends on the DOM update callback completing, not on
|
|
18
|
+
* the browser finishing the visual transition animation. Awaiting
|
|
19
|
+
* `finished` here can leave router transactions artificially in-flight and
|
|
20
|
+
* block later navigations during rapid repeated clicks, so the router waits
|
|
21
|
+
* for `updateCallbackDone` and lets the animation finish in the background.
|
|
16
22
|
* @param callback - The DOM update callback to execute
|
|
17
|
-
* @returns Promise that resolves when the
|
|
23
|
+
* @returns Promise that resolves when the DOM update has committed
|
|
18
24
|
*/
|
|
19
25
|
async transition(callback) {
|
|
20
26
|
if (!this.enabled || !this.isSupported()) {
|
|
@@ -26,11 +32,10 @@ class ViewTransitionManager {
|
|
|
26
32
|
await callback();
|
|
27
33
|
applyViewTransitionNames();
|
|
28
34
|
});
|
|
29
|
-
|
|
30
|
-
await transition.finished;
|
|
31
|
-
} finally {
|
|
35
|
+
void transition.finished.finally(() => {
|
|
32
36
|
clearViewTransitionNames();
|
|
33
|
-
}
|
|
37
|
+
});
|
|
38
|
+
await transition.updateCallbackDone;
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
export {
|
package/src/client/types.d.ts
CHANGED
|
@@ -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' */
|
|
@@ -67,6 +74,9 @@ export interface EcoRouterOptions {
|
|
|
67
74
|
*/
|
|
68
75
|
prefetch?: PrefetchConfig | false;
|
|
69
76
|
}
|
|
77
|
+
export type BrowserRouterNavigateOptions = {
|
|
78
|
+
direction?: 'forward' | 'back' | 'replace';
|
|
79
|
+
};
|
|
70
80
|
/** Events emitted during the navigation lifecycle */
|
|
71
81
|
export interface EcoNavigationEvent {
|
|
72
82
|
url: URL;
|
|
@@ -80,5 +90,7 @@ export interface EcoBeforeSwapEvent extends EcoNavigationEvent {
|
|
|
80
90
|
/** Event fired after the DOM swap completes */
|
|
81
91
|
export interface EcoAfterSwapEvent extends EcoNavigationEvent {
|
|
82
92
|
}
|
|
93
|
+
/** Default document-level `<html>` attributes synchronized during navigation swaps. */
|
|
94
|
+
export declare const DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC: string[];
|
|
83
95
|
/** Default configuration options */
|
|
84
96
|
export declare const DEFAULT_OPTIONS: Required<EcoRouterOptions>;
|
package/src/client/types.js
CHANGED
|
@@ -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
|
};
|
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/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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|