wrangler 0.0.0-e2a26a0 → 0.0.0-e322475

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