@megabudino/stack-utils 2.0.2 → 2.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 +34 -11
- package/dist/proxy/rewrite.js +5 -0
- package/dist/proxy/types.d.ts +24 -1
- package/dist/proxy/upstream-fetch.d.ts +10 -0
- package/dist/proxy/upstream-fetch.js +62 -0
- package/dist/proxy/vite-plugin.d.ts +15 -1
- package/dist/proxy/vite-plugin.js +29 -8
- package/package.json +2 -1
package/dist/proxy/handle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateIslandsConfigAtStartup } from './islands.js';
|
|
2
2
|
import { injectIntoSitemapIndex, normalizeSitemapPath, removeExcludedUrlsFromSitemap, rewriteHtml } from './rewrite.js';
|
|
3
3
|
import { transformProxiedHtml } from './transform-html.js';
|
|
4
|
+
import { fetchViaTransport } from './upstream-fetch.js';
|
|
4
5
|
/**
|
|
5
6
|
* Create a SvelteKit `Handle` hook that reverse-proxies requests to an origin.
|
|
6
7
|
*
|
|
@@ -10,6 +11,11 @@ import { transformProxiedHtml } from './transform-html.js';
|
|
|
10
11
|
export function createProxyHandle(config) {
|
|
11
12
|
assertNoRemovedConfig(config);
|
|
12
13
|
const { originUrl, siteUrl } = config;
|
|
14
|
+
const logicalOrigin = new URL(originUrl);
|
|
15
|
+
const upstreamUrlTrimmed = config.upstreamUrl?.trim();
|
|
16
|
+
const useTransport = Boolean(upstreamUrlTrimmed);
|
|
17
|
+
const transport = new URL(upstreamUrlTrimmed || originUrl);
|
|
18
|
+
const forwardedProto = new URL(siteUrl).protocol.replace(':', '');
|
|
13
19
|
const sitemap = config.sitemap ?? {};
|
|
14
20
|
const sitemapIndexPath = sitemap.indexPath ? normalizeSitemapPath(sitemap.indexPath) : undefined;
|
|
15
21
|
const sitemapIncludeFiles = sitemap.includeFiles ?? [];
|
|
@@ -34,21 +40,38 @@ export function createProxyHandle(config) {
|
|
|
34
40
|
}
|
|
35
41
|
const headers = new Headers(event.request.headers);
|
|
36
42
|
headers.delete('accept-encoding');
|
|
37
|
-
|
|
43
|
+
if (useTransport) {
|
|
44
|
+
headers.set('host', logicalOrigin.host);
|
|
45
|
+
headers.set('x-forwarded-proto', forwardedProto);
|
|
46
|
+
if (!headers.has('x-forwarded-host')) {
|
|
47
|
+
headers.set('x-forwarded-host', siteHost);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
headers.set('host', siteHost);
|
|
52
|
+
}
|
|
38
53
|
headers.delete('content-length');
|
|
54
|
+
const connectionBase = useTransport ? transport.origin : originUrl;
|
|
55
|
+
const requestBody = event.request.method === 'GET' || event.request.method === 'HEAD'
|
|
56
|
+
? undefined
|
|
57
|
+
: event.request.body;
|
|
39
58
|
let currentPath = pathname + search;
|
|
40
59
|
for (let hops = 0;; hops++) {
|
|
41
|
-
const upstream = new URL(currentPath,
|
|
60
|
+
const upstream = new URL(currentPath, connectionBase);
|
|
42
61
|
let upstreamResponse;
|
|
43
62
|
try {
|
|
44
|
-
upstreamResponse =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
upstreamResponse = useTransport
|
|
64
|
+
? await fetchViaTransport(upstream, {
|
|
65
|
+
method: event.request.method,
|
|
66
|
+
headers,
|
|
67
|
+
body: requestBody
|
|
68
|
+
})
|
|
69
|
+
: await fetch(upstream, {
|
|
70
|
+
method: event.request.method,
|
|
71
|
+
headers,
|
|
72
|
+
redirect: 'manual',
|
|
73
|
+
body: requestBody
|
|
74
|
+
});
|
|
52
75
|
}
|
|
53
76
|
catch {
|
|
54
77
|
return new Response('Bad Gateway', { status: 502 });
|
|
@@ -56,7 +79,7 @@ export function createProxyHandle(config) {
|
|
|
56
79
|
if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
|
|
57
80
|
const location = upstreamResponse.headers.get('location');
|
|
58
81
|
if (location) {
|
|
59
|
-
const nextUrl = new URL(location, upstream);
|
|
82
|
+
const nextUrl = new URL(location, useTransport ? logicalOrigin : upstream);
|
|
60
83
|
const isOrigin = originHosts.has(nextUrl.hostname);
|
|
61
84
|
const isSite = nextUrl.hostname === siteHost;
|
|
62
85
|
if ((isOrigin || isSite) && hops < maxRedirects) {
|
package/dist/proxy/rewrite.js
CHANGED
|
@@ -39,6 +39,11 @@ export function rewriteHtml(html, originUrl, siteUrl, proxyAssets = false, addit
|
|
|
39
39
|
html = html.replace(originPattern(origin), '');
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
// SvelteKit origins emit relative `./_app/...` assets. Browsers resolve those to
|
|
43
|
+
// `/_app/...` on the proxy host, which collides with the local SvelteKit build.
|
|
44
|
+
// Point them at the origin so CSS/JS load from the upstream app (CORS permitting).
|
|
45
|
+
const originBase = originUrl.replace(/\/$/, '');
|
|
46
|
+
html = html.replace(/(["'(])\.\/_app\//g, `$1${originBase}/_app/`);
|
|
42
47
|
return html;
|
|
43
48
|
}
|
|
44
49
|
/** Normalize URL-like values to comparable sitemap pathnames. */
|
package/dist/proxy/types.d.ts
CHANGED
|
@@ -133,8 +133,31 @@ export interface SitemapConfig {
|
|
|
133
133
|
}
|
|
134
134
|
/** Configuration for the reverse proxy. */
|
|
135
135
|
export interface ProxyConfig {
|
|
136
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Logical origin URL: the host and scheme WordPress (or the upstream CMS) should
|
|
138
|
+
* believe it is serving. Used for the `Host` header when `upstreamUrl` is set,
|
|
139
|
+
* and as the source domain for response rewriting (`originUrl` → `siteUrl`).
|
|
140
|
+
*
|
|
141
|
+
* When `upstreamUrl` is omitted, this URL is also the connection target.
|
|
142
|
+
*/
|
|
137
143
|
originUrl: string;
|
|
144
|
+
/**
|
|
145
|
+
* Optional transport URL for the actual TCP/HTTP connection to the origin.
|
|
146
|
+
*
|
|
147
|
+
* Use this to reach an internal service name (e.g. a Docker overlay hostname)
|
|
148
|
+
* while still presenting the public `originUrl` identity to the CMS. When
|
|
149
|
+
* absent or blank, the proxy connects to `originUrl` and behavior is unchanged.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* createProxyHandle({
|
|
154
|
+
* originUrl: 'https://wordpress.example.com',
|
|
155
|
+
* upstreamUrl: process.env.WP_UPSTREAM, // e.g. http://wordpress-service:80
|
|
156
|
+
* siteUrl: 'https://www.example.com'
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
upstreamUrl?: string;
|
|
138
161
|
/** The public site URL (used in sitemaps and link rewriting). */
|
|
139
162
|
siteUrl: string;
|
|
140
163
|
/** Explicit sitemap rewriting configuration for proxied XML responses. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward a request to the origin using undici so a custom `Host` header is
|
|
3
|
+
* honored. Node's built-in `fetch` derives `Host` from the destination URL and
|
|
4
|
+
* ignores `headers.set('host', ...)`, which breaks transport/logical-origin splits.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fetchViaTransport(url: URL, init: {
|
|
7
|
+
method: string;
|
|
8
|
+
headers: Headers;
|
|
9
|
+
body?: BodyInit | null;
|
|
10
|
+
}): Promise<Response>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { request as undiciRequest } from 'undici';
|
|
2
|
+
/**
|
|
3
|
+
* Forward a request to the origin using undici so a custom `Host` header is
|
|
4
|
+
* honored. Node's built-in `fetch` derives `Host` from the destination URL and
|
|
5
|
+
* ignores `headers.set('host', ...)`, which breaks transport/logical-origin splits.
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchViaTransport(url, init) {
|
|
8
|
+
const { statusCode, headers, body } = await undiciRequest(url, {
|
|
9
|
+
method: init.method,
|
|
10
|
+
headers: headersRecord(init.headers),
|
|
11
|
+
body: init.method === 'GET' || init.method === 'HEAD'
|
|
12
|
+
? undefined
|
|
13
|
+
: init.body
|
|
14
|
+
});
|
|
15
|
+
const responseHeaders = new Headers();
|
|
16
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
for (const entry of value) {
|
|
22
|
+
responseHeaders.append(key, entry);
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
responseHeaders.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
return new Response(body, {
|
|
29
|
+
status: statusCode,
|
|
30
|
+
headers: responseHeaders
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Hop-by-hop and other headers undici rejects when forwarding a client request. */
|
|
34
|
+
const UNDICI_STRIPPED_HEADERS = new Set([
|
|
35
|
+
'connection',
|
|
36
|
+
'keep-alive',
|
|
37
|
+
'proxy-authenticate',
|
|
38
|
+
'proxy-authorization',
|
|
39
|
+
'proxy-connection',
|
|
40
|
+
'te',
|
|
41
|
+
'trailer',
|
|
42
|
+
'transfer-encoding',
|
|
43
|
+
'upgrade',
|
|
44
|
+
'http2-settings'
|
|
45
|
+
]);
|
|
46
|
+
function headersRecord(headers) {
|
|
47
|
+
const skip = new Set(UNDICI_STRIPPED_HEADERS);
|
|
48
|
+
const connection = headers.get('connection');
|
|
49
|
+
if (connection) {
|
|
50
|
+
for (const token of connection.split(',')) {
|
|
51
|
+
skip.add(token.trim().toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const record = {};
|
|
55
|
+
for (const [key, value] of headers.entries()) {
|
|
56
|
+
if (skip.has(key.toLowerCase())) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
record[key] = value;
|
|
60
|
+
}
|
|
61
|
+
return record;
|
|
62
|
+
}
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
1
|
+
import type { Plugin, Rollup } from 'vite';
|
|
2
2
|
export declare function proxyIslands(): Plugin;
|
|
3
|
+
/**
|
|
4
|
+
* Collect every client stylesheet that must load for the islands bootstrap to
|
|
5
|
+
* paint correctly.
|
|
6
|
+
*
|
|
7
|
+
* Vite/Rollup code-splitting frequently hoists an island component into a
|
|
8
|
+
* shared chunk (for example when the same component is also imported by a
|
|
9
|
+
* normal SvelteKit route). In that case the component's `importedCss` is
|
|
10
|
+
* attributed to the shared chunk, not to the bootstrap entry chunk, so reading
|
|
11
|
+
* only the entry chunk's own `importedCss` misses the styles. We therefore walk
|
|
12
|
+
* the chunk graph transitively from the entry chunk, following each chunk's
|
|
13
|
+
* STATIC `imports` (dynamic imports are not required for first paint and could
|
|
14
|
+
* over-inject unrelated CSS), unioning the `importedCss` we find along the way.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractClientStylesheetUrls(entryChunk: Rollup.OutputChunk, bundle: Rollup.OutputBundle, base: string): string[];
|
|
@@ -104,7 +104,7 @@ export function proxyIslands() {
|
|
|
104
104
|
throw new Error('[stack-utils proxy] failed to locate the generated islands client chunk.');
|
|
105
105
|
}
|
|
106
106
|
const clientEntryUrl = toPublicAssetUrl(clientEntryFileName, config.base);
|
|
107
|
-
const clientStylesheetUrls = extractClientStylesheetUrls(clientChunk, config.base);
|
|
107
|
+
const clientStylesheetUrls = extractClientStylesheetUrls(clientChunk, bundle, config.base);
|
|
108
108
|
await patchServerOutputFiles(config.root, clientEntryUrl, clientStylesheetUrls);
|
|
109
109
|
}
|
|
110
110
|
};
|
|
@@ -250,14 +250,35 @@ function findHooksServerFile(root) {
|
|
|
250
250
|
function toDevFsUrl(filePath) {
|
|
251
251
|
return `/@fs/${filePath.split(path.sep).join('/')}`;
|
|
252
252
|
}
|
|
253
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Collect every client stylesheet that must load for the islands bootstrap to
|
|
255
|
+
* paint correctly.
|
|
256
|
+
*
|
|
257
|
+
* Vite/Rollup code-splitting frequently hoists an island component into a
|
|
258
|
+
* shared chunk (for example when the same component is also imported by a
|
|
259
|
+
* normal SvelteKit route). In that case the component's `importedCss` is
|
|
260
|
+
* attributed to the shared chunk, not to the bootstrap entry chunk, so reading
|
|
261
|
+
* only the entry chunk's own `importedCss` misses the styles. We therefore walk
|
|
262
|
+
* the chunk graph transitively from the entry chunk, following each chunk's
|
|
263
|
+
* STATIC `imports` (dynamic imports are not required for first paint and could
|
|
264
|
+
* over-inject unrelated CSS), unioning the `importedCss` we find along the way.
|
|
265
|
+
*/
|
|
266
|
+
export function extractClientStylesheetUrls(entryChunk, bundle, base) {
|
|
254
267
|
const urls = new Set();
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
const visited = new Set();
|
|
269
|
+
const visit = (chunk) => {
|
|
270
|
+
if (!chunk || chunk.type !== 'chunk' || visited.has(chunk.fileName)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
visited.add(chunk.fileName);
|
|
274
|
+
for (const fileName of chunk.viteMetadata?.importedCss ?? []) {
|
|
275
|
+
urls.add(toPublicAssetUrl(fileName, base));
|
|
276
|
+
}
|
|
277
|
+
for (const importFileName of chunk.imports ?? []) {
|
|
278
|
+
visit(bundle[importFileName]);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
visit(entryChunk);
|
|
261
282
|
return [...urls];
|
|
262
283
|
}
|
|
263
284
|
async function patchServerOutputFiles(root, clientEntryUrl, clientStylesheetUrls) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@megabudino/stack-utils",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.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": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"cheerio": "^1.2.0",
|
|
44
|
+
"undici": "^7.24.0",
|
|
44
45
|
"node-addon-api": "^8.8.0",
|
|
45
46
|
"node-gyp": "^12.3.0",
|
|
46
47
|
"sharp": "^0.34.5"
|