wrangler 0.0.0-e1c615f → 0.0.0-e5d3690

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/src/pages.tsx CHANGED
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable no-shadow */
2
2
 
3
- import assert from "assert";
4
3
  import type { BuilderCallback } from "yargs";
5
4
  import { join } from "path";
6
5
  import { tmpdir } from "os";
7
- import { existsSync, lstatSync, readFileSync } from "fs";
6
+ import { existsSync, lstatSync, readFileSync, writeFileSync } from "fs";
8
7
  import { execSync, spawn } from "child_process";
9
8
  import { URL } from "url";
10
9
  import { getType } from "mime";
@@ -22,7 +21,7 @@ import { generateConfigFromFileTree } from "../pages/functions/filepath-routing"
22
21
  import type { Headers, Request, fetch } from "@miniflare/core";
23
22
  import type { MiniflareOptions } from "miniflare";
24
23
 
25
- const EXIT_CALLBACKS = [];
24
+ const EXIT_CALLBACKS: (() => void)[] = [];
26
25
  const EXIT = (message?: string, code?: number) => {
27
26
  if (message) console.log(message);
28
27
  if (code) process.exitCode = code;
@@ -122,7 +121,9 @@ async function spawnProxyProcess({
122
121
  },
123
122
  }
124
123
  );
125
- EXIT_CALLBACKS.push(() => proxy.kill());
124
+ EXIT_CALLBACKS.push(() => {
125
+ proxy.kill();
126
+ });
126
127
 
127
128
  proxy.stdout.on("data", (data) => {
128
129
  console.log(`[proxy]: ${data}`);
@@ -642,16 +643,20 @@ const RUNNING_BUILDERS: BuildResult[] = [];
642
643
 
643
644
  async function buildFunctions({
644
645
  scriptPath,
646
+ outputConfigPath,
645
647
  functionsDirectory,
646
648
  minify = false,
647
649
  sourcemap = false,
650
+ fallbackService = "ASSETS",
648
651
  watch = false,
649
652
  onEnd,
650
653
  }: {
651
654
  scriptPath: string;
655
+ outputConfigPath?: string;
652
656
  functionsDirectory: string;
653
657
  minify?: boolean;
654
658
  sourcemap?: boolean;
659
+ fallbackService?: string;
655
660
  watch?: boolean;
656
661
  onEnd?: () => void;
657
662
  }) {
@@ -666,6 +671,13 @@ async function buildFunctions({
666
671
  baseURL: "/",
667
672
  });
668
673
 
674
+ if (outputConfigPath) {
675
+ writeFileSync(
676
+ outputConfigPath,
677
+ JSON.stringify({ ...config, baseURL: "/" }, null, 2)
678
+ );
679
+ }
680
+
669
681
  await writeRoutesModule({
670
682
  config,
671
683
  srcDir: functionsDirectory,
@@ -678,6 +690,7 @@ async function buildFunctions({
678
690
  outfile: scriptPath,
679
691
  minify,
680
692
  sourcemap,
693
+ fallbackService,
681
694
  watch,
682
695
  onEnd,
683
696
  })
@@ -738,6 +751,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
738
751
  description: "Durable Object to bind (NAME=CLASS)",
739
752
  alias: "o",
740
753
  },
754
+ "live-reload": {
755
+ type: "boolean",
756
+ default: false,
757
+ description: "Auto reload HTML pages when change is detected",
758
+ },
741
759
  // TODO: Miniflare user options
742
760
  });
743
761
  },
@@ -750,6 +768,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
750
768
  binding: bindings = [],
751
769
  kv: kvs = [],
752
770
  do: durableObjects = [],
771
+ "live-reload": liveReload,
753
772
  "--": remaining = [],
754
773
  }) => {
755
774
  if (!local) {
@@ -774,7 +793,10 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
774
793
 
775
794
  let miniflareArgs: MiniflareOptions = {};
776
795
 
777
- let scriptReady = true;
796
+ let scriptReadyResolve;
797
+ const scriptReadyPromise = new Promise(
798
+ (resolve) => (scriptReadyResolve = resolve)
799
+ );
778
800
 
779
801
  if (usingFunctions) {
780
802
  const scriptPath = join(tmpdir(), "./functionsWorker.js");
@@ -786,22 +808,19 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
786
808
  functionsDirectory,
787
809
  sourcemap: true,
788
810
  watch: true,
789
- onEnd: () => {
790
- scriptReady = true;
791
- },
811
+ onEnd: () => scriptReadyResolve(),
792
812
  });
793
813
 
794
814
  watch([functionsDirectory], {
795
815
  persistent: true,
816
+ ignoreInitial: true,
796
817
  }).on("all", async () => {
797
818
  await buildFunctions({
798
819
  scriptPath,
799
820
  functionsDirectory,
800
821
  sourcemap: true,
801
822
  watch: true,
802
- onEnd: () => {
803
- scriptReady = true;
804
- },
823
+ onEnd: () => scriptReadyResolve(),
805
824
  });
806
825
  });
807
826
 
@@ -838,11 +857,19 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
838
857
  const { Miniflare, Log, LogLevel } = await import("miniflare");
839
858
  const { Response, fetch } = await import("@miniflare/core");
840
859
 
841
- // Should only be called if no proxyPort, using `assert.fail()` here
842
- // means the type of `assetsFetch` is still `typeof fetch`
843
- const assetsFetch = proxyPort
844
- ? () => assert.fail()
845
- : await generateAssetsFetch(directory);
860
+ // Wait for esbuild to finish building before starting Miniflare.
861
+ // This must be before the call to `new Miniflare`, as that will
862
+ // asynchronously start loading the script. `await startServer()`
863
+ // internally just waits for that promise to resolve.
864
+ await scriptReadyPromise;
865
+
866
+ // `assetsFetch()` will only be called if there is `proxyPort` defined.
867
+ // We only define `proxyPort`, above, when there is no `directory` defined.
868
+ const assetsFetch =
869
+ directory !== undefined
870
+ ? await generateAssetsFetch(directory)
871
+ : invalidAssetsFetch;
872
+
846
873
  const miniflare = new Miniflare({
847
874
  port,
848
875
  watch: true,
@@ -873,7 +900,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
873
900
  if (proxyPort) {
874
901
  try {
875
902
  const url = new URL(request.url);
876
- url.host = `127.0.0.1:${proxyPort}`;
903
+ url.host = `localhost:${proxyPort}`;
877
904
  return await fetch(url, request);
878
905
  } catch (thrown) {
879
906
  console.error(`Could not proxy request: ${thrown}`);
@@ -903,22 +930,29 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
903
930
  kvPersist: true,
904
931
  durableObjectsPersist: true,
905
932
  cachePersist: true,
933
+ liveReload,
906
934
 
907
935
  ...miniflareArgs,
908
936
  });
909
937
 
910
- // Wait for esbuild to finish building the script
911
- while (!scriptReady) {}
912
-
913
938
  try {
914
939
  // `startServer` might throw if user code contains errors
915
940
  const server = await miniflare.startServer();
916
- console.log(`Serving at http://127.0.0.1:${port}/`);
941
+ console.log(`Serving at http://localhost:${port}/`);
917
942
 
918
943
  if (process.env.BROWSER !== "none") {
919
- try {
920
- await open(`http://127.0.0.1:${port}/`);
921
- } catch {}
944
+ const childProcess = await open(`http://localhost:${port}/`);
945
+ // fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
946
+ childProcess.on("error", (_err) => {});
947
+ }
948
+
949
+ if (directory !== undefined && liveReload) {
950
+ watch([directory], {
951
+ persistent: true,
952
+ ignoreInitial: true,
953
+ }).on("all", async () => {
954
+ await miniflare.reload();
955
+ });
922
956
  }
923
957
 
924
958
  EXIT_CALLBACKS.push(() => {
@@ -933,36 +967,74 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
933
967
  )
934
968
  .command("functions", "Cloudflare Pages Functions", (yargs) =>
935
969
  yargs.command(
936
- "build",
970
+ "build [directory]",
937
971
  "Compile a folder of Cloudflare Pages Functions into a single Worker",
938
972
  (yargs) =>
939
- yargs.options({
940
- "script-path": {
973
+ yargs
974
+ .positional("directory", {
941
975
  type: "string",
942
- default: "_worker.js",
943
- description: "The location of the output Worker script",
944
- },
945
- minify: {
946
- type: "boolean",
947
- default: false,
948
- description: "Minify the output Worker script",
949
- },
950
- sourcemap: {
951
- type: "boolean",
952
- default: false,
953
- description: "Generate a sourcemap for the output Worker script",
954
- },
955
- }),
956
- async ({ "script-path": scriptPath, minify, sourcemap }) => {
957
- const functionsDirectory = "./functions";
958
-
976
+ default: "functions",
977
+ description: "The directory of Pages Functions",
978
+ })
979
+ .options({
980
+ "script-path": {
981
+ type: "string",
982
+ default: "_worker.js",
983
+ description: "The location of the output Worker script",
984
+ },
985
+ "output-config-path": {
986
+ type: "string",
987
+ description: "The location for the output config file",
988
+ },
989
+ minify: {
990
+ type: "boolean",
991
+ default: false,
992
+ description: "Minify the output Worker script",
993
+ },
994
+ sourcemap: {
995
+ type: "boolean",
996
+ default: false,
997
+ description:
998
+ "Generate a sourcemap for the output Worker script",
999
+ },
1000
+ "fallback-service": {
1001
+ type: "string",
1002
+ default: "ASSETS",
1003
+ description:
1004
+ "The service to fallback to at the end of the `next` chain. Setting to '' will fallback to the global `fetch`.",
1005
+ },
1006
+ watch: {
1007
+ type: "boolean",
1008
+ default: false,
1009
+ description:
1010
+ "Watch for changes to the functions and automatically rebuild the Worker script",
1011
+ },
1012
+ }),
1013
+ async ({
1014
+ directory,
1015
+ "script-path": scriptPath,
1016
+ "output-config-path": outputConfigPath,
1017
+ minify,
1018
+ sourcemap,
1019
+ fallbackService,
1020
+ watch,
1021
+ }) => {
959
1022
  await buildFunctions({
960
1023
  scriptPath,
961
- functionsDirectory,
1024
+ outputConfigPath,
1025
+ functionsDirectory: directory,
962
1026
  minify,
963
1027
  sourcemap,
1028
+ fallbackService,
1029
+ watch,
964
1030
  });
965
1031
  }
966
1032
  )
967
1033
  );
968
1034
  };
1035
+
1036
+ const invalidAssetsFetch: typeof fetch = () => {
1037
+ throw new Error(
1038
+ "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1039
+ );
1040
+ };
package/src/proxy.ts CHANGED
@@ -1,97 +1,245 @@
1
1
  import { connect } from "node:http2";
2
+ import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
2
3
  import { createServer } from "node:http";
3
- import type { Server, IncomingHttpHeaders, RequestListener } from "node:http";
4
+ import type {
5
+ IncomingHttpHeaders,
6
+ RequestListener,
7
+ IncomingMessage,
8
+ ServerResponse,
9
+ Server,
10
+ } from "node:http";
4
11
  import WebSocket from "faye-websocket";
5
12
  import serveStatic from "serve-static";
13
+ import type { CfPreviewToken } from "./api/preview";
14
+ import { useEffect, useRef } from "react";
6
15
 
7
- export interface HttpProxyInit {
8
- host: string;
9
- assetPath?: string | null;
10
- onRequest?: (headers: IncomingHttpHeaders) => void;
11
- onResponse?: (headers: IncomingHttpHeaders) => void;
16
+ /**
17
+ * `usePreviewServer` is a React hook that creates a local development
18
+ * server that can be used to develop a Worker.
19
+ *
20
+ * When we run `wrangler dev`, we start by uploading the compiled worker
21
+ * to the preview service, which responds with a preview token.
22
+ * (see `useWorker`/`createWorker` for details.)
23
+ * We can then use that token to connect to the preview server for a
24
+ * great local development experience. Further, as we change the worker,
25
+ * we can update the preview token transparently without having to restart
26
+ * the development server.
27
+ */
28
+
29
+ /** Rewrite request headers to add the preview token. */
30
+ function addCfPreviewTokenHeader(
31
+ headers: IncomingHttpHeaders,
32
+ previewTokenValue: string
33
+ ) {
34
+ headers["cf-workers-preview-token"] = previewTokenValue;
12
35
  }
13
36
 
14
37
  /**
15
- * Creates a HTTP/1 proxy that sends requests over HTTP/2.
38
+ * Rewrite references in request headers
39
+ * from the preview host to the local host.
16
40
  */
17
- export function createHttpProxy(init: HttpProxyInit): Server {
18
- const { host, assetPath, onRequest = () => {}, onResponse = () => {} } = init;
19
- const remote = connect(`https://${host}`);
20
- const local = createServer();
21
- // HTTP/2 -> HTTP/2
22
- local.on("stream", (stream, headers: IncomingHttpHeaders) => {
23
- onRequest(headers);
24
- headers[":authority"] = host;
25
- const request = stream.pipe(remote.request(headers));
26
- request.on("response", (responseHeaders: IncomingHttpHeaders) => {
27
- onResponse(responseHeaders);
28
- stream.respond(responseHeaders);
29
- request.pipe(stream, { end: true });
30
- });
31
- });
32
- // HTTP/1 -> HTTP/2
33
- const handleRequest: RequestListener = (message, response) => {
34
- const { httpVersionMajor, headers, method, url } = message;
35
- if (httpVersionMajor >= 2) {
36
- return; // Already handled by the "stream" event.
41
+ function rewriteRemoteHostToLocalHostInHeaders(
42
+ headers: IncomingHttpHeaders,
43
+ remoteHost: string,
44
+ localPort: number
45
+ ) {
46
+ for (const [name, value] of Object.entries(headers)) {
47
+ // Rewrite the remote host to the local host.
48
+ if (typeof value === "string" && value.includes(remoteHost)) {
49
+ headers[name] = value
50
+ .replaceAll(`https://${remoteHost}`, `http://localhost:${localPort}`)
51
+ .replaceAll(remoteHost, `localhost:${localPort}`);
37
52
  }
38
- onRequest(headers);
39
- headers[":method"] = method;
40
- headers[":path"] = url;
41
- headers[":authority"] = host;
42
- headers[":scheme"] = "https";
43
- for (const name of Object.keys(headers)) {
44
- if (HTTP1_HEADERS.has(name.toLowerCase())) {
45
- delete headers[name];
46
- }
53
+ }
54
+ }
55
+
56
+ export function usePreviewServer({
57
+ previewToken,
58
+ publicRoot,
59
+ port,
60
+ }: {
61
+ previewToken: CfPreviewToken | undefined;
62
+ publicRoot: undefined | string;
63
+ port: number;
64
+ }) {
65
+ /** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
66
+ const proxyServer = useRef<Server>();
67
+ const proxy = (proxyServer.current ??= createProxyServer());
68
+
69
+ /**
70
+ * When we're not connected / getting a fresh token on changes,
71
+ * we'd like to buffer streams/requests until we're connected.
72
+ * Once connected, we can flush the buffered streams/requests.
73
+ * streamBufferRef is used to buffer http/2 streams, while
74
+ * requestResponseBufferRef is used to buffer http/1 requests.
75
+ */
76
+ const streamBufferRef = useRef<
77
+ { stream: ServerHttp2Stream; headers: IncomingHttpHeaders }[]
78
+ >([]);
79
+ const requestResponseBufferRef = useRef<
80
+ { request: IncomingMessage; response: ServerResponse }[]
81
+ >([]);
82
+
83
+ useEffect(() => {
84
+ // If we don't have a token, that means either we're just starting up,
85
+ // or we're refreshing the token.
86
+ if (!previewToken) {
87
+ const cleanupListeners: (() => void)[] = [];
88
+ const bufferStream = (
89
+ stream: ServerHttp2Stream,
90
+ headers: IncomingHttpHeaders
91
+ ) => {
92
+ // store the stream in a buffer so we can replay it later
93
+ streamBufferRef.current.push({ stream, headers });
94
+ };
95
+ proxy.on("stream", bufferStream);
96
+ cleanupListeners.push(() => proxy.off("stream", bufferStream));
97
+
98
+ const bufferRequestResponse = (
99
+ request: IncomingMessage,
100
+ response: ServerResponse
101
+ ) => {
102
+ // store the request and response in a buffer so we can replay it later
103
+ requestResponseBufferRef.current.push({ request, response });
104
+ };
105
+
106
+ proxy.on("request", bufferRequestResponse);
107
+ cleanupListeners.push(() => proxy.off("request", bufferRequestResponse));
108
+ return () => {
109
+ cleanupListeners.forEach((cleanup) => cleanup());
110
+ };
47
111
  }
48
- const request = message.pipe(remote.request(headers));
49
- request.on("response", (responseHeaders) => {
50
- const status = responseHeaders[":status"];
51
- onResponse(responseHeaders);
52
- for (const name of Object.keys(responseHeaders)) {
53
- if (name.startsWith(":")) {
54
- delete responseHeaders[name];
112
+
113
+ // We have a token. Let's proxy requests to the preview end point.
114
+ const cleanupListeners: (() => void)[] = [];
115
+
116
+ const assetPath = typeof publicRoot === "string" ? publicRoot : null;
117
+
118
+ // create a ClientHttp2Session
119
+ const remote = connect(`https://${previewToken.host}`);
120
+ cleanupListeners.push(() => remote.destroy());
121
+
122
+ /** HTTP/2 -> HTTP/2 */
123
+ const handleStream = createStreamHandler(previewToken, remote, port);
124
+ proxy.on("stream", handleStream);
125
+ cleanupListeners.push(() => proxy.off("stream", handleStream));
126
+
127
+ // flush and replay buffered streams
128
+ streamBufferRef.current.forEach((buffer) =>
129
+ handleStream(buffer.stream, buffer.headers)
130
+ );
131
+ streamBufferRef.current = [];
132
+
133
+ /** HTTP/1 -> HTTP/2 */
134
+ const handleRequest: RequestListener = (
135
+ message: IncomingMessage,
136
+ response: ServerResponse
137
+ ) => {
138
+ const { httpVersionMajor, headers, method, url } = message;
139
+ if (httpVersionMajor >= 2) {
140
+ return; // Already handled by the "stream" event.
141
+ }
142
+ addCfPreviewTokenHeader(headers, previewToken.value);
143
+ headers[":method"] = method;
144
+ headers[":path"] = url;
145
+ headers[":authority"] = previewToken.host;
146
+ headers[":scheme"] = "https";
147
+ for (const name of Object.keys(headers)) {
148
+ if (HTTP1_HEADERS.has(name.toLowerCase())) {
149
+ delete headers[name];
55
150
  }
56
151
  }
57
- response.writeHead(status, responseHeaders);
58
- request.pipe(response, { end: true });
59
- });
60
- };
61
- // If an asset path is defined, check the file system
62
- // for a file first and serve if it exists.
63
- if (assetPath) {
64
- const handleAsset = serveStatic(assetPath, {
65
- cacheControl: false,
66
- });
67
- local.on("request", (request, response) => {
68
- handleAsset(request, response, () => {
69
- handleRequest(request, response);
152
+ const request = message.pipe(remote.request(headers));
153
+ request.on("response", (responseHeaders) => {
154
+ const status = responseHeaders[":status"] ?? 500;
155
+ rewriteRemoteHostToLocalHostInHeaders(
156
+ responseHeaders,
157
+ previewToken.host,
158
+ port
159
+ );
160
+ for (const name of Object.keys(responseHeaders)) {
161
+ if (name.startsWith(":")) {
162
+ delete responseHeaders[name];
163
+ }
164
+ }
165
+ response.writeHead(status, responseHeaders);
166
+ request.pipe(response, { end: true });
70
167
  });
71
- });
72
- } else {
73
- local.on("request", handleRequest);
74
- }
75
- // HTTP/1 -> WebSocket (over HTTP/1)
76
- local.on("upgrade", (message, socket, body) => {
77
- const { headers, url } = message;
78
- onRequest(headers);
79
- headers["host"] = host;
80
- const localWebsocket = new WebSocket(message, socket, body);
81
- // TODO(soon): Custom WebSocket protocol is not working?
82
- const remoteWebsocketClient = new WebSocket.Client(
83
- `wss://${host}${url}`,
84
- [],
85
- { headers }
168
+ };
169
+
170
+ // If an asset path is defined, check the file system
171
+ // for a file first and serve if it exists.
172
+ const actualHandleRequest = assetPath
173
+ ? createHandleAssetsRequest(assetPath, handleRequest)
174
+ : handleRequest;
175
+
176
+ proxy.on("request", actualHandleRequest);
177
+ cleanupListeners.push(() => proxy.off("request", actualHandleRequest));
178
+
179
+ // flush and replay buffered requests
180
+ requestResponseBufferRef.current.forEach(({ request, response }) =>
181
+ actualHandleRequest(request, response)
86
182
  );
87
- localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
88
- });
89
- remote.on("close", () => {
90
- local.close();
183
+ requestResponseBufferRef.current = [];
184
+
185
+ /** HTTP/1 -> WebSocket (over HTTP/1) */
186
+ const handleUpgrade = (
187
+ message: IncomingMessage,
188
+ socket: WebSocket,
189
+ body: Buffer
190
+ ) => {
191
+ const { headers, url } = message;
192
+ addCfPreviewTokenHeader(headers, previewToken.value);
193
+ headers["host"] = previewToken.host;
194
+ const localWebsocket = new WebSocket(message, socket, body);
195
+ // TODO(soon): Custom WebSocket protocol is not working?
196
+ const remoteWebsocketClient = new WebSocket.Client(
197
+ `wss://${previewToken.host}${url}`,
198
+ [],
199
+ { headers }
200
+ );
201
+ localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
202
+ // We close down websockets whenever we refresh the token.
203
+ cleanupListeners.push(() => {
204
+ localWebsocket.destroy();
205
+ remoteWebsocketClient.destroy();
206
+ });
207
+ };
208
+ proxy.on("upgrade", handleUpgrade);
209
+ cleanupListeners.push(() => proxy.off("upgrade", handleUpgrade));
210
+
211
+ return () => {
212
+ cleanupListeners.forEach((d) => d());
213
+ };
214
+ }, [previewToken, publicRoot, port, proxy]);
215
+
216
+ // Start/stop the server whenever the
217
+ // containing component is mounted/unmounted.
218
+ useEffect(() => {
219
+ proxy.listen(port);
220
+ console.log(`⬣ Listening at http://localhost:${port}`);
221
+
222
+ return () => {
223
+ proxy.close();
224
+ };
225
+ }, [port, proxy]);
226
+ }
227
+
228
+ function createHandleAssetsRequest(
229
+ assetPath: string,
230
+ handleRequest: RequestListener
231
+ ) {
232
+ const handleAsset = serveStatic(assetPath, {
233
+ cacheControl: false,
91
234
  });
92
- return local;
235
+ return (request: IncomingMessage, response: ServerResponse) => {
236
+ handleAsset(request, response, () => {
237
+ handleRequest(request, response);
238
+ });
239
+ };
93
240
  }
94
241
 
242
+ /** A Set of headers we want to remove from HTTP/1 requests. */
95
243
  const HTTP1_HEADERS = new Set([
96
244
  "host",
97
245
  "connection",
@@ -101,3 +249,54 @@ const HTTP1_HEADERS = new Set([
101
249
  "transfer-encoding",
102
250
  "http2-settings",
103
251
  ]);
252
+
253
+ function createProxyServer() {
254
+ return createServer()
255
+ .on("request", function (req, res) {
256
+ // log all requests
257
+ console.log(
258
+ new Date().toLocaleTimeString(),
259
+ req.method,
260
+ req.url,
261
+ res.statusCode
262
+ );
263
+ })
264
+ .on("upgrade", (req) => {
265
+ // log all websocket connections
266
+ console.log(
267
+ new Date().toLocaleTimeString(),
268
+ req.method,
269
+ req.url,
270
+ 101,
271
+ "(WebSocket)"
272
+ );
273
+ })
274
+ .on("error", (err) => {
275
+ // log all connection errors
276
+ console.error(new Date().toLocaleTimeString(), err);
277
+ });
278
+ }
279
+
280
+ function createStreamHandler(
281
+ previewToken: CfPreviewToken,
282
+ remote: ClientHttp2Session,
283
+ port: number
284
+ ) {
285
+ return function handleStream(
286
+ stream: ServerHttp2Stream,
287
+ headers: IncomingHttpHeaders
288
+ ) {
289
+ addCfPreviewTokenHeader(headers, previewToken.value);
290
+ headers[":authority"] = previewToken.host;
291
+ const request = stream.pipe(remote.request(headers));
292
+ request.on("response", (responseHeaders: IncomingHttpHeaders) => {
293
+ rewriteRemoteHostToLocalHostInHeaders(
294
+ responseHeaders,
295
+ previewToken.host,
296
+ port
297
+ );
298
+ stream.respond(responseHeaders);
299
+ request.pipe(stream, { end: true });
300
+ });
301
+ };
302
+ }