@ecopages/react-router 0.2.0-alpha.10 → 0.2.0-alpha.12
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 +2 -4
- package/package.json +3 -3
- package/src/head-morpher.d.ts +6 -2
- package/src/head-morpher.js +67 -7
- package/src/router.js +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,10 +8,8 @@ All notable changes to `@ecopages/react-router` are documented here.
|
|
|
8
8
|
|
|
9
9
|
### Bug Fixes
|
|
10
10
|
|
|
11
|
-
- Fixed React-to-
|
|
12
|
-
-
|
|
13
|
-
- Standardized React route payload reads on `window.__ECO_PAGES__.page` and explicit document owner markers so mixed-router page ownership stays stable.
|
|
14
|
-
- Restored current-page HMR refreshes with persist layouts enabled by targeting the active React-router owner.
|
|
11
|
+
- Fixed React-to-browser-router handoffs, queued-click replay, and stale-navigation races during mixed-router navigations.
|
|
12
|
+
- Standardized route payload reads, document-owner markers, rerun scripts, and current-page HMR refreshes for persisted React layouts.
|
|
15
13
|
|
|
16
14
|
### Refactoring
|
|
17
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/react-router",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.12",
|
|
4
4
|
"description": "Client-side SPA router for EcoPages React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"directory": "packages/react-router"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@ecopages/core": "0.2.0-alpha.
|
|
36
|
-
"@ecopages/react": "0.2.0-alpha.
|
|
35
|
+
"@ecopages/core": "0.2.0-alpha.12",
|
|
36
|
+
"@ecopages/react": "0.2.0-alpha.12"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"react": "^19",
|
package/src/head-morpher.d.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Intelligently syncs head elements between pages using key-based diffing.
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
+
export type HeadMorphResult = {
|
|
7
|
+
cleanup: () => void;
|
|
8
|
+
flushRerunScripts: () => void;
|
|
9
|
+
};
|
|
6
10
|
/**
|
|
7
11
|
* Morphs the current document head to match the new document's head.
|
|
8
12
|
* Now splits the process into adding new elements and returning a cleanup function
|
|
@@ -10,6 +14,6 @@
|
|
|
10
14
|
* don't disappear before the "old" snapshot is taken.
|
|
11
15
|
*
|
|
12
16
|
* @param newDocument - The parsed document from the navigation target
|
|
13
|
-
* @returns Promise that resolves to
|
|
17
|
+
* @returns Promise that resolves to cleanup and rerun hooks when new stylesheets have loaded
|
|
14
18
|
*/
|
|
15
|
-
export declare function morphHead(newDocument: Document): Promise<
|
|
19
|
+
export declare function morphHead(newDocument: Document): Promise<HeadMorphResult>;
|
package/src/head-morpher.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const PRESERVE_SELECTORS = ['script[type="importmap"]', "meta[charset]", "[data-eco-persist]"];
|
|
2
|
+
const RERUN_SRC_ATTR = "data-eco-rerun-src";
|
|
3
|
+
let rerunNonce = 0;
|
|
2
4
|
function isNonExecutableHeadScript(el) {
|
|
3
5
|
if (el.tagName !== "SCRIPT") {
|
|
4
6
|
return false;
|
|
@@ -31,6 +33,13 @@ function shouldPersistExecutableInlineHeadScript(el) {
|
|
|
31
33
|
}
|
|
32
34
|
return !isNonExecutableHeadScript(el);
|
|
33
35
|
}
|
|
36
|
+
function isRerunScript(el) {
|
|
37
|
+
return el.tagName === "SCRIPT" && el.hasAttribute("data-eco-rerun");
|
|
38
|
+
}
|
|
39
|
+
function isHydrationScript(el) {
|
|
40
|
+
const src = el.getAttribute("src");
|
|
41
|
+
return !!src && src.includes("hydration.js") && src.includes("ecopages-react");
|
|
42
|
+
}
|
|
34
43
|
function getHeadElementKey(el) {
|
|
35
44
|
const tag = el.tagName.toLowerCase();
|
|
36
45
|
switch (tag) {
|
|
@@ -52,7 +61,7 @@ function getHeadElementKey(el) {
|
|
|
52
61
|
if (el.getAttribute("type") === "importmap") return "importmap";
|
|
53
62
|
const scriptId = el.getAttribute("data-eco-script-id") || el.getAttribute("id");
|
|
54
63
|
if (scriptId) return `script-id:${scriptId}`;
|
|
55
|
-
const src = el.src;
|
|
64
|
+
const src = el.getAttribute(RERUN_SRC_ATTR) || el.src;
|
|
56
65
|
return src ? `script:${src}` : null;
|
|
57
66
|
}
|
|
58
67
|
case "style": {
|
|
@@ -70,6 +79,12 @@ async function morphHead(newDocument) {
|
|
|
70
79
|
const newElements = /* @__PURE__ */ new Map();
|
|
71
80
|
const stylesheetPromises = [];
|
|
72
81
|
const elementsToRemove = [];
|
|
82
|
+
const pendingRerunScripts = Array.from(newHead.querySelectorAll("script[data-eco-rerun]")).filter((script) => !isHydrationScript(script)).map((script) => ({
|
|
83
|
+
attributes: Array.from(script.attributes).map((attr) => [attr.name, attr.value]),
|
|
84
|
+
textContent: script.textContent ?? "",
|
|
85
|
+
scriptId: script.getAttribute("data-eco-script-id"),
|
|
86
|
+
src: script.getAttribute("src")
|
|
87
|
+
}));
|
|
73
88
|
for (const el of Array.from(currentHead.children)) {
|
|
74
89
|
const key = getHeadElementKey(el);
|
|
75
90
|
if (key) currentElements.set(key, el);
|
|
@@ -80,9 +95,11 @@ async function morphHead(newDocument) {
|
|
|
80
95
|
}
|
|
81
96
|
for (const [key, newEl] of newElements) {
|
|
82
97
|
const currentEl = currentElements.get(key);
|
|
98
|
+
if (isRerunScript(newEl)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
83
101
|
if (!currentEl) {
|
|
84
|
-
|
|
85
|
-
if (newEl.tagName === "SCRIPT" && src && src.includes("hydration.js") && src.includes("ecopages-react")) {
|
|
102
|
+
if (newEl.tagName === "SCRIPT" && isHydrationScript(newEl)) {
|
|
86
103
|
continue;
|
|
87
104
|
}
|
|
88
105
|
const cloned = newEl.cloneNode(true);
|
|
@@ -104,7 +121,7 @@ async function morphHead(newDocument) {
|
|
|
104
121
|
}
|
|
105
122
|
for (const newEl of Array.from(newHead.children)) {
|
|
106
123
|
const key = getHeadElementKey(newEl);
|
|
107
|
-
if (!key) {
|
|
124
|
+
if (!key && !isRerunScript(newEl)) {
|
|
108
125
|
currentHead.appendChild(newEl.cloneNode(true));
|
|
109
126
|
}
|
|
110
127
|
}
|
|
@@ -119,12 +136,55 @@ async function morphHead(newDocument) {
|
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
el
|
|
139
|
+
return {
|
|
140
|
+
cleanup: () => {
|
|
141
|
+
for (const el of elementsToRemove) {
|
|
142
|
+
el.remove();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
flushRerunScripts: () => {
|
|
146
|
+
for (const script of pendingRerunScripts) {
|
|
147
|
+
const replacement = document.createElement("script");
|
|
148
|
+
const shouldBustModuleSrc = isExternalModuleRerunScript(script);
|
|
149
|
+
for (const [name, value] of script.attributes) {
|
|
150
|
+
if (name === "src" && shouldBustModuleSrc) {
|
|
151
|
+
replacement.setAttribute(RERUN_SRC_ATTR, value);
|
|
152
|
+
replacement.setAttribute("src", createRerunScriptUrl(value));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
replacement.setAttribute(name, value);
|
|
156
|
+
}
|
|
157
|
+
replacement.textContent = script.textContent;
|
|
158
|
+
const existingScript = findExistingRerunScript(script);
|
|
159
|
+
if (existingScript) {
|
|
160
|
+
existingScript.replaceWith(replacement);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
document.head.appendChild(replacement);
|
|
164
|
+
}
|
|
125
165
|
}
|
|
126
166
|
};
|
|
127
167
|
}
|
|
168
|
+
function findExistingRerunScript(script) {
|
|
169
|
+
const scripts = Array.from(document.head.querySelectorAll("script"));
|
|
170
|
+
if (script.scriptId) {
|
|
171
|
+
return scripts.find((candidate) => candidate.getAttribute("data-eco-script-id") === script.scriptId) ?? null;
|
|
172
|
+
}
|
|
173
|
+
return scripts.find(
|
|
174
|
+
(candidate) => (candidate.getAttribute(RERUN_SRC_ATTR) ?? candidate.getAttribute("src")) === script.src && (candidate.textContent ?? "") === script.textContent
|
|
175
|
+
) ?? null;
|
|
176
|
+
}
|
|
177
|
+
function isExternalModuleRerunScript(script) {
|
|
178
|
+
if (!script.src) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return script.attributes.some(([name, value]) => name === "type" && value === "module");
|
|
182
|
+
}
|
|
183
|
+
function createRerunScriptUrl(src) {
|
|
184
|
+
const url = new URL(src, document.baseURI);
|
|
185
|
+
url.searchParams.set("__eco_rerun", String(++rerunNonce));
|
|
186
|
+
return url.toString();
|
|
187
|
+
}
|
|
128
188
|
export {
|
|
129
189
|
morphHead
|
|
130
190
|
};
|
package/src/router.js
CHANGED
|
@@ -217,7 +217,7 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
217
217
|
if (result) {
|
|
218
218
|
const { Component, props, doc, finalPath, moduleUrl } = result;
|
|
219
219
|
const nextPage = { Component, props };
|
|
220
|
-
const cleanupHead = await morphHead(doc);
|
|
220
|
+
const { cleanup: cleanupHead, flushRerunScripts } = await morphHead(doc);
|
|
221
221
|
if (isStale()) {
|
|
222
222
|
cleanupHead();
|
|
223
223
|
return;
|
|
@@ -257,6 +257,7 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
257
257
|
if (isStale()) {
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
|
+
flushRerunScripts();
|
|
260
261
|
cleanupHead();
|
|
261
262
|
applyViewTransitionNames();
|
|
262
263
|
} finally {
|
|
@@ -266,8 +267,21 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
266
267
|
});
|
|
267
268
|
await navigationCommitPromise;
|
|
268
269
|
} else {
|
|
270
|
+
pendingRenderRef.current?.resolve();
|
|
271
|
+
const renderDfd = createDeferred();
|
|
272
|
+
pendingRenderRef.current = {
|
|
273
|
+
navigationId,
|
|
274
|
+
page: nextPage,
|
|
275
|
+
resolve: renderDfd.resolve
|
|
276
|
+
};
|
|
269
277
|
commitPageData(moduleUrl, props);
|
|
270
278
|
setCurrentPage(nextPage);
|
|
279
|
+
await renderDfd.promise;
|
|
280
|
+
if (isStale()) {
|
|
281
|
+
cleanupHead();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
flushRerunScripts();
|
|
271
285
|
cleanupHead();
|
|
272
286
|
applyViewTransitionNames();
|
|
273
287
|
}
|