dinou 4.0.12 → 4.0.13
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 +17 -0
- package/dinou/core/client-webpack.jsx +17 -3
- package/dinou/core/client.jsx +17 -3
- package/dinou/core/link.jsx +5 -4
- package/dinou/core/navigation-utils.js +4 -2
- package/dinou/core/render-app-to-html.js +29 -7
- package/dinou/core/server-function-proxy-webpack.js +8 -4
- package/dinou/core/server-function-proxy.js +8 -4
- package/dinou/core/server.js +51 -11
- package/dinou/core/url-resolver.js +13 -0
- package/dinou/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [4.0.13] - 2026-06-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Universal Redirects**: Full support for redirects on Server Components, `getProps`, Server Actions, and client SPA navigation.
|
|
12
|
+
- **Standard Relative Paths**: Support for relative paths (`../sibling`, `./child`, `?query`, `#hash`) matching directory-first URL resolution.
|
|
13
|
+
- **Absolute Link Hover**: Links now render resolved absolute paths to ensure correct browser previews and right-click behaviors.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **RSC Stream Stability**: Resolved socket crashes (`Connection closed`) and duplicate Express header warnings when redirects occur during streamed RSC requests.
|
|
17
|
+
- **SSR Process Leaks**: Render processes are now cleaned up immediately after an HTML redirect is triggered.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
- **Redirect Sanitization**: Blocked unsafe external domains (`https://...`), protocol-relative URLs (`//...`), and scripting payloads (`javascript:...`).
|
|
21
|
+
|
|
22
|
+
### Refactored
|
|
23
|
+
- **Shared Helpers**: Unified path resolution logic into a shared module (`url-resolver.js`).
|
|
24
|
+
|
|
8
25
|
## [4.0.12] - 2026-06-28
|
|
9
26
|
|
|
10
27
|
### Security
|
|
@@ -38,7 +38,7 @@ const isHashChangeOnly = (finalPath) => {
|
|
|
38
38
|
);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
const getRSCPayload = (rscKey) => {
|
|
41
|
+
const getRSCPayload = (rscKey, isPrefetch = false) => {
|
|
42
42
|
const url = rscKey.split("::")[0];
|
|
43
43
|
// 1. Check Idempotence (Avoids the infinite loop of React)
|
|
44
44
|
if (cache.has(url)) {
|
|
@@ -64,7 +64,21 @@ const getRSCPayload = (rscKey) => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// 3. Fetch and Cache Storage
|
|
67
|
-
const promise = createFromFetch(
|
|
67
|
+
const promise = createFromFetch(
|
|
68
|
+
fetch(payloadUrl).then((res) => {
|
|
69
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
70
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
71
|
+
if (!isPrefetch) {
|
|
72
|
+
window.location.href = redirectUrl;
|
|
73
|
+
} else {
|
|
74
|
+
cache.delete(url);
|
|
75
|
+
}
|
|
76
|
+
// Return a promise that never resolves to avoid React Server DOM throwing "Connection closed"
|
|
77
|
+
return new Promise(() => {});
|
|
78
|
+
}
|
|
79
|
+
return res;
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
68
82
|
cache.set(url, promise); // <--- KEY TO AVOID LOOP
|
|
69
83
|
return promise;
|
|
70
84
|
};
|
|
@@ -84,7 +98,7 @@ function Router() {
|
|
|
84
98
|
window.__DINOU_PREFETCH__ = (url) => {
|
|
85
99
|
// 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
|
|
86
100
|
if (isHashChangeOnly(url)) return;
|
|
87
|
-
getRSCPayload(url);
|
|
101
|
+
getRSCPayload(url, true);
|
|
88
102
|
};
|
|
89
103
|
|
|
90
104
|
// Hydration
|
package/dinou/core/client.jsx
CHANGED
|
@@ -39,7 +39,7 @@ const isHashChangeOnly = (finalPath) => {
|
|
|
39
39
|
);
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const getRSCPayload = (rscKey) => {
|
|
42
|
+
const getRSCPayload = (rscKey, isPrefetch = false) => {
|
|
43
43
|
const url = rscKey.split("::")[0];
|
|
44
44
|
// 1. Check Idempotence (Avoids the infinite loop of React)
|
|
45
45
|
if (cache.has(url)) {
|
|
@@ -65,7 +65,21 @@ const getRSCPayload = (rscKey) => {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 3. Fetch and Cache Storage
|
|
68
|
-
const promise = createFromFetch(
|
|
68
|
+
const promise = createFromFetch(
|
|
69
|
+
fetch(payloadUrl).then((res) => {
|
|
70
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
71
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
72
|
+
if (!isPrefetch) {
|
|
73
|
+
window.location.href = redirectUrl;
|
|
74
|
+
} else {
|
|
75
|
+
cache.delete(url);
|
|
76
|
+
}
|
|
77
|
+
// Return a promise that never resolves to avoid React Server DOM throwing "Connection closed"
|
|
78
|
+
return new Promise(() => {});
|
|
79
|
+
}
|
|
80
|
+
return res;
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
69
83
|
cache.set(url, promise); // <--- KEY TO AVOID LOOP
|
|
70
84
|
return promise;
|
|
71
85
|
};
|
|
@@ -85,7 +99,7 @@ function Router() {
|
|
|
85
99
|
window.__DINOU_PREFETCH__ = (url) => {
|
|
86
100
|
// 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
|
|
87
101
|
if (isHashChangeOnly(url)) return;
|
|
88
|
-
getRSCPayload(url);
|
|
102
|
+
getRSCPayload(url, true);
|
|
89
103
|
};
|
|
90
104
|
|
|
91
105
|
// Hydration
|
package/dinou/core/link.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useRouter } from "./navigation.js";
|
|
3
|
+
import { useRouter, usePathname } from "./navigation.js";
|
|
4
4
|
import { resolveUrl } from "./navigation-utils.js";
|
|
5
5
|
|
|
6
6
|
export function Link({
|
|
@@ -11,12 +11,13 @@ export function Link({
|
|
|
11
11
|
...props
|
|
12
12
|
}) {
|
|
13
13
|
const { push } = useRouter();
|
|
14
|
+
const pathname = usePathname();
|
|
15
|
+
const resolvedHref = resolveUrl(href, pathname);
|
|
14
16
|
|
|
15
17
|
const handlePrefetch = () => {
|
|
16
18
|
if (!prefetch || !href || fresh) return;
|
|
17
|
-
const finalPath = resolveUrl(href, window.location.pathname);
|
|
18
19
|
if (window.__DINOU_PREFETCH__) {
|
|
19
|
-
window.__DINOU_PREFETCH__(
|
|
20
|
+
window.__DINOU_PREFETCH__(resolvedHref);
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
|
|
@@ -29,7 +30,7 @@ export function Link({
|
|
|
29
30
|
|
|
30
31
|
return (
|
|
31
32
|
<a
|
|
32
|
-
href={
|
|
33
|
+
href={resolvedHref}
|
|
33
34
|
onClick={handleClick}
|
|
34
35
|
onMouseEnter={handlePrefetch}
|
|
35
36
|
{...props}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export function resolveUrl(href, currentPathname) {
|
|
2
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
3
|
+
|
|
2
4
|
if (href.startsWith("/") || href.includes("://")) {
|
|
3
|
-
const url = new URL(href,
|
|
5
|
+
const url = new URL(href, origin);
|
|
4
6
|
return normalize(url.pathname + url.search + url.hash);
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
let base = currentPathname;
|
|
8
10
|
if (!base.endsWith("/")) base += "/";
|
|
9
11
|
|
|
10
|
-
const resolved = new URL(href,
|
|
12
|
+
const resolved = new URL(href, origin + base);
|
|
11
13
|
return normalize(resolved.pathname + resolved.search + resolved.hash);
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -27,6 +27,8 @@ const childExecArgv = ESSENTIAL_NODE_ARGS.concat(loaderArg);
|
|
|
27
27
|
|
|
28
28
|
// ----------------------------------------------------
|
|
29
29
|
|
|
30
|
+
const { resolveRelativeUrl } = require("./url-resolver");
|
|
31
|
+
|
|
30
32
|
function renderAppToHtml(
|
|
31
33
|
reqPath,
|
|
32
34
|
paramsString,
|
|
@@ -82,13 +84,28 @@ function renderAppToHtml(
|
|
|
82
84
|
if (res.headersSent) {
|
|
83
85
|
// --- REDIRECT (JS Injection) ---
|
|
84
86
|
if (command === "redirect") {
|
|
85
|
-
const
|
|
87
|
+
const rawUrl = args.length === 1 ? args[0] : args[1];
|
|
88
|
+
const resolvedUrl = resolveRelativeUrl(rawUrl, reqPath);
|
|
86
89
|
console.log(
|
|
87
|
-
`[Dinou] Streaming active. Redirecting via JavaScript to: ${
|
|
90
|
+
`[Dinou] Streaming active. Redirecting via JavaScript to: ${resolvedUrl}`,
|
|
88
91
|
);
|
|
89
|
-
|
|
92
|
+
let finalUrl = "/";
|
|
93
|
+
if (
|
|
94
|
+
typeof resolvedUrl === "string" &&
|
|
95
|
+
resolvedUrl.startsWith("/") &&
|
|
96
|
+
!resolvedUrl.startsWith("//")
|
|
97
|
+
) {
|
|
98
|
+
finalUrl = resolvedUrl;
|
|
99
|
+
} else {
|
|
100
|
+
console.warn(
|
|
101
|
+
`[Dinou Security] Blocked unsafe streaming redirect to: ${rawUrl}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const safeUrl = JSON.stringify(finalUrl);
|
|
90
105
|
res.write(`<script>window.location.href = ${safeUrl};</script>`);
|
|
91
106
|
res.end();
|
|
107
|
+
child.stdout.unpipe(res);
|
|
108
|
+
child.kill();
|
|
92
109
|
return;
|
|
93
110
|
}
|
|
94
111
|
|
|
@@ -186,16 +203,19 @@ function renderAppToHtml(
|
|
|
186
203
|
rawUrl = args[0];
|
|
187
204
|
}
|
|
188
205
|
|
|
206
|
+
// Resolve relative URL relative to reqPath
|
|
207
|
+
const resolvedUrl = resolveRelativeUrl(rawUrl, reqPath);
|
|
208
|
+
|
|
189
209
|
// 2. Security Logic (Safe Redirect)
|
|
190
210
|
// We allow only relative routes starting with '/' but not '//'
|
|
191
211
|
let finalUrl = "/";
|
|
192
212
|
|
|
193
213
|
if (
|
|
194
|
-
typeof
|
|
195
|
-
|
|
196
|
-
!
|
|
214
|
+
typeof resolvedUrl === "string" &&
|
|
215
|
+
resolvedUrl.startsWith("/") &&
|
|
216
|
+
!resolvedUrl.startsWith("//")
|
|
197
217
|
) {
|
|
198
|
-
finalUrl =
|
|
218
|
+
finalUrl = resolvedUrl;
|
|
199
219
|
} else {
|
|
200
220
|
console.warn(
|
|
201
221
|
`[Dinou Security] Blocked unsafe redirect to: ${rawUrl}`,
|
|
@@ -205,6 +225,8 @@ function renderAppToHtml(
|
|
|
205
225
|
|
|
206
226
|
// 3. Execute safe redirect always using (status, url)
|
|
207
227
|
res.redirect.apply(res, [status, finalUrl]);
|
|
228
|
+
child.stdout.unpipe(res);
|
|
229
|
+
child.kill();
|
|
208
230
|
return;
|
|
209
231
|
}
|
|
210
232
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createFromFetch } from "react-server-dom-webpack/client";
|
|
2
2
|
|
|
3
|
+
function isSafeRedirect(url) {
|
|
4
|
+
return typeof url === "string" && url.startsWith("/") && !url.startsWith("//");
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
function createServerFunctionProxy(id) {
|
|
4
8
|
return new Proxy(() => { }, {
|
|
5
9
|
apply: async (_target, _thisArg, args) => {
|
|
@@ -33,7 +37,7 @@ function createServerFunctionProxy(id) {
|
|
|
33
37
|
// Check header first
|
|
34
38
|
const redirectUrl = res.headers.get("X-Dinou-Redirect");
|
|
35
39
|
if (redirectUrl) {
|
|
36
|
-
window.location.href = redirectUrl;
|
|
40
|
+
window.location.href = isSafeRedirect(redirectUrl) ? redirectUrl : "/";
|
|
37
41
|
return new Promise(() => { });
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -50,7 +54,7 @@ function createServerFunctionProxy(id) {
|
|
|
50
54
|
if (contentType.includes("application/json")) {
|
|
51
55
|
const data = await res.json();
|
|
52
56
|
if (data && data.redirect) {
|
|
53
|
-
window.location.href = data.redirect;
|
|
57
|
+
window.location.href = isSafeRedirect(data.redirect) ? data.redirect : "/";
|
|
54
58
|
return new Promise(() => { });
|
|
55
59
|
}
|
|
56
60
|
return data;
|
|
@@ -78,7 +82,7 @@ function createServerFunctionProxy(id) {
|
|
|
78
82
|
const payload = JSON.parse(buffer.slice(2));
|
|
79
83
|
if (payload.type === "redirect") {
|
|
80
84
|
isRedirecting = true;
|
|
81
|
-
window.location.href = payload.url;
|
|
85
|
+
window.location.href = isSafeRedirect(payload.url) ? payload.url : "/";
|
|
82
86
|
} else if (payload.type === "cookie") {
|
|
83
87
|
document.cookie = payload.cookie;
|
|
84
88
|
}
|
|
@@ -115,7 +119,7 @@ function createServerFunctionProxy(id) {
|
|
|
115
119
|
const payload = JSON.parse(line.slice(2));
|
|
116
120
|
if (payload.type === "redirect") {
|
|
117
121
|
isRedirecting = true;
|
|
118
|
-
window.location.href = payload.url;
|
|
122
|
+
window.location.href = isSafeRedirect(payload.url) ? payload.url : "/";
|
|
119
123
|
} else if (payload.type === "cookie") {
|
|
120
124
|
document.cookie = payload.cookie;
|
|
121
125
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// public/server-function-proxy.js
|
|
2
2
|
import { createFromFetch } from "@roggc/react-server-dom-esm/client";
|
|
3
3
|
|
|
4
|
+
function isSafeRedirect(url) {
|
|
5
|
+
return typeof url === "string" && url.startsWith("/") && !url.startsWith("//");
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export function createServerFunctionProxy(id) {
|
|
5
9
|
return new Proxy(() => {}, {
|
|
6
10
|
apply: async (_target, _thisArg, args) => {
|
|
@@ -34,7 +38,7 @@ export function createServerFunctionProxy(id) {
|
|
|
34
38
|
// Check header first
|
|
35
39
|
const redirectUrl = res.headers.get("X-Dinou-Redirect");
|
|
36
40
|
if (redirectUrl) {
|
|
37
|
-
window.location.href = redirectUrl;
|
|
41
|
+
window.location.href = isSafeRedirect(redirectUrl) ? redirectUrl : "/";
|
|
38
42
|
return new Promise(() => {});
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -51,7 +55,7 @@ export function createServerFunctionProxy(id) {
|
|
|
51
55
|
if (contentType.includes("application/json")) {
|
|
52
56
|
const data = await res.json();
|
|
53
57
|
if (data && data.redirect) {
|
|
54
|
-
window.location.href = data.redirect;
|
|
58
|
+
window.location.href = isSafeRedirect(data.redirect) ? data.redirect : "/";
|
|
55
59
|
return new Promise(() => {});
|
|
56
60
|
}
|
|
57
61
|
return data;
|
|
@@ -79,7 +83,7 @@ export function createServerFunctionProxy(id) {
|
|
|
79
83
|
const payload = JSON.parse(buffer.slice(2));
|
|
80
84
|
if (payload.type === "redirect") {
|
|
81
85
|
isRedirecting = true;
|
|
82
|
-
window.location.href = payload.url;
|
|
86
|
+
window.location.href = isSafeRedirect(payload.url) ? payload.url : "/";
|
|
83
87
|
} else if (payload.type === "cookie") {
|
|
84
88
|
document.cookie = payload.cookie;
|
|
85
89
|
}
|
|
@@ -116,7 +120,7 @@ export function createServerFunctionProxy(id) {
|
|
|
116
120
|
const payload = JSON.parse(line.slice(2));
|
|
117
121
|
if (payload.type === "redirect") {
|
|
118
122
|
isRedirecting = true;
|
|
119
|
-
window.location.href = payload.url;
|
|
123
|
+
window.location.href = isSafeRedirect(payload.url) ? payload.url : "/";
|
|
120
124
|
} else if (payload.type === "cookie") {
|
|
121
125
|
document.cookie = payload.cookie;
|
|
122
126
|
}
|
package/dinou/core/server.js
CHANGED
|
@@ -248,20 +248,24 @@ const appUseCookieParser = cookieParser();
|
|
|
248
248
|
const app = express();
|
|
249
249
|
app.use(appUseCookieParser);
|
|
250
250
|
app.use(express.json());
|
|
251
|
+
const { resolveRelativeUrl } = require("./url-resolver");
|
|
251
252
|
|
|
252
253
|
function getContext(req, res) {
|
|
254
|
+
let hasRedirected = false;
|
|
253
255
|
// Helper to execute res methods safely
|
|
254
256
|
const safeResCall = (methodName, ...args) => {
|
|
257
|
+
if (hasRedirected) return;
|
|
255
258
|
if (res.headersSent) {
|
|
259
|
+
if (methodName === "redirect" && req.path.includes("____rsc_payload")) {
|
|
260
|
+
return; // Silence streaming redirects during RSC payload requests
|
|
261
|
+
}
|
|
256
262
|
console.log(
|
|
257
263
|
`[Dinou] res.${methodName} called but headers already sent. Ignoring.`,
|
|
258
264
|
);
|
|
259
|
-
// console.warn(
|
|
260
|
-
// `[Dinou Warning] RSC Stream active. Ignoring res.${methodName}() to avoid crash.`
|
|
261
|
-
// );
|
|
262
265
|
return; // Exit silently
|
|
263
266
|
}
|
|
264
267
|
if (methodName === "redirect") {
|
|
268
|
+
hasRedirected = true;
|
|
265
269
|
// 1. Normalize arguments (Status and URL)
|
|
266
270
|
let url = args[0];
|
|
267
271
|
let status = 302; // Default
|
|
@@ -272,16 +276,27 @@ function getContext(req, res) {
|
|
|
272
276
|
}
|
|
273
277
|
|
|
274
278
|
function safeRedirect(targetUrl) {
|
|
275
|
-
|
|
279
|
+
const resolvedUrl = resolveRelativeUrl(targetUrl, req.path);
|
|
280
|
+
let finalUrl = "/";
|
|
276
281
|
if (
|
|
277
|
-
typeof
|
|
278
|
-
|
|
279
|
-
!
|
|
282
|
+
typeof resolvedUrl === "string" &&
|
|
283
|
+
resolvedUrl.startsWith("/") &&
|
|
284
|
+
!resolvedUrl.startsWith("//")
|
|
280
285
|
) {
|
|
281
|
-
|
|
286
|
+
finalUrl = resolvedUrl;
|
|
282
287
|
} else {
|
|
283
|
-
|
|
288
|
+
console.warn(
|
|
289
|
+
`[Dinou Security] Blocked unsafe redirect to: ${targetUrl}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (req.path.includes("____rsc_payload")) {
|
|
294
|
+
res.setHeader("x-rsc-redirect", finalUrl);
|
|
295
|
+
res.status(200).end();
|
|
296
|
+
return;
|
|
284
297
|
}
|
|
298
|
+
|
|
299
|
+
res.redirect.apply(res, [status, finalUrl]);
|
|
285
300
|
}
|
|
286
301
|
return safeRedirect(url);
|
|
287
302
|
}
|
|
@@ -351,11 +366,31 @@ function getContextForServerFunctionEndpoint(req, res) {
|
|
|
351
366
|
},
|
|
352
367
|
res: {
|
|
353
368
|
redirect: (urlOrStatus, url) => {
|
|
354
|
-
const
|
|
369
|
+
const rawUrl = url || urlOrStatus;
|
|
370
|
+
const referer = req.headers["referer"];
|
|
371
|
+
let refererPath = "/";
|
|
372
|
+
if (referer) {
|
|
373
|
+
try {
|
|
374
|
+
refererPath = new URL(referer).pathname;
|
|
375
|
+
} catch (e) {}
|
|
376
|
+
}
|
|
377
|
+
const resolvedUrl = resolveRelativeUrl(rawUrl, refererPath);
|
|
378
|
+
let finalUrl = "/";
|
|
379
|
+
if (
|
|
380
|
+
typeof resolvedUrl === "string" &&
|
|
381
|
+
resolvedUrl.startsWith("/") &&
|
|
382
|
+
!resolvedUrl.startsWith("//")
|
|
383
|
+
) {
|
|
384
|
+
finalUrl = resolvedUrl;
|
|
385
|
+
} else {
|
|
386
|
+
console.warn(
|
|
387
|
+
`[Dinou Security] Blocked unsafe server function redirect to: ${rawUrl}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
355
390
|
// We throw a special object that the endpoint will intercept
|
|
356
391
|
throw {
|
|
357
392
|
$$type: "dinou-internal-redirect",
|
|
358
|
-
url:
|
|
393
|
+
url: finalUrl,
|
|
359
394
|
};
|
|
360
395
|
},
|
|
361
396
|
status: (code) => {
|
|
@@ -530,6 +565,11 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
|
|
|
530
565
|
isNotFound,
|
|
531
566
|
isDevelopment,
|
|
532
567
|
);
|
|
568
|
+
|
|
569
|
+
if (res.headersSent) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
533
573
|
const manifest = isDevelopment
|
|
534
574
|
? JSON.parse(
|
|
535
575
|
readFileSync(
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function resolveRelativeUrl(href, currentPathname) {
|
|
2
|
+
if (href.startsWith("/") || href.includes("://")) {
|
|
3
|
+
return href;
|
|
4
|
+
}
|
|
5
|
+
let base = currentPathname || "/";
|
|
6
|
+
if (!base.endsWith("/")) {
|
|
7
|
+
base += "/";
|
|
8
|
+
}
|
|
9
|
+
const resolved = new URL(href, "http://localhost" + base);
|
|
10
|
+
return resolved.pathname + resolved.search + resolved.hash;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { resolveRelativeUrl };
|
package/dinou/package.json
CHANGED