wrangler 0.0.0-e2a26a0 → 0.0.0-e3a458e

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 (84) hide show
  1. package/package.json +24 -11
  2. package/pages/functions/filepath-routing.test.ts +212 -36
  3. package/pages/functions/filepath-routing.ts +69 -119
  4. package/pages/functions/routes.ts +8 -17
  5. package/pages/functions/template-worker.ts +3 -9
  6. package/src/__tests__/configuration.test.ts +2598 -0
  7. package/src/__tests__/dev.test.tsx +437 -63
  8. package/src/__tests__/dev2.test.tsx +154 -0
  9. package/src/__tests__/guess-worker-format.test.ts +58 -0
  10. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  11. package/src/__tests__/helpers/mock-account-id.ts +26 -8
  12. package/src/__tests__/helpers/mock-bin.ts +4 -1
  13. package/src/__tests__/helpers/mock-cfetch.ts +5 -5
  14. package/src/__tests__/helpers/mock-console.ts +21 -7
  15. package/src/__tests__/helpers/mock-kv.ts +9 -16
  16. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  17. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  18. package/src/__tests__/helpers/run-in-tmp.ts +3 -0
  19. package/src/__tests__/helpers/run-wrangler.ts +5 -3
  20. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  21. package/src/__tests__/https-options.test.ts +127 -0
  22. package/src/__tests__/index.test.ts +468 -20
  23. package/src/__tests__/jest.setup.ts +37 -5
  24. package/src/__tests__/kv.test.ts +426 -183
  25. package/src/__tests__/logout.test.ts +15 -13
  26. package/src/__tests__/package-manager.test.ts +2 -2
  27. package/src/__tests__/pages.test.ts +44 -0
  28. package/src/__tests__/parse.test.ts +322 -0
  29. package/src/__tests__/publish.test.ts +1733 -344
  30. package/src/__tests__/r2.test.ts +198 -0
  31. package/src/__tests__/route.test.ts +45 -0
  32. package/src/__tests__/secret.test.ts +301 -86
  33. package/src/__tests__/sentry.test.ts +146 -0
  34. package/src/__tests__/tail.test.ts +564 -0
  35. package/src/__tests__/whoami.test.tsx +21 -7
  36. package/src/bundle.ts +142 -0
  37. package/src/cfetch/index.ts +8 -6
  38. package/src/cfetch/internal.ts +60 -22
  39. package/src/cli.ts +10 -0
  40. package/src/config/README.md +107 -0
  41. package/src/config/config.ts +279 -0
  42. package/src/config/diagnostics.ts +80 -0
  43. package/src/config/environment.ts +305 -0
  44. package/src/config/index.ts +52 -0
  45. package/src/config/validation-helpers.ts +468 -0
  46. package/src/config/validation.ts +1057 -0
  47. package/src/{api/preview.ts → create-worker-preview.ts} +59 -21
  48. package/src/create-worker-upload-form.ts +217 -0
  49. package/src/dev/dev.tsx +362 -0
  50. package/src/dev/local.tsx +247 -0
  51. package/src/dev/remote.tsx +203 -0
  52. package/src/dev/use-esbuild.ts +100 -0
  53. package/src/entry.ts +159 -0
  54. package/src/environment-variables.ts +33 -0
  55. package/src/https-options.ts +122 -0
  56. package/src/index.tsx +1030 -766
  57. package/src/inspect.ts +25 -4
  58. package/src/{kv.tsx → kv.ts} +110 -55
  59. package/src/module-collection.ts +212 -22
  60. package/src/open-in-browser.ts +25 -0
  61. package/src/package-manager.ts +21 -15
  62. package/src/pages.tsx +10 -11
  63. package/src/parse.ts +183 -0
  64. package/src/proxy.ts +115 -45
  65. package/src/publish.ts +273 -329
  66. package/src/r2.ts +50 -0
  67. package/src/reporting.ts +131 -0
  68. package/src/selfsigned.d.ts +29 -0
  69. package/src/sites.tsx +66 -33
  70. package/src/tail/filters.ts +279 -0
  71. package/src/tail/index.ts +299 -0
  72. package/src/tail/printing.ts +62 -0
  73. package/src/user.tsx +223 -124
  74. package/src/{api/worker.ts → worker.ts} +44 -36
  75. package/templates/new-worker.js +1 -1
  76. package/templates/new-worker.ts +1 -1
  77. package/templates/tsconfig.json +105 -0
  78. package/wrangler-dist/cli.js +129900 -113128
  79. package/wrangler-dist/cli.js.map +3 -3
  80. package/src/__tests__/helpers/mock-user.ts +0 -27
  81. package/src/api/form_data.ts +0 -134
  82. package/src/config.ts +0 -641
  83. package/src/dev.tsx +0 -834
  84. package/src/tail.tsx +0 -73
@@ -3,49 +3,51 @@ import { join } from "node:path";
3
3
  import { execa, execaCommandSync } from "execa";
4
4
 
5
5
  export interface PackageManager {
6
+ cwd: string;
7
+ type: "npm" | "yarn";
6
8
  addDevDeps(...packages: string[]): Promise<void>;
7
9
  install(): Promise<void>;
8
10
  }
9
11
 
10
- export async function getPackageManager(root: string): Promise<PackageManager> {
12
+ export async function getPackageManager(cwd: string): Promise<PackageManager> {
11
13
  const [hasYarn, hasNpm] = await Promise.all([supportsYarn(), supportsNpm()]);
12
- const hasYarnLock = existsSync(join(root, "yarn.lock"));
13
- const hasNpmLock = existsSync(join(root, "package-lock.json"));
14
+ const hasYarnLock = existsSync(join(cwd, "yarn.lock"));
15
+ const hasNpmLock = existsSync(join(cwd, "package-lock.json"));
14
16
 
15
17
  if (hasNpmLock) {
16
18
  if (hasNpm) {
17
19
  console.log(
18
20
  "Using npm as package manager, as there is already a package-lock.json file."
19
21
  );
20
- return NpmPackageManager;
22
+ return { ...NpmPackageManager, cwd };
21
23
  } else if (hasYarn) {
22
24
  console.log("Using yarn as package manager.");
23
25
  console.warn(
24
26
  "There is already a package-lock.json file but could not find npm on the PATH."
25
27
  );
26
- return YarnPackageManager;
28
+ return { ...YarnPackageManager, cwd };
27
29
  }
28
30
  } else if (hasYarnLock) {
29
31
  if (hasYarn) {
30
32
  console.log(
31
33
  "Using yarn as package manager, as there is already a yarn.lock file."
32
34
  );
33
- return YarnPackageManager;
35
+ return { ...YarnPackageManager, cwd };
34
36
  } else if (hasNpm) {
35
37
  console.log("Using npm as package manager.");
36
38
  console.warn(
37
39
  "There is already a yarn.lock file but could not find yarn on the PATH."
38
40
  );
39
- return NpmPackageManager;
41
+ return { ...NpmPackageManager, cwd };
40
42
  }
41
43
  }
42
44
 
43
45
  if (hasNpm) {
44
46
  console.log("Using npm as package manager.");
45
- return NpmPackageManager;
47
+ return { ...NpmPackageManager, cwd };
46
48
  } else if (hasYarn) {
47
49
  console.log("Using yarn as package manager.");
48
- return YarnPackageManager;
50
+ return { ...YarnPackageManager, cwd };
49
51
  } else {
50
52
  throw new Error(
51
53
  "Unable to find a package manager. Supported managers are: npm and yarn."
@@ -56,22 +58,21 @@ export async function getPackageManager(root: string): Promise<PackageManager> {
56
58
  /**
57
59
  * Get the name of the given `packageManager`.
58
60
  */
59
- export function getPackageManagerName(packageManager: unknown): string {
60
- return packageManager === NpmPackageManager
61
- ? "npm"
62
- : packageManager === YarnPackageManager
63
- ? "yarn"
64
- : "unknown";
61
+ export function getPackageManagerName(packageManager: PackageManager): string {
62
+ return packageManager.type ?? "unknown";
65
63
  }
66
64
 
67
65
  /**
68
66
  * Manage packages using npm
69
67
  */
70
68
  const NpmPackageManager: PackageManager = {
69
+ cwd: process.cwd(),
70
+ type: "npm",
71
71
  /** Add and install a new devDependency into the local package.json. */
72
72
  async addDevDeps(...packages: string[]): Promise<void> {
73
73
  await execa("npm", ["install", ...packages, "--save-dev"], {
74
74
  stdio: "inherit",
75
+ cwd: this.cwd,
75
76
  });
76
77
  },
77
78
 
@@ -79,6 +80,7 @@ const NpmPackageManager: PackageManager = {
79
80
  async install(): Promise<void> {
80
81
  await execa("npm", ["install"], {
81
82
  stdio: "inherit",
83
+ cwd: this.cwd,
82
84
  });
83
85
  },
84
86
  };
@@ -87,10 +89,13 @@ const NpmPackageManager: PackageManager = {
87
89
  * Manage packages using yarn
88
90
  */
89
91
  const YarnPackageManager: PackageManager = {
92
+ cwd: process.cwd(),
93
+ type: "yarn",
90
94
  /** Add and install a new devDependency into the local package.json. */
91
95
  async addDevDeps(...packages: string[]): Promise<void> {
92
96
  await execa("yarn", ["add", ...packages, "--dev"], {
93
97
  stdio: "inherit",
98
+ cwd: this.cwd,
94
99
  });
95
100
  },
96
101
 
@@ -98,6 +103,7 @@ const YarnPackageManager: PackageManager = {
98
103
  async install(): Promise<void> {
99
104
  await execa("yarn", ["install"], {
100
105
  stdio: "inherit",
106
+ cwd: this.cwd,
101
107
  });
102
108
  },
103
109
  };
package/src/pages.tsx CHANGED
@@ -7,10 +7,10 @@ import { join } from "node:path";
7
7
  import { URL } from "node:url";
8
8
  import { watch } from "chokidar";
9
9
  import { getType } from "mime";
10
- import open from "open";
11
10
  import { buildWorker } from "../pages/functions/buildWorker";
12
11
  import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
13
12
  import { writeRoutesModule } from "../pages/functions/routes";
13
+ import openInBrowser from "./open-in-browser";
14
14
  import { toUrlPath } from "./paths";
15
15
  import type { Config } from "../pages/functions/routes";
16
16
  import type { Headers, Request, fetch } from "@miniflare/core";
@@ -771,7 +771,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
771
771
  kv: kvs = [],
772
772
  do: durableObjects = [],
773
773
  "live-reload": liveReload,
774
- "--": remaining = [],
774
+ _: [_pages, _dev, ...remaining],
775
775
  }) => {
776
776
  if (!local) {
777
777
  console.error("Only local mode is supported at the moment.");
@@ -795,8 +795,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
795
795
 
796
796
  let miniflareArgs: MiniflareOptions = {};
797
797
 
798
- let scriptReadyResolve;
799
- const scriptReadyPromise = new Promise(
798
+ let scriptReadyResolve: () => void;
799
+ const scriptReadyPromise = new Promise<void>(
800
800
  (resolve) => (scriptReadyResolve = resolve)
801
801
  );
802
802
 
@@ -830,7 +830,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
830
830
  scriptPath,
831
831
  };
832
832
  } else {
833
- scriptReadyResolve();
833
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
834
+ scriptReadyResolve!();
834
835
 
835
836
  const scriptPath =
836
837
  directory !== undefined
@@ -894,13 +895,13 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
894
895
  // User bindings
895
896
  bindings: {
896
897
  ...Object.fromEntries(
897
- bindings.map((binding) => binding.toString().split("="))
898
+ bindings.map((binding) => binding.toString().split("=", 1))
898
899
  ),
899
900
  },
900
901
 
901
902
  // env.ASSETS.fetch
902
903
  serviceBindings: {
903
- async ASSETS(request) {
904
+ async ASSETS(request: Request) {
904
905
  if (proxyPort) {
905
906
  try {
906
907
  const url = new URL(request.url);
@@ -945,9 +946,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
945
946
  console.log(`Serving at http://localhost:${port}/`);
946
947
 
947
948
  if (process.env.BROWSER !== "none") {
948
- const childProcess = await open(`http://localhost:${port}/`);
949
- // fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
950
- childProcess.on("error", (_err) => {});
949
+ await openInBrowser(`http://localhost:${port}/`);
951
950
  }
952
951
 
953
952
  if (directory !== undefined && liveReload) {
@@ -964,7 +963,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
964
963
  miniflare.dispose().catch((err) => miniflare.log.error(err));
965
964
  });
966
965
  } catch (e) {
967
- miniflare.log.error(e);
966
+ miniflare.log.error(e as Error);
968
967
  EXIT("Could not start Miniflare.", 1);
969
968
  }
970
969
  }
package/src/parse.ts ADDED
@@ -0,0 +1,183 @@
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
+ /* eslint-disable @typescript-eslint/no-explicit-any */
66
+
67
+ const TOML_ERROR_NAME = "TomlError";
68
+ const TOML_ERROR_SUFFIX = " at row ";
69
+
70
+ type TomlError = Error & {
71
+ line: number;
72
+ col: number;
73
+ };
74
+
75
+ /**
76
+ * A wrapper around `TOML.parse` that throws a `ParseError`.
77
+ */
78
+ export function parseTOML(input: string, file?: string): object {
79
+ try {
80
+ return TOML.parse(input);
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 wrapper around `JSON.parse` that throws a `ParseError`.
103
+ */
104
+ export function parseJSON(input: string, file?: string): any {
105
+ try {
106
+ return JSON.parse(input);
107
+ } catch (err) {
108
+ const { message } = err as Error;
109
+ const index = message.lastIndexOf(JSON_ERROR_SUFFIX);
110
+ if (index < 0) {
111
+ throw err;
112
+ }
113
+ const text = message.substring(0, index);
114
+ const position = parseInt(
115
+ message.substring(index + JSON_ERROR_SUFFIX.length)
116
+ );
117
+ const location = indexLocation({ file, fileText: input }, position);
118
+ throw new ParseError({ text, location });
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Reads a file and parses it based on its type.
124
+ */
125
+ export function readFileSync(file: string): string {
126
+ try {
127
+ return fs.readFileSync(file, { encoding: "utf-8" });
128
+ } catch (err) {
129
+ const { message } = err as Error;
130
+ throw new ParseError({
131
+ text: `Could not read file: ${file}`,
132
+ notes: [
133
+ {
134
+ text: message.replace(file, resolve(file)),
135
+ },
136
+ ],
137
+ });
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Calculates the line and column location from an index.
143
+ */
144
+ export function indexLocation(file: File, index: number): Location {
145
+ let lineText,
146
+ line = 0,
147
+ column = 0,
148
+ cursor = 0;
149
+ const { fileText = "" } = file;
150
+ for (const row of fileText.split("\n")) {
151
+ line++;
152
+ cursor += row.length + 1;
153
+ if (cursor >= index) {
154
+ lineText = row;
155
+ column = row.length - (cursor - index);
156
+ break;
157
+ }
158
+ }
159
+ return { lineText, line, column, ...file };
160
+ }
161
+
162
+ /**
163
+ * Guesses the line and column location of a search query.
164
+ */
165
+ export function searchLocation(file: File, query: unknown): Location {
166
+ let lineText,
167
+ length,
168
+ line = 0,
169
+ column = 0;
170
+ const queryText = String(query);
171
+ const { fileText = "" } = file;
172
+ for (const content of fileText.split("\n")) {
173
+ line++;
174
+ const index = content.indexOf(queryText);
175
+ if (index >= 0) {
176
+ lineText = content;
177
+ column = index;
178
+ length = queryText.length;
179
+ break;
180
+ }
181
+ }
182
+ return { lineText, line, column, length, ...file };
183
+ }
package/src/proxy.ts CHANGED
@@ -1,17 +1,27 @@
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";
4
5
  import { useEffect, useRef, useState } from "react";
5
6
  import serveStatic from "serve-static";
6
- import type { CfPreviewToken } from "./api/preview";
7
+ import { getHttpsOptions } from "./https-options";
8
+ import { reportError } from "./reporting";
9
+ import type { CfPreviewToken } from "./create-worker-preview";
7
10
  import type {
8
11
  IncomingHttpHeaders,
9
12
  RequestListener,
10
13
  IncomingMessage,
11
14
  ServerResponse,
12
- Server,
15
+ Server as HttpServer,
13
16
  } from "node:http";
14
17
  import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2";
18
+ import type { Server as HttpsServer } from "node:https";
19
+ import type ws from "ws";
20
+
21
+ interface IWebsocket extends ws {
22
+ // Pipe implements .on("message", ...)
23
+ pipe<T>(fn: T): IWebsocket;
24
+ }
15
25
 
16
26
  /**
17
27
  * `usePreviewServer` is a React hook that creates a local development
@@ -41,13 +51,17 @@ function addCfPreviewTokenHeader(
41
51
  function rewriteRemoteHostToLocalHostInHeaders(
42
52
  headers: IncomingHttpHeaders,
43
53
  remoteHost: string,
44
- localPort: number
54
+ localPort: number,
55
+ localProtocol: "https" | "http"
45
56
  ) {
46
57
  for (const [name, value] of Object.entries(headers)) {
47
58
  // Rewrite the remote host to the local host.
48
59
  if (typeof value === "string" && value.includes(remoteHost)) {
49
60
  headers[name] = value
50
- .replaceAll(`https://${remoteHost}`, `http://localhost:${localPort}`)
61
+ .replaceAll(
62
+ `https://${remoteHost}`,
63
+ `${localProtocol}://localhost:${localPort}`
64
+ )
51
65
  .replaceAll(remoteHost, `localhost:${localPort}`);
52
66
  }
53
67
  }
@@ -56,15 +70,33 @@ function rewriteRemoteHostToLocalHostInHeaders(
56
70
  export function usePreviewServer({
57
71
  previewToken,
58
72
  publicRoot,
59
- port,
73
+ localProtocol,
74
+ localPort: port,
75
+ ip,
60
76
  }: {
61
77
  previewToken: CfPreviewToken | undefined;
62
78
  publicRoot: undefined | string;
63
- port: number;
79
+ localProtocol: "https" | "http";
80
+ localPort: number;
81
+ ip: string;
64
82
  }) {
65
83
  /** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
66
- const proxyServer = useRef<Server>();
67
- const proxy = (proxyServer.current ??= createProxyServer());
84
+ const [proxyServer, setProxyServer] = useState<HttpServer | HttpsServer>();
85
+
86
+ /**
87
+ * Create the instance of the local proxy server that will pass on
88
+ * requests to the preview worker.
89
+ */
90
+ useEffect(() => {
91
+ if (proxyServer === undefined) {
92
+ createProxyServer(localProtocol)
93
+ .then((proxy) => setProxyServer(proxy))
94
+ .catch(async (err) => {
95
+ console.error("Failed to create proxy server.", err);
96
+ await reportError(err);
97
+ });
98
+ }
99
+ }, [proxyServer, localProtocol]);
68
100
 
69
101
  /**
70
102
  * When we're not connected / getting a fresh token on changes,
@@ -81,7 +113,7 @@ export function usePreviewServer({
81
113
  >([]);
82
114
 
83
115
  /**
84
- * The session doesn't last forever, and will evetually drop
116
+ * The session doesn't last forever, and will eventually drop
85
117
  * (usually within 5-15 minutes). When that happens, we simply
86
118
  * restart the effect, effectively restarting the server. We use
87
119
  * a state sigil as an effect dependency to do so.
@@ -92,6 +124,10 @@ export function usePreviewServer({
92
124
  }
93
125
 
94
126
  useEffect(() => {
127
+ if (proxyServer === undefined) {
128
+ return;
129
+ }
130
+
95
131
  // If we don't have a token, that means either we're just starting up,
96
132
  // or we're refreshing the token.
97
133
  if (!previewToken) {
@@ -103,8 +139,8 @@ export function usePreviewServer({
103
139
  // store the stream in a buffer so we can replay it later
104
140
  streamBufferRef.current.push({ stream, headers });
105
141
  };
106
- proxy.on("stream", bufferStream);
107
- cleanupListeners.push(() => proxy.off("stream", bufferStream));
142
+ proxyServer.on("stream", bufferStream);
143
+ cleanupListeners.push(() => proxyServer.off("stream", bufferStream));
108
144
 
109
145
  const bufferRequestResponse = (
110
146
  request: IncomingMessage,
@@ -114,8 +150,10 @@ export function usePreviewServer({
114
150
  requestResponseBufferRef.current.push({ request, response });
115
151
  };
116
152
 
117
- proxy.on("request", bufferRequestResponse);
118
- cleanupListeners.push(() => proxy.off("request", bufferRequestResponse));
153
+ proxyServer.on("request", bufferRequestResponse);
154
+ cleanupListeners.push(() =>
155
+ proxyServer.off("request", bufferRequestResponse)
156
+ );
119
157
  return () => {
120
158
  cleanupListeners.forEach((cleanup) => cleanup());
121
159
  };
@@ -136,9 +174,14 @@ export function usePreviewServer({
136
174
  cleanupListeners.push(() => remote.off("close", retryServerSetup));
137
175
 
138
176
  /** HTTP/2 -> HTTP/2 */
139
- const handleStream = createStreamHandler(previewToken, remote, port);
140
- proxy.on("stream", handleStream);
141
- cleanupListeners.push(() => proxy.off("stream", handleStream));
177
+ const handleStream = createStreamHandler(
178
+ previewToken,
179
+ remote,
180
+ port,
181
+ localProtocol
182
+ );
183
+ proxyServer.on("stream", handleStream);
184
+ cleanupListeners.push(() => proxyServer.off("stream", handleStream));
142
185
 
143
186
  // flush and replay buffered streams
144
187
  streamBufferRef.current.forEach((buffer) =>
@@ -171,7 +214,8 @@ export function usePreviewServer({
171
214
  rewriteRemoteHostToLocalHostInHeaders(
172
215
  responseHeaders,
173
216
  previewToken.host,
174
- port
217
+ port,
218
+ localProtocol
175
219
  );
176
220
  for (const name of Object.keys(responseHeaders)) {
177
221
  if (name.startsWith(":")) {
@@ -189,8 +233,10 @@ export function usePreviewServer({
189
233
  ? createHandleAssetsRequest(assetPath, handleRequest)
190
234
  : handleRequest;
191
235
 
192
- proxy.on("request", actualHandleRequest);
193
- cleanupListeners.push(() => proxy.off("request", actualHandleRequest));
236
+ proxyServer.on("request", actualHandleRequest);
237
+ cleanupListeners.push(() =>
238
+ proxyServer.off("request", actualHandleRequest)
239
+ );
194
240
 
195
241
  // flush and replay buffered requests
196
242
  requestResponseBufferRef.current.forEach(({ request, response }) =>
@@ -207,22 +253,22 @@ export function usePreviewServer({
207
253
  const { headers, url } = message;
208
254
  addCfPreviewTokenHeader(headers, previewToken.value);
209
255
  headers["host"] = previewToken.host;
210
- const localWebsocket = new WebSocket(message, socket, body);
256
+ const localWebsocket = new WebSocket(message, socket, body) as IWebsocket;
211
257
  // TODO(soon): Custom WebSocket protocol is not working?
212
258
  const remoteWebsocketClient = new WebSocket.Client(
213
259
  `wss://${previewToken.host}${url}`,
214
260
  [],
215
261
  { headers }
216
- );
262
+ ) as IWebsocket;
217
263
  localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket);
218
264
  // We close down websockets whenever we refresh the token.
219
265
  cleanupListeners.push(() => {
220
- localWebsocket.destroy();
221
- remoteWebsocketClient.destroy();
266
+ localWebsocket.close();
267
+ remoteWebsocketClient.close();
222
268
  });
223
269
  };
224
- proxy.on("upgrade", handleUpgrade);
225
- cleanupListeners.push(() => proxy.off("upgrade", handleUpgrade));
270
+ proxyServer.on("upgrade", handleUpgrade);
271
+ cleanupListeners.push(() => proxyServer.off("upgrade", handleUpgrade));
226
272
 
227
273
  return () => {
228
274
  cleanupListeners.forEach((cleanup) => cleanup());
@@ -231,7 +277,8 @@ export function usePreviewServer({
231
277
  previewToken,
232
278
  publicRoot,
233
279
  port,
234
- proxy,
280
+ localProtocol,
281
+ proxyServer,
235
282
  // We use a state value as a sigil to trigger reconnecting the server.
236
283
  // It's not used inside the effect, so react-hooks/exhaustive-deps
237
284
  // doesn't complain if it's not included in the dependency array.
@@ -242,19 +289,23 @@ export function usePreviewServer({
242
289
  // Start/stop the server whenever the
243
290
  // containing component is mounted/unmounted.
244
291
  useEffect(() => {
292
+ if (proxyServer === undefined) {
293
+ return;
294
+ }
295
+
245
296
  waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 })
246
297
  .then(() => {
247
- proxy.listen(port);
248
- console.log(`⬣ Listening at http://localhost:${port}`);
298
+ proxyServer.listen(port, ip);
299
+ console.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
249
300
  })
250
301
  .catch((err) => {
251
302
  console.error(`⬣ Failed to start server: ${err}`);
252
303
  });
253
304
 
254
305
  return () => {
255
- proxy.close();
306
+ proxyServer.close();
256
307
  };
257
- }, [port, proxy]);
308
+ }, [port, ip, proxyServer, localProtocol]);
258
309
  }
259
310
 
260
311
  function createHandleAssetsRequest(
@@ -282,8 +333,15 @@ const HTTP1_HEADERS = new Set([
282
333
  "http2-settings",
283
334
  ]);
284
335
 
285
- function createProxyServer() {
286
- return createServer()
336
+ async function createProxyServer(
337
+ localProtocol: "https" | "http"
338
+ ): Promise<HttpServer | HttpsServer> {
339
+ const server: HttpServer | HttpsServer =
340
+ localProtocol === "https"
341
+ ? createHttpsServer(await getHttpsOptions())
342
+ : createHttpServer();
343
+
344
+ return server
287
345
  .on("request", function (req, res) {
288
346
  // log all requests
289
347
  console.log(
@@ -312,7 +370,8 @@ function createProxyServer() {
312
370
  function createStreamHandler(
313
371
  previewToken: CfPreviewToken,
314
372
  remote: ClientHttp2Session,
315
- port: number
373
+ localPort: number,
374
+ localProtocol: "https" | "http"
316
375
  ) {
317
376
  return function handleStream(
318
377
  stream: ServerHttp2Stream,
@@ -325,7 +384,8 @@ function createStreamHandler(
325
384
  rewriteRemoteHostToLocalHostInHeaders(
326
385
  responseHeaders,
327
386
  previewToken.host,
328
- port
387
+ localPort,
388
+ localProtocol
329
389
  );
330
390
  stream.respond(responseHeaders);
331
391
  request.pipe(stream, { end: true });
@@ -342,29 +402,39 @@ export async function waitForPortToBeAvailable(
342
402
  ): Promise<void> {
343
403
  return new Promise((resolve, reject) => {
344
404
  const timeout = setTimeout(() => {
345
- reject(new Error(`Timed out waiting for port ${port}`));
405
+ doReject(new Error(`Timed out waiting for port ${port}`));
346
406
  }, options.timeout);
347
407
 
408
+ const interval = setInterval(checkPort, options.retryPeriod);
409
+ checkPort();
410
+
411
+ function doResolve() {
412
+ clearTimeout(timeout);
413
+ clearInterval(interval);
414
+ resolve();
415
+ }
416
+
417
+ function doReject(err: unknown) {
418
+ clearInterval(interval);
419
+ clearTimeout(timeout);
420
+ reject(err);
421
+ }
422
+
348
423
  function checkPort() {
349
424
  // Testing whether a port is 'available' involves simply
350
425
  // trying to make a server listen on that port, and retrying
351
426
  // until it succeeds.
352
- const server = createServer();
427
+ const server = createHttpServer();
353
428
  server.on("error", (err) => {
354
429
  // @ts-expect-error non standard property on Error
355
- if (err.code === "EADDRINUSE") {
356
- setTimeout(checkPort, options.retryPeriod);
357
- } else {
358
- reject(err);
430
+ if (err.code !== "EADDRINUSE") {
431
+ doReject(err);
359
432
  }
360
433
  });
361
434
  server.listen(port, () => {
362
435
  server.close();
363
- clearTimeout(timeout);
364
- resolve();
436
+ doResolve();
365
437
  });
366
438
  }
367
-
368
- checkPort();
369
439
  });
370
440
  }