wrangler 0.0.0-e2a26a0 → 0.0.0-e3278fa

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.
Files changed (150) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +49 -5
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +51 -27
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +223 -36
  9. package/pages/functions/filepath-routing.ts +78 -120
  10. package/pages/functions/routes.ts +13 -17
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +24 -17
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +3408 -0
  15. package/src/__tests__/dev.test.tsx +977 -65
  16. package/src/__tests__/guess-worker-format.test.ts +85 -0
  17. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  18. package/src/__tests__/helpers/mock-account-id.ts +26 -8
  19. package/src/__tests__/helpers/mock-bin.ts +4 -1
  20. package/src/__tests__/helpers/mock-cfetch.ts +10 -6
  21. package/src/__tests__/helpers/mock-console.ts +39 -8
  22. package/src/__tests__/helpers/mock-dialogs.ts +67 -2
  23. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  24. package/src/__tests__/helpers/mock-istty.ts +27 -0
  25. package/src/__tests__/helpers/mock-kv.ts +9 -16
  26. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  27. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  28. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  29. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  30. package/src/__tests__/helpers/run-wrangler.ts +5 -3
  31. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  32. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  33. package/src/__tests__/https-options.test.ts +130 -0
  34. package/src/__tests__/index.test.ts +148 -435
  35. package/src/__tests__/init.test.ts +1637 -0
  36. package/src/__tests__/jest.setup.ts +45 -5
  37. package/src/__tests__/kv.test.ts +590 -248
  38. package/src/__tests__/logger.test.ts +115 -0
  39. package/src/__tests__/package-manager.test.ts +156 -9
  40. package/src/__tests__/pages.test.ts +754 -0
  41. package/src/__tests__/parse.test.ts +330 -0
  42. package/src/__tests__/publish.test.ts +5471 -939
  43. package/src/__tests__/r2.test.ts +233 -0
  44. package/src/__tests__/route.test.ts +45 -0
  45. package/src/__tests__/secret.test.ts +362 -91
  46. package/src/__tests__/tail.test.ts +690 -0
  47. package/src/__tests__/user.test.ts +131 -0
  48. package/src/__tests__/whoami.test.tsx +60 -7
  49. package/src/abort.d.ts +3 -0
  50. package/src/bundle.ts +197 -0
  51. package/src/cfetch/index.ts +27 -8
  52. package/src/cfetch/internal.ts +73 -32
  53. package/src/cli.ts +6 -2
  54. package/src/config/README.md +107 -0
  55. package/src/config/config.ts +234 -0
  56. package/src/config/diagnostics.ts +80 -0
  57. package/src/config/environment.ts +420 -0
  58. package/src/config/index.ts +225 -0
  59. package/src/config/validation-helpers.ts +584 -0
  60. package/src/config/validation.ts +1539 -0
  61. package/src/config-cache.ts +45 -0
  62. package/src/create-worker-preview.ts +185 -0
  63. package/src/create-worker-upload-form.ts +254 -0
  64. package/src/dev/dev-vars.ts +36 -0
  65. package/src/dev/dev.tsx +412 -0
  66. package/src/dev/local.tsx +322 -0
  67. package/src/dev/remote.tsx +237 -0
  68. package/src/dev/use-esbuild.ts +118 -0
  69. package/src/dialogs.tsx +64 -6
  70. package/src/durable.ts +102 -0
  71. package/src/entry.ts +322 -0
  72. package/src/environment-variables.ts +35 -0
  73. package/src/errors.ts +11 -0
  74. package/src/https-options.ts +123 -0
  75. package/src/index.tsx +1747 -1002
  76. package/src/inspect.ts +80 -14
  77. package/src/intl-polyfill.d.ts +139 -0
  78. package/src/{kv.tsx → kv.ts} +192 -68
  79. package/src/logger.ts +66 -0
  80. package/src/miniflare-cli/README.md +30 -0
  81. package/src/miniflare-cli/enum-keys.ts +17 -0
  82. package/src/miniflare-cli/index.ts +41 -0
  83. package/src/miniflare-cli/request-context.ts +40 -0
  84. package/src/module-collection.ts +226 -31
  85. package/src/open-in-browser.ts +19 -0
  86. package/src/package-manager.ts +79 -25
  87. package/src/pages.tsx +1131 -84
  88. package/src/parse.ts +202 -0
  89. package/src/proxy.ts +170 -62
  90. package/src/publish.ts +612 -306
  91. package/src/r2.ts +50 -0
  92. package/src/selfsigned.d.ts +29 -0
  93. package/src/sites.tsx +143 -57
  94. package/src/tail/filters.ts +279 -0
  95. package/src/tail/index.ts +299 -0
  96. package/src/tail/printing.ts +67 -0
  97. package/src/update-check.ts +19 -0
  98. package/src/user.tsx +341 -146
  99. package/src/whoami.tsx +5 -3
  100. package/src/{api/worker.ts → worker.ts} +65 -39
  101. package/src/zones.ts +73 -0
  102. package/templates/gitignore +171 -0
  103. package/templates/new-worker-scheduled.js +17 -0
  104. package/templates/new-worker-scheduled.ts +32 -0
  105. package/templates/new-worker.js +1 -1
  106. package/templates/new-worker.ts +17 -2
  107. package/templates/static-asset-facade.js +1 -5
  108. package/templates/tsconfig.json +105 -0
  109. package/wrangler-dist/cli.js +72295 -71006
  110. package/src/__tests__/helpers/mock-user.ts +0 -27
  111. package/src/__tests__/logout.test.ts +0 -50
  112. package/src/api/form_data.ts +0 -134
  113. package/src/api/preview.ts +0 -128
  114. package/src/config.ts +0 -641
  115. package/src/dev.tsx +0 -834
  116. package/src/tail.tsx +0 -73
  117. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  118. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  119. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  120. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  121. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  122. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  123. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  124. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  125. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  126. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  127. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  128. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  129. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  130. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  131. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  132. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  133. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  134. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  135. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  136. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  137. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  138. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  139. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  140. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  141. package/vendor/wrangler-mime/LICENSE +0 -21
  142. package/vendor/wrangler-mime/Mime.js +0 -97
  143. package/vendor/wrangler-mime/README.md +0 -187
  144. package/vendor/wrangler-mime/cli.js +0 -46
  145. package/vendor/wrangler-mime/index.js +0 -4
  146. package/vendor/wrangler-mime/lite.js +0 -4
  147. package/vendor/wrangler-mime/package.json +0 -52
  148. package/vendor/wrangler-mime/types/other.js +0 -1
  149. package/vendor/wrangler-mime/types/standard.js +0 -1
  150. package/wrangler-dist/cli.js.map +0 -7
package/src/parse.ts ADDED
@@ -0,0 +1,202 @@
1
+ import * as fs from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import TOML from "@iarna/toml";
4
+ import { formatMessagesSync } from "esbuild";
5
+
6
+ export type Message = {
7
+ text: string;
8
+ location?: Location;
9
+ notes?: Message[];
10
+ kind?: "warning" | "error";
11
+ };
12
+
13
+ export type Location = File & {
14
+ line: number;
15
+ column: number;
16
+ length?: number;
17
+ lineText?: string;
18
+ suggestion?: string;
19
+ };
20
+
21
+ export type File = {
22
+ file?: string;
23
+ fileText?: string;
24
+ };
25
+
26
+ /**
27
+ * Formats a `Message` using esbuild's pretty-printing algorithm.
28
+ */
29
+ export function formatMessage(
30
+ { text, notes, location, kind = "error" }: Message,
31
+ color = true
32
+ ): string {
33
+ const input = { text, notes, location };
34
+ delete input.location?.fileText;
35
+ for (const note of notes ?? []) {
36
+ delete note.location?.fileText;
37
+ }
38
+ const lines = formatMessagesSync([input], {
39
+ color,
40
+ kind: kind,
41
+ terminalWidth: process.stderr.columns,
42
+ });
43
+ return lines.join("\n");
44
+ }
45
+
46
+ /**
47
+ * An error that's thrown when something fails to parse.
48
+ */
49
+ export class ParseError extends Error implements Message {
50
+ readonly text: string;
51
+ readonly notes: Message[];
52
+ readonly location?: Location;
53
+ readonly kind: "warning" | "error";
54
+
55
+ constructor({ text, notes, location, kind }: Message) {
56
+ super(text);
57
+ this.name = this.constructor.name;
58
+ this.text = text;
59
+ this.notes = notes ?? [];
60
+ this.location = location;
61
+ this.kind = kind ?? "error";
62
+ }
63
+ }
64
+
65
+ const TOML_ERROR_NAME = "TomlError";
66
+ const TOML_ERROR_SUFFIX = " at row ";
67
+
68
+ type TomlError = Error & {
69
+ line: number;
70
+ col: number;
71
+ };
72
+
73
+ /**
74
+ * A wrapper around `TOML.parse` that throws a `ParseError`.
75
+ */
76
+ export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
77
+ try {
78
+ // Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
79
+ const normalizedInput = input.replace(/\r\n/g, "\n");
80
+ return TOML.parse(normalizedInput);
81
+ } catch (err) {
82
+ const { name, message, line, col } = err as TomlError;
83
+ if (name !== TOML_ERROR_NAME) {
84
+ throw err;
85
+ }
86
+ const text = message.substring(0, message.lastIndexOf(TOML_ERROR_SUFFIX));
87
+ const lineText = input.split("\n")[line];
88
+ const location = {
89
+ lineText,
90
+ line: line + 1,
91
+ column: col - 1,
92
+ file,
93
+ fileText: input,
94
+ };
95
+ throw new ParseError({ text, location });
96
+ }
97
+ }
98
+
99
+ const JSON_ERROR_SUFFIX = " in JSON at position ";
100
+
101
+ /**
102
+ * A minimal type describing a package.json file.
103
+ */
104
+ export type PackageJSON = {
105
+ devDependencies?: Record<string, unknown>;
106
+ dependencies?: Record<string, unknown>;
107
+ scripts?: Record<string, unknown>;
108
+ };
109
+
110
+ /**
111
+ * A typed version of `parseJSON()`.
112
+ */
113
+ export function parsePackageJSON<T extends PackageJSON = PackageJSON>(
114
+ input: string,
115
+ file?: string
116
+ ): T {
117
+ return parseJSON<T>(input, file);
118
+ }
119
+
120
+ /**
121
+ * A wrapper around `JSON.parse` that throws a `ParseError`.
122
+ */
123
+ export function parseJSON<T>(input: string, file?: string): T {
124
+ try {
125
+ return JSON.parse(input);
126
+ } catch (err) {
127
+ const { message } = err as Error;
128
+ const index = message.lastIndexOf(JSON_ERROR_SUFFIX);
129
+ if (index < 0) {
130
+ throw err;
131
+ }
132
+ const text = message.substring(0, index);
133
+ const position = parseInt(
134
+ message.substring(index + JSON_ERROR_SUFFIX.length)
135
+ );
136
+ const location = indexLocation({ file, fileText: input }, position);
137
+ throw new ParseError({ text, location });
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Reads a file and parses it based on its type.
143
+ */
144
+ export function readFileSync(file: string): string {
145
+ try {
146
+ return fs.readFileSync(file, { encoding: "utf-8" });
147
+ } catch (err) {
148
+ const { message } = err as Error;
149
+ throw new ParseError({
150
+ text: `Could not read file: ${file}`,
151
+ notes: [
152
+ {
153
+ text: message.replace(file, resolve(file)),
154
+ },
155
+ ],
156
+ });
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Calculates the line and column location from an index.
162
+ */
163
+ export function indexLocation(file: File, index: number): Location {
164
+ let lineText,
165
+ line = 0,
166
+ column = 0,
167
+ cursor = 0;
168
+ const { fileText = "" } = file;
169
+ for (const row of fileText.split("\n")) {
170
+ line++;
171
+ cursor += row.length + 1;
172
+ if (cursor >= index) {
173
+ lineText = row;
174
+ column = row.length - (cursor - index);
175
+ break;
176
+ }
177
+ }
178
+ return { lineText, line, column, ...file };
179
+ }
180
+
181
+ /**
182
+ * Guesses the line and column location of a search query.
183
+ */
184
+ export function searchLocation(file: File, query: unknown): Location {
185
+ let lineText,
186
+ length,
187
+ line = 0,
188
+ column = 0;
189
+ const queryText = String(query);
190
+ const { fileText = "" } = file;
191
+ for (const content of fileText.split("\n")) {
192
+ line++;
193
+ const index = content.indexOf(queryText);
194
+ if (index >= 0) {
195
+ lineText = content;
196
+ column = index;
197
+ length = queryText.length;
198
+ break;
199
+ }
200
+ }
201
+ return { lineText, line, column, length, ...file };
202
+ }
package/src/proxy.ts CHANGED
@@ -1,17 +1,29 @@
1
- import { createServer } from "node:http";
1
+ import { createServer as createHttpServer } from "node:http";
2
2
  import { connect } from "node:http2";
3
+ import { createServer as createHttpsServer } from "node:https";
3
4
  import WebSocket from "faye-websocket";
5
+ import { createHttpTerminator } from "http-terminator";
4
6
  import { useEffect, useRef, useState } from "react";
5
7
  import serveStatic from "serve-static";
6
- import type { CfPreviewToken } from "./api/preview";
8
+ import { getHttpsOptions } from "./https-options";
9
+ import { logger } from "./logger";
10
+ import type { CfPreviewToken } from "./create-worker-preview";
11
+ import type { HttpTerminator } from "http-terminator";
7
12
  import type {
8
13
  IncomingHttpHeaders,
9
14
  RequestListener,
10
15
  IncomingMessage,
11
16
  ServerResponse,
12
- Server,
17
+ Server as HttpServer,
13
18
  } from "node:http";
14
19
  import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
20
+ import type { Server as HttpsServer } from "node:https";
21
+ import type ws from "ws";
22
+
23
+ interface IWebsocket extends ws {
24
+ // Pipe implements .on("message", ...)
25
+ pipe<T>(fn: T): IWebsocket;
26
+ }
15
27
 
16
28
  /**
17
29
  * `usePreviewServer` is a React hook that creates a local development
@@ -41,30 +53,64 @@ function addCfPreviewTokenHeader(
41
53
  function rewriteRemoteHostToLocalHostInHeaders(
42
54
  headers: IncomingHttpHeaders,
43
55
  remoteHost: string,
44
- localPort: number
56
+ localPort: number,
57
+ localProtocol: "https" | "http"
45
58
  ) {
46
59
  for (const [name, value] of Object.entries(headers)) {
47
60
  // Rewrite the remote host to the local host.
48
61
  if (typeof value === "string" && value.includes(remoteHost)) {
49
62
  headers[name] = value
50
- .replaceAll(`https://${remoteHost}`, `http://localhost:${localPort}`)
63
+ .replaceAll(
64
+ `https://${remoteHost}`,
65
+ `${localProtocol}://localhost:${localPort}`
66
+ )
51
67
  .replaceAll(remoteHost, `localhost:${localPort}`);
52
68
  }
53
69
  }
54
70
  }
55
71
 
72
+ type PreviewProxy = {
73
+ server: HttpServer | HttpsServer;
74
+ terminator: HttpTerminator;
75
+ };
76
+
56
77
  export function usePreviewServer({
57
78
  previewToken,
58
79
  publicRoot,
59
- port,
80
+ localProtocol,
81
+ localPort: port,
82
+ ip,
60
83
  }: {
61
84
  previewToken: CfPreviewToken | undefined;
62
- publicRoot: undefined | string;
63
- port: number;
85
+ publicRoot: string | undefined;
86
+ localProtocol: "https" | "http";
87
+ localPort: number;
88
+ ip: string;
64
89
  }) {
65
90
  /** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
66
- const proxyServer = useRef<Server>();
67
- const proxy = (proxyServer.current ??= createProxyServer());
91
+ const [proxy, setProxy] = useState<PreviewProxy>();
92
+
93
+ /**
94
+ * Create the instance of the local proxy server that will pass on
95
+ * requests to the preview worker.
96
+ */
97
+ useEffect(() => {
98
+ if (proxy === undefined) {
99
+ createProxyServer(localProtocol)
100
+ .then((server) => {
101
+ setProxy({
102
+ server,
103
+ terminator: createHttpTerminator({
104
+ server,
105
+ gracefulTerminationTimeout: 0,
106
+ }),
107
+ });
108
+ })
109
+ .catch(async (err) => {
110
+ logger.error("Failed to create proxy server:", err);
111
+ });
112
+ }
113
+ }, [proxy, localProtocol]);
68
114
 
69
115
  /**
70
116
  * When we're not connected / getting a fresh token on changes,
@@ -81,7 +127,7 @@ export function usePreviewServer({
81
127
  >([]);
82
128
 
83
129
  /**
84
- * The session doesn't last forever, and will evetually drop
130
+ * The session doesn't last forever, and will eventually drop
85
131
  * (usually within 5-15 minutes). When that happens, we simply
86
132
  * restart the effect, effectively restarting the server. We use
87
133
  * a state sigil as an effect dependency to do so.
@@ -92,6 +138,10 @@ export function usePreviewServer({
92
138
  }
93
139
 
94
140
  useEffect(() => {
141
+ if (proxy === undefined) {
142
+ return;
143
+ }
144
+
95
145
  // If we don't have a token, that means either we're just starting up,
96
146
  // or we're refreshing the token.
97
147
  if (!previewToken) {
@@ -103,8 +153,8 @@ export function usePreviewServer({
103
153
  // store the stream in a buffer so we can replay it later
104
154
  streamBufferRef.current.push({ stream, headers });
105
155
  };
106
- proxy.on("stream", bufferStream);
107
- cleanupListeners.push(() => proxy.off("stream", bufferStream));
156
+ proxy.server.on("stream", bufferStream);
157
+ cleanupListeners.push(() => proxy.server.off("stream", bufferStream));
108
158
 
109
159
  const bufferRequestResponse = (
110
160
  request: IncomingMessage,
@@ -114,8 +164,10 @@ export function usePreviewServer({
114
164
  requestResponseBufferRef.current.push({ request, response });
115
165
  };
116
166
 
117
- proxy.on("request", bufferRequestResponse);
118
- cleanupListeners.push(() => proxy.off("request", bufferRequestResponse));
167
+ proxy.server.on("request", bufferRequestResponse);
168
+ cleanupListeners.push(() =>
169
+ proxy.server.off("request", bufferRequestResponse)
170
+ );
119
171
  return () => {
120
172
  cleanupListeners.forEach((cleanup) => cleanup());
121
173
  };
@@ -136,9 +188,14 @@ export function usePreviewServer({
136
188
  cleanupListeners.push(() => remote.off("close", retryServerSetup));
137
189
 
138
190
  /** HTTP/2 -> HTTP/2 */
139
- const handleStream = createStreamHandler(previewToken, remote, port);
140
- proxy.on("stream", handleStream);
141
- cleanupListeners.push(() => proxy.off("stream", handleStream));
191
+ const handleStream = createStreamHandler(
192
+ previewToken,
193
+ remote,
194
+ port,
195
+ localProtocol
196
+ );
197
+ proxy.server.on("stream", handleStream);
198
+ cleanupListeners.push(() => proxy.server.off("stream", handleStream));
142
199
 
143
200
  // flush and replay buffered streams
144
201
  streamBufferRef.current.forEach((buffer) =>
@@ -168,10 +225,15 @@ export function usePreviewServer({
168
225
  const request = message.pipe(remote.request(headers));
169
226
  request.on("response", (responseHeaders) => {
170
227
  const status = responseHeaders[":status"] ?? 500;
228
+
229
+ // log all requests to terminal
230
+ logger.log(new Date().toLocaleTimeString(), method, url, status);
231
+
171
232
  rewriteRemoteHostToLocalHostInHeaders(
172
233
  responseHeaders,
173
234
  previewToken.host,
174
- port
235
+ port,
236
+ localProtocol
175
237
  );
176
238
  for (const name of Object.keys(responseHeaders)) {
177
239
  if (name.startsWith(":")) {
@@ -189,8 +251,10 @@ export function usePreviewServer({
189
251
  ? createHandleAssetsRequest(assetPath, handleRequest)
190
252
  : handleRequest;
191
253
 
192
- proxy.on("request", actualHandleRequest);
193
- cleanupListeners.push(() => proxy.off("request", actualHandleRequest));
254
+ proxy.server.on("request", actualHandleRequest);
255
+ cleanupListeners.push(() =>
256
+ proxy.server.off("request", actualHandleRequest)
257
+ );
194
258
 
195
259
  // flush and replay buffered requests
196
260
  requestResponseBufferRef.current.forEach(({ request, response }) =>
@@ -207,22 +271,22 @@ export function usePreviewServer({
207
271
  const { headers, url } = message;
208
272
  addCfPreviewTokenHeader(headers, previewToken.value);
209
273
  headers["host"] = previewToken.host;
210
- const localWebsocket = new WebSocket(message, socket, body);
274
+ const localWebsocket = new WebSocket(message, socket, body) as IWebsocket;
211
275
  // TODO(soon): Custom WebSocket protocol is not working?
212
276
  const remoteWebsocketClient = new WebSocket.Client(
213
277
  `wss://${previewToken.host}${url}`,
214
278
  [],
215
279
  { headers }
216
- );
280
+ ) as IWebsocket;
217
281
  localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
218
282
  // We close down websockets whenever we refresh the token.
219
283
  cleanupListeners.push(() => {
220
- localWebsocket.destroy();
221
- remoteWebsocketClient.destroy();
284
+ localWebsocket.close();
285
+ remoteWebsocketClient.close();
222
286
  });
223
287
  };
224
- proxy.on("upgrade", handleUpgrade);
225
- cleanupListeners.push(() => proxy.off("upgrade", handleUpgrade));
288
+ proxy.server.on("upgrade", handleUpgrade);
289
+ cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade));
226
290
 
227
291
  return () => {
228
292
  cleanupListeners.forEach((cleanup) => cleanup());
@@ -231,6 +295,7 @@ export function usePreviewServer({
231
295
  previewToken,
232
296
  publicRoot,
233
297
  port,
298
+ localProtocol,
234
299
  proxy,
235
300
  // We use a state value as a sigil to trigger reconnecting the server.
236
301
  // It's not used inside the effect, so react-hooks/exhaustive-deps
@@ -242,19 +307,37 @@ export function usePreviewServer({
242
307
  // Start/stop the server whenever the
243
308
  // containing component is mounted/unmounted.
244
309
  useEffect(() => {
245
- waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 })
310
+ const abortController = new AbortController();
311
+ if (proxy === undefined) {
312
+ return;
313
+ }
314
+
315
+ waitForPortToBeAvailable(port, {
316
+ retryPeriod: 200,
317
+ timeout: 2000,
318
+ abortSignal: abortController.signal,
319
+ })
246
320
  .then(() => {
247
- proxy.listen(port);
248
- console.log(`⬣ Listening at http://localhost:${port}`);
321
+ proxy.server.on("listening", () => {
322
+ logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
323
+ });
324
+ proxy.server.listen(port, ip);
249
325
  })
250
326
  .catch((err) => {
251
- console.error(`⬣ Failed to start server: ${err}`);
327
+ if ((err as { code: string }).code !== "ABORT_ERR") {
328
+ logger.error(`Failed to start server: ${err}`);
329
+ }
252
330
  });
253
331
 
254
332
  return () => {
255
- proxy.close();
333
+ abortController.abort();
334
+ // Running `proxy.server.close()` does not close open connections, preventing the process from exiting.
335
+ // So we use this `terminator` to close all the connections and force the server to shutdown.
336
+ proxy.terminator
337
+ .terminate()
338
+ .catch(() => logger.error("Failed to terminate the proxy server."));
256
339
  };
257
- }, [port, proxy]);
340
+ }, [port, ip, proxy, localProtocol]);
258
341
  }
259
342
 
260
343
  function createHandleAssetsRequest(
@@ -282,20 +365,18 @@ const HTTP1_HEADERS = new Set([
282
365
  "http2-settings",
283
366
  ]);
284
367
 
285
- function createProxyServer() {
286
- return createServer()
287
- .on("request", function (req, res) {
288
- // log all requests
289
- console.log(
290
- new Date().toLocaleTimeString(),
291
- req.method,
292
- req.url,
293
- res.statusCode
294
- );
295
- })
368
+ async function createProxyServer(
369
+ localProtocol: "https" | "http"
370
+ ): Promise<HttpServer | HttpsServer> {
371
+ const server: HttpServer | HttpsServer =
372
+ localProtocol === "https"
373
+ ? createHttpsServer(await getHttpsOptions())
374
+ : createHttpServer();
375
+
376
+ return server
296
377
  .on("upgrade", (req) => {
297
378
  // log all websocket connections
298
- console.log(
379
+ logger.log(
299
380
  new Date().toLocaleTimeString(),
300
381
  req.method,
301
382
  req.url,
@@ -305,14 +386,15 @@ function createProxyServer() {
305
386
  })
306
387
  .on("error", (err) => {
307
388
  // log all connection errors
308
- console.error(new Date().toLocaleTimeString(), err);
389
+ logger.error(new Date().toLocaleTimeString(), err);
309
390
  });
310
391
  }
311
392
 
312
393
  function createStreamHandler(
313
394
  previewToken: CfPreviewToken,
314
395
  remote: ClientHttp2Session,
315
- port: number
396
+ localPort: number,
397
+ localProtocol: "https" | "http"
316
398
  ) {
317
399
  return function handleStream(
318
400
  stream: ServerHttp2Stream,
@@ -325,7 +407,8 @@ function createStreamHandler(
325
407
  rewriteRemoteHostToLocalHostInHeaders(
326
408
  responseHeaders,
327
409
  previewToken.host,
328
- port
410
+ localPort,
411
+ localProtocol
329
412
  );
330
413
  stream.respond(responseHeaders);
331
414
  request.pipe(stream, { end: true });
@@ -338,33 +421,58 @@ function createStreamHandler(
338
421
  */
339
422
  export async function waitForPortToBeAvailable(
340
423
  port: number,
341
- options: { retryPeriod: number; timeout: number }
424
+ options: { retryPeriod: number; timeout: number; abortSignal: AbortSignal }
342
425
  ): Promise<void> {
343
426
  return new Promise((resolve, reject) => {
427
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
428
+ options.abortSignal.addEventListener("abort", () => {
429
+ const abortError = new Error("waitForPortToBeAvailable() aborted");
430
+ (abortError as Error & { code: string }).code = "ABORT_ERR";
431
+ doReject(abortError);
432
+ });
433
+
344
434
  const timeout = setTimeout(() => {
345
- reject(new Error(`Timed out waiting for port ${port}`));
435
+ doReject(new Error(`Timed out waiting for port ${port}`));
346
436
  }, options.timeout);
347
437
 
438
+ const interval = setInterval(checkPort, options.retryPeriod);
439
+ checkPort();
440
+
441
+ function doResolve() {
442
+ clearTimeout(timeout);
443
+ clearInterval(interval);
444
+ resolve();
445
+ }
446
+
447
+ function doReject(err: unknown) {
448
+ clearInterval(interval);
449
+ clearTimeout(timeout);
450
+ reject(err);
451
+ }
452
+
348
453
  function checkPort() {
349
454
  // Testing whether a port is 'available' involves simply
350
455
  // trying to make a server listen on that port, and retrying
351
456
  // until it succeeds.
352
- const server = createServer();
457
+ const server = createHttpServer();
458
+ const terminator = createHttpTerminator({
459
+ server,
460
+ gracefulTerminationTimeout: 0, // default 1000
461
+ });
462
+
353
463
  server.on("error", (err) => {
354
464
  // @ts-expect-error non standard property on Error
355
- if (err.code === "EADDRINUSE") {
356
- setTimeout(checkPort, options.retryPeriod);
357
- } else {
358
- reject(err);
465
+ if (err.code !== "EADDRINUSE") {
466
+ doReject(err);
359
467
  }
360
468
  });
361
- server.listen(port, () => {
362
- server.close();
363
- clearTimeout(timeout);
364
- resolve();
365
- });
469
+ server.listen(port, () =>
470
+ terminator
471
+ .terminate()
472
+ .then(doResolve, () =>
473
+ logger.error("Failed to terminate the port checker.")
474
+ )
475
+ );
366
476
  }
367
-
368
- checkPort();
369
477
  });
370
478
  }