@megabudino/stack-utils 1.0.0 → 1.2.0
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/dist/proxy/handle.js +65 -53
- package/dist/proxy/types.d.ts +12 -0
- package/package.json +2 -1
package/dist/proxy/handle.js
CHANGED
|
@@ -11,80 +11,92 @@ export function createProxyHandle(config) {
|
|
|
11
11
|
const sitemapCache = `public, max-age=${config.sitemapCacheSeconds ?? 3600}`;
|
|
12
12
|
const proxyAssets = config.proxyAssets ?? false;
|
|
13
13
|
const additionalOrigins = config.additionalOrigins ?? [];
|
|
14
|
+
const maxRedirects = config.maxInternalRedirects ?? 5;
|
|
15
|
+
const textReplacements = config.textReplacements ?? {};
|
|
16
|
+
const siteHost = new URL(siteUrl).hostname;
|
|
17
|
+
const originHosts = new Set([
|
|
18
|
+
new URL(originUrl).hostname,
|
|
19
|
+
...additionalOrigins.map((url) => new URL(url).hostname)
|
|
20
|
+
]);
|
|
14
21
|
return async ({ event, resolve }) => {
|
|
15
22
|
const { pathname, search } = event.url;
|
|
16
23
|
if (event.route.id) {
|
|
17
24
|
return resolve(event);
|
|
18
25
|
}
|
|
19
|
-
const upstream = new URL(pathname + search, originUrl);
|
|
20
26
|
const headers = new Headers(event.request.headers);
|
|
21
27
|
headers.delete('accept-encoding');
|
|
22
|
-
headers.
|
|
28
|
+
headers.set('host', siteHost);
|
|
23
29
|
headers.delete('content-length');
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
let currentPath = pathname + search;
|
|
31
|
+
for (let hops = 0;; hops++) {
|
|
32
|
+
const upstream = new URL(currentPath, originUrl);
|
|
33
|
+
let upstreamResponse;
|
|
34
|
+
try {
|
|
35
|
+
upstreamResponse = await fetch(upstream, {
|
|
36
|
+
method: event.request.method,
|
|
37
|
+
headers,
|
|
38
|
+
redirect: 'manual',
|
|
39
|
+
body: event.request.method === 'GET' || event.request.method === 'HEAD'
|
|
40
|
+
? undefined
|
|
41
|
+
: event.request.body
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return new Response('Bad Gateway', { status: 502 });
|
|
46
|
+
}
|
|
47
|
+
if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
|
|
48
|
+
const location = upstreamResponse.headers.get('location');
|
|
49
|
+
if (location) {
|
|
50
|
+
const nextUrl = new URL(location, upstream);
|
|
51
|
+
const isOrigin = originHosts.has(nextUrl.hostname);
|
|
52
|
+
const isSite = nextUrl.hostname === siteHost;
|
|
53
|
+
if ((isOrigin || isSite) && hops < maxRedirects) {
|
|
54
|
+
currentPath = nextUrl.pathname + nextUrl.search;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
43
57
|
return new Response(null, {
|
|
44
58
|
status: upstreamResponse.status,
|
|
45
59
|
headers: {
|
|
46
|
-
location: `${siteUrl}${nextUrl.pathname}${nextUrl.search}`
|
|
60
|
+
location: isOrigin ? `${siteUrl}${nextUrl.pathname}${nextUrl.search}` : location
|
|
47
61
|
}
|
|
48
62
|
});
|
|
49
63
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
}
|
|
65
|
+
const cleanHeaders = new Headers(upstreamResponse.headers);
|
|
66
|
+
cleanHeaders.delete('content-encoding');
|
|
67
|
+
cleanHeaders.delete('content-length');
|
|
68
|
+
cleanHeaders.delete('transfer-encoding');
|
|
69
|
+
const contentType = upstreamResponse.headers.get('content-type') ?? '';
|
|
70
|
+
if (contentType.includes('text/xml') || contentType.includes('application/xml')) {
|
|
71
|
+
let xml = await upstreamResponse.text();
|
|
72
|
+
xml = xml.replace(/<\?xml-stylesheet[^?]*\?>\s*/g, '');
|
|
73
|
+
if (sitemaps.length > 0 && xml.includes('</sitemapindex>')) {
|
|
74
|
+
xml = injectIntoSitemapIndex(xml, siteUrl, sitemaps);
|
|
55
75
|
}
|
|
76
|
+
cleanHeaders.set('cache-control', sitemapCache);
|
|
77
|
+
return new Response(xml, {
|
|
78
|
+
status: upstreamResponse.status,
|
|
79
|
+
statusText: upstreamResponse.statusText,
|
|
80
|
+
headers: cleanHeaders
|
|
81
|
+
});
|
|
56
82
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
if (contentType.includes('text/html')) {
|
|
84
|
+
const html = await upstreamResponse.text();
|
|
85
|
+
let result = rewriteHtml(html, originUrl, siteUrl, proxyAssets, additionalOrigins);
|
|
86
|
+
for (const [search, replace] of Object.entries(textReplacements)) {
|
|
87
|
+
result = result.replaceAll(search, replace);
|
|
88
|
+
}
|
|
89
|
+
return new Response(result, {
|
|
90
|
+
status: upstreamResponse.status,
|
|
91
|
+
statusText: upstreamResponse.statusText,
|
|
92
|
+
headers: cleanHeaders
|
|
93
|
+
});
|
|
68
94
|
}
|
|
69
|
-
|
|
70
|
-
return new Response(xml, {
|
|
71
|
-
status: upstreamResponse.status,
|
|
72
|
-
statusText: upstreamResponse.statusText,
|
|
73
|
-
headers: cleanHeaders
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
if (contentType.includes('text/html')) {
|
|
77
|
-
const html = await upstreamResponse.text();
|
|
78
|
-
return new Response(rewriteHtml(html, originUrl, siteUrl, proxyAssets, additionalOrigins), {
|
|
95
|
+
return new Response(upstreamResponse.body, {
|
|
79
96
|
status: upstreamResponse.status,
|
|
80
97
|
statusText: upstreamResponse.statusText,
|
|
81
98
|
headers: cleanHeaders
|
|
82
99
|
});
|
|
83
100
|
}
|
|
84
|
-
return new Response(upstreamResponse.body, {
|
|
85
|
-
status: upstreamResponse.status,
|
|
86
|
-
statusText: upstreamResponse.statusText,
|
|
87
|
-
headers: cleanHeaders
|
|
88
|
-
});
|
|
89
101
|
};
|
|
90
102
|
}
|
package/dist/proxy/types.d.ts
CHANGED
|
@@ -25,4 +25,16 @@ export interface ProxyConfig {
|
|
|
25
25
|
* rewritten the same way as `originUrl` (e.g. legacy/migration domains).
|
|
26
26
|
*/
|
|
27
27
|
additionalOrigins?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of internal redirects to follow when the origin responds
|
|
30
|
+
* with a location pointing back to the origin or public site host.
|
|
31
|
+
* Defaults to 5.
|
|
32
|
+
*/
|
|
33
|
+
maxInternalRedirects?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Key-value map of text replacements applied to HTML responses
|
|
36
|
+
* after all standard URL rewrites. Each key is replaced with its value
|
|
37
|
+
* using `String.prototype.replaceAll`.
|
|
38
|
+
*/
|
|
39
|
+
textReplacements?: Record<string, string>;
|
|
28
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@megabudino/stack-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Reusable utilities for a SvelteKit stack, including a reverse proxy and static image pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node ./scripts/build.mjs",
|
|
31
|
+
"test": "node --test proxy.test.mjs",
|
|
31
32
|
"typecheck": "tsc --noEmit"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|