wrangler 0.0.0-e043479 → 0.0.0-e0b9366

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 (51) hide show
  1. package/package.json +10 -4
  2. package/pages/functions/filepath-routing.test.ts +125 -25
  3. package/pages/functions/filepath-routing.ts +6 -3
  4. package/src/__tests__/configuration.test.ts +479 -114
  5. package/src/__tests__/dev.test.tsx +162 -8
  6. package/src/__tests__/dev2.test.tsx +152 -0
  7. package/src/__tests__/guess-worker-format.test.ts +9 -17
  8. package/src/__tests__/helpers/mock-account-id.ts +4 -0
  9. package/src/__tests__/helpers/mock-cfetch.ts +3 -3
  10. package/src/__tests__/helpers/mock-kv.ts +9 -16
  11. package/src/__tests__/helpers/run-in-tmp.ts +3 -0
  12. package/src/__tests__/https-options.test.ts +127 -0
  13. package/src/__tests__/index.test.ts +125 -0
  14. package/src/__tests__/jest.setup.ts +1 -1
  15. package/src/__tests__/kv.test.ts +27 -53
  16. package/src/__tests__/logout.test.ts +15 -13
  17. package/src/__tests__/pages.test.ts +44 -0
  18. package/src/__tests__/publish.test.ts +504 -32
  19. package/src/__tests__/route.test.ts +45 -0
  20. package/src/__tests__/whoami.test.tsx +21 -7
  21. package/src/bundle.ts +20 -11
  22. package/src/cfetch/internal.ts +11 -5
  23. package/src/config/config.ts +53 -44
  24. package/src/config/validation-helpers.ts +110 -12
  25. package/src/config/validation.ts +59 -41
  26. package/src/{api/preview.ts → create-worker-preview.ts} +23 -4
  27. package/src/{api/form_data.ts → create-worker-upload-form.ts} +1 -1
  28. package/src/dev/dev.tsx +357 -0
  29. package/src/dev/local.tsx +241 -0
  30. package/src/dev/remote.tsx +201 -0
  31. package/src/dev/use-esbuild.ts +100 -0
  32. package/src/entry.ts +111 -9
  33. package/src/https-options.ts +122 -0
  34. package/src/index.tsx +171 -109
  35. package/src/inspect.ts +9 -1
  36. package/src/{kv.tsx → kv.ts} +0 -0
  37. package/src/module-collection.ts +85 -2
  38. package/src/open-in-browser.ts +14 -2
  39. package/src/pages.tsx +1 -1
  40. package/src/proxy.ts +102 -40
  41. package/src/publish.ts +41 -50
  42. package/src/reporting.ts +25 -9
  43. package/src/selfsigned.d.ts +29 -0
  44. package/src/sites.tsx +51 -16
  45. package/src/user.tsx +132 -77
  46. package/src/{api/worker.ts → worker.ts} +0 -23
  47. package/wrangler-dist/cli.js +5012 -3107
  48. package/wrangler-dist/cli.js.map +3 -3
  49. package/src/__tests__/helpers/mock-user.ts +0 -27
  50. package/src/dev.tsx +0 -960
  51. package/src/guess-worker-format.ts +0 -68
@@ -19,8 +19,17 @@ import {
19
19
  validateOptionalTypedArray,
20
20
  validateRequiredProperty,
21
21
  validateTypedArray,
22
+ all,
23
+ isMutuallyExclusiveWith,
24
+ inheritableInLegacyEnvironments,
22
25
  } from "./validation-helpers";
23
- import type { Config, DeprecatedUpload, RawConfig } from "./config";
26
+ import type {
27
+ Config,
28
+ DeprecatedUpload,
29
+ DevConfig,
30
+ RawConfig,
31
+ RawDevConfig,
32
+ } from "./config";
24
33
  import type { RawEnvironment, Environment, Rule } from "./environment";
25
34
  import type { ValidatorFn } from "./validation-helpers";
26
35
 
@@ -61,6 +70,15 @@ export function normalizeAndValidateConfig(
61
70
  true
62
71
  );
63
72
 
73
+ deprecated(
74
+ diagnostics,
75
+ rawConfig,
76
+ `site.entry-point`,
77
+ `The \`site.entry-point\` config field is no longer used.\nThe entry-point should be specified via the command line or the \`main\` config field.`,
78
+ false,
79
+ true
80
+ );
81
+
64
82
  const { deprecatedUpload, ...build } = normalizeAndValidateBuild(
65
83
  diagnostics,
66
84
  rawConfig,
@@ -136,7 +154,7 @@ export function normalizeAndValidateConfig(
136
154
  diagnostics,
137
155
  "top-level",
138
156
  Object.keys(rawConfig),
139
- Object.keys(config)
157
+ [...Object.keys(config), "miniflare"]
140
158
  );
141
159
 
142
160
  return { config, diagnostics };
@@ -222,7 +240,7 @@ function normalizeAndValidateMainField(
222
240
  return path.resolve(directory, rawMain);
223
241
  } else {
224
242
  diagnostics.errors.push(
225
- `ERROR: "main" should be a string but got ${JSON.stringify(rawMain)}`
243
+ `Expected "main" to be a string but got ${JSON.stringify(rawMain)}`
226
244
  );
227
245
  return;
228
246
  }
@@ -239,8 +257,8 @@ function normalizeAndValidateMainField(
239
257
  */
240
258
  function normalizeAndValidateDev(
241
259
  diagnostics: Diagnostics,
242
- rawDev: Config["dev"]
243
- ): Config["dev"] {
260
+ rawDev: RawDevConfig
261
+ ): DevConfig {
244
262
  const {
245
263
  ip = "127.0.0.1",
246
264
  port = 8787,
@@ -258,14 +276,16 @@ function normalizeAndValidateDev(
258
276
  "dev",
259
277
  "local_protocol",
260
278
  local_protocol,
261
- "string"
279
+ "string",
280
+ ["http", "https"]
262
281
  );
263
282
  validateOptionalProperty(
264
283
  diagnostics,
265
284
  "dev",
266
285
  "upstream_protocol",
267
286
  upstream_protocol,
268
- "string"
287
+ "string",
288
+ ["http", "https"]
269
289
  );
270
290
  validateOptionalProperty(diagnostics, "dev", "host", host, "string");
271
291
  return { ip, port, local_protocol, upstream_protocol, host };
@@ -304,7 +324,7 @@ function normalizeAndValidateMigrations(
304
324
  if (migration.renamed_classes !== undefined) {
305
325
  if (!Array.isArray(migration.renamed_classes)) {
306
326
  diagnostics.errors.push(
307
- `"migrations[${i}].renamed_classes" should be an array of "{from: string, to: string}" objects but got ${JSON.stringify(
327
+ `Expected "migrations[${i}].renamed_classes" to be an array of "{from: string, to: string}" objects but got ${JSON.stringify(
308
328
  migration.renamed_classes
309
329
  )}.`
310
330
  );
@@ -317,7 +337,7 @@ function normalizeAndValidateMigrations(
317
337
  )
318
338
  ) {
319
339
  diagnostics.errors.push(
320
- `"migrations[${i}].renamed_classes" should be an array of "{from: string, to: string}" objects but got ${JSON.stringify(
340
+ `Expected "migrations[${i}].renamed_classes" to be an array of "{from: string, to: string}" objects but got ${JSON.stringify(
321
341
  migration.renamed_classes
322
342
  )}.`
323
343
  );
@@ -342,25 +362,12 @@ function normalizeAndValidateSite(
342
362
  rawSite: Config["site"]
343
363
  ): Config["site"] {
344
364
  if (rawSite !== undefined) {
345
- const {
346
- bucket,
347
- "entry-point": entryPoint,
348
- include = [],
349
- exclude = [],
350
- ...rest
351
- } = rawSite;
365
+ const { bucket, include = [], exclude = [], ...rest } = rawSite;
352
366
  validateAdditionalProperties(diagnostics, "site", Object.keys(rest), []);
353
367
  validateRequiredProperty(diagnostics, "site", "bucket", bucket, "string");
354
- validateOptionalProperty(
355
- diagnostics,
356
- "site",
357
- "entry_point",
358
- entryPoint,
359
- "string"
360
- );
361
368
  validateTypedArray(diagnostics, "sites.include", include, "string");
362
369
  validateTypedArray(diagnostics, "sites.exclude", exclude, "string");
363
- return { bucket, "entry-point": entryPoint, include, exclude };
370
+ return { bucket, include, exclude };
364
371
  }
365
372
  return undefined;
366
373
  }
@@ -470,7 +477,7 @@ function normalizeAndValidateEnvironment(
470
477
  config,
471
478
  rawEnv,
472
479
  "routes",
473
- isStringArray,
480
+ all(isStringArray, isMutuallyExclusiveWith(rawEnv, "route")),
474
481
  undefined
475
482
  );
476
483
  const workers_dev = inheritable(
@@ -531,7 +538,14 @@ function normalizeAndValidateEnvironment(
531
538
  deprecatedRules,
532
539
  envName
533
540
  ),
534
- name: inheritable(diagnostics, config, rawEnv, "name", isString, undefined),
541
+ name: inheritableInLegacyEnvironments(
542
+ diagnostics,
543
+ config,
544
+ rawEnv,
545
+ "name",
546
+ isString,
547
+ undefined
548
+ ),
535
549
  route,
536
550
  routes,
537
551
  triggers: inheritable(
@@ -623,7 +637,7 @@ const validateAndNormalizeRules = (
623
637
  // Only create errors/warnings for the top-level environment
624
638
  if (rawEnv.rules && deprecatedRules) {
625
639
  diagnostics.errors.push(
626
- `ERROR: You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.`
640
+ `You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.`
627
641
  );
628
642
  } else if (deprecatedRules) {
629
643
  diagnostics.warnings.push(
@@ -673,7 +687,9 @@ const validateRules =
673
687
 
674
688
  const validateRule: ValidatorFn = (diagnostics, field, value) => {
675
689
  if (typeof value !== "object" || value === null) {
676
- diagnostics.errors.push(`ERROR: should be an object.`);
690
+ diagnostics.errors.push(
691
+ `"${field}" should be an object but got ${JSON.stringify(value)}.`
692
+ );
677
693
  return false;
678
694
  }
679
695
  // Rules must have a type string and glob string array, and optionally a fallthrough boolean.
@@ -690,7 +706,7 @@ const validateRule: ValidatorFn = (diagnostics, field, value) => {
690
706
  ])
691
707
  ) {
692
708
  diagnostics.errors.push(
693
- `ERROR: bindings should have a string "type" field, which contains one of "ESModule", "CommonJS", "CompiledWasm", "Text", or "Data".`
709
+ `bindings should have a string "type" field, which contains one of "ESModule", "CommonJS", "CompiledWasm", "Text", or "Data".`
694
710
  );
695
711
  isValid = false;
696
712
  }
@@ -701,7 +717,7 @@ const validateRule: ValidatorFn = (diagnostics, field, value) => {
701
717
 
702
718
  if (!isOptionalProperty(rule, "fallthrough", "boolean")) {
703
719
  diagnostics.errors.push(
704
- `ERROR: should, optionally, have a boolean "fallthrough" field.`
720
+ `binding should, optionally, have a boolean "fallthrough" field.`
705
721
  );
706
722
  isValid = false;
707
723
  }
@@ -820,29 +836,29 @@ const getBindingNames = (value: unknown): string[] =>
820
836
  */
821
837
  const validateDurableObjectBinding: ValidatorFn = (
822
838
  diagnostics,
823
- _field,
839
+ field,
824
840
  value
825
841
  ) => {
826
842
  if (typeof value !== "object" || value === null) {
827
- diagnostics.errors.push(`ERROR: should be an object.`);
843
+ diagnostics.errors.push(
844
+ `Expected "${field}" to be an object but got ${JSON.stringify(value)}`
845
+ );
828
846
  return false;
829
847
  }
830
848
 
831
849
  // Durable Object bindings must have a name and class_name, and optionally a script_name.
832
850
  let isValid = true;
833
851
  if (!isRequiredProperty(value, "name", "string")) {
834
- diagnostics.errors.push(
835
- `ERROR: bindings should have a string "name" field.`
836
- );
852
+ diagnostics.errors.push(`binding should have a string "name" field.`);
837
853
  isValid = false;
838
854
  }
839
855
  if (!isRequiredProperty(value, "class_name", "string")) {
840
- diagnostics.errors.push(`ERROR: should have a string "class_name" field.`);
856
+ diagnostics.errors.push(`binding should have a string "class_name" field.`);
841
857
  isValid = false;
842
858
  }
843
859
  if (!isOptionalProperty(value, "script_name", "string")) {
844
860
  diagnostics.errors.push(
845
- `ERROR: should, optionally, have a string "script_name" field.`
861
+ `binding should, optionally, have a string "script_name" field.`
846
862
  );
847
863
  isValid = false;
848
864
  }
@@ -855,16 +871,18 @@ const validateDurableObjectBinding: ValidatorFn = (
855
871
  *
856
872
  * TODO: further validation of known unsafe bindings.
857
873
  */
858
- const validateUnsafeBinding: ValidatorFn = (diagnostics, _field, value) => {
874
+ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
859
875
  if (typeof value !== "object" || value === null) {
860
- diagnostics.errors.push(`ERROR: should be an object.`);
876
+ diagnostics.errors.push(
877
+ `Expected ${field} to be an object but got ${JSON.stringify(value)}.`
878
+ );
861
879
  return false;
862
880
  }
863
881
 
864
882
  let isValid = true;
865
883
  // Unsafe bindings must have a name and type.
866
884
  if (!isRequiredProperty(value, "name", "string")) {
867
- diagnostics.errors.push(`ERROR: should have a string "name" field.`);
885
+ diagnostics.errors.push(`binding should have a string "name" field.`);
868
886
  isValid = false;
869
887
  }
870
888
  if (isRequiredProperty(value, "type", "string")) {
@@ -883,7 +901,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, _field, value) => {
883
901
  );
884
902
  }
885
903
  } else {
886
- diagnostics.errors.push(`ERROR: should have a string "type" field.`);
904
+ diagnostics.errors.push(`binding should have a string "type" field.`);
887
905
  isValid = false;
888
906
  }
889
907
  return isValid;
@@ -1,7 +1,7 @@
1
1
  import { URL } from "node:url";
2
2
  import { fetch } from "undici";
3
- import { fetchResult } from "../cfetch";
4
- import { toFormData } from "./form_data";
3
+ import { fetchResult } from "./cfetch";
4
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
5
5
  import type { CfAccount, CfWorkerContext, CfWorkerInit } from "./worker";
6
6
 
7
7
  /**
@@ -93,7 +93,7 @@ function randomId(): string {
93
93
  /**
94
94
  * Creates a preview token.
95
95
  */
96
- export async function previewToken(
96
+ async function createPreviewToken(
97
97
  account: CfAccount,
98
98
  worker: CfWorkerInit,
99
99
  ctx: CfWorkerContext
@@ -114,7 +114,7 @@ export async function previewToken(
114
114
  ? { routes: ["*/*"] } // TODO: should we support routes here? how?
115
115
  : { workers_dev: true };
116
116
 
117
- const formData = toFormData(worker);
117
+ const formData = createWorkerUploadForm(worker);
118
118
  formData.set("wrangler-session-config", JSON.stringify(mode));
119
119
 
120
120
  const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
@@ -145,3 +145,22 @@ export async function previewToken(
145
145
  prewarmUrl,
146
146
  };
147
147
  }
148
+
149
+ /**
150
+ * A stub to create a Cloudflare Worker preview.
151
+ *
152
+ * @example
153
+ * const {value, host} = await createWorker(init, acct);
154
+ */
155
+ export async function createWorkerPreview(
156
+ init: CfWorkerInit,
157
+ account: CfAccount,
158
+ ctx: CfWorkerContext
159
+ ): Promise<CfPreviewToken> {
160
+ const token = await createPreviewToken(account, init, ctx);
161
+ const response = await fetch(token.prewarmUrl.href, { method: "POST" });
162
+ if (!response.ok) {
163
+ // console.error("worker failed to prewarm: ", response.statusText);
164
+ }
165
+ return token;
166
+ }
@@ -51,7 +51,7 @@ export interface WorkerMetadata {
51
51
  /**
52
52
  * Creates a `FormData` upload from a `CfWorkerInit`.
53
53
  */
54
- export function toFormData(worker: CfWorkerInit): FormData {
54
+ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
55
55
  const formData = new FormData();
56
56
  const {
57
57
  main,
@@ -0,0 +1,357 @@
1
+ import { spawn } from "node:child_process";
2
+ import { watch } from "chokidar";
3
+ import clipboardy from "clipboardy";
4
+ import commandExists from "command-exists";
5
+ import { Box, Text, useApp, useInput } from "ink";
6
+ import React, { useState, useEffect, useRef } from "react";
7
+ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
8
+ import onExit from "signal-exit";
9
+ import tmp from "tmp-promise";
10
+ import { fetch } from "undici";
11
+ import { runCustomBuild } from "../entry";
12
+ import openInBrowser from "../open-in-browser";
13
+ import { reportError } from "../reporting";
14
+ import { getAPIToken } from "../user";
15
+ import { Local } from "./local";
16
+ import { Remote } from "./remote";
17
+ import { useEsbuild } from "./use-esbuild";
18
+ import type { Config } from "../config";
19
+ import type { Entry } from "../entry";
20
+ import type { AssetPaths } from "../sites";
21
+ import type { CfWorkerInit } from "../worker";
22
+
23
+ export type DevProps = {
24
+ name?: string;
25
+ entry: Entry;
26
+ port?: number;
27
+ inspectorPort: number;
28
+ rules: Config["rules"];
29
+ accountId: undefined | string;
30
+ initialMode: "local" | "remote";
31
+ jsxFactory: undefined | string;
32
+ jsxFragment: undefined | string;
33
+ upstreamProtocol: "https" | "http";
34
+ localProtocol: "https" | "http";
35
+ enableLocalPersistence: boolean;
36
+ bindings: CfWorkerInit["bindings"];
37
+ public: undefined | string;
38
+ assetPaths: undefined | AssetPaths;
39
+ compatibilityDate: undefined | string;
40
+ compatibilityFlags: undefined | string[];
41
+ usageModel: undefined | "bundled" | "unbound";
42
+ buildCommand: {
43
+ command?: undefined | string;
44
+ cwd?: undefined | string;
45
+ watch_dir?: undefined | string;
46
+ };
47
+ env: string | undefined;
48
+ legacyEnv: boolean;
49
+ zone:
50
+ | {
51
+ id: string;
52
+ host: string;
53
+ }
54
+ | undefined;
55
+ };
56
+
57
+ export function DevImplementation(props: DevProps): JSX.Element {
58
+ const port = props.port ?? 8787;
59
+ const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
60
+ const directory = useTmpDir();
61
+
62
+ useCustomBuild(props.entry, props.buildCommand);
63
+
64
+ if (props.public && props.entry.format === "service-worker") {
65
+ throw new Error(
66
+ "You cannot use the service worker format with a `public` directory."
67
+ );
68
+ }
69
+
70
+ if (props.bindings.wasm_modules && props.entry.format === "modules") {
71
+ throw new Error(
72
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
73
+ );
74
+ }
75
+
76
+ if (props.bindings.text_blobs && props.entry.format === "modules") {
77
+ throw new Error(
78
+ "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[build.upload.rules]` in your wrangler.toml"
79
+ );
80
+ }
81
+
82
+ const bundle = useEsbuild({
83
+ entry: props.entry,
84
+ destination: directory,
85
+ staticRoot: props.public,
86
+ jsxFactory: props.jsxFactory,
87
+ rules: props.rules,
88
+ jsxFragment: props.jsxFragment,
89
+ serveAssetsFromWorker: !!props.public,
90
+ });
91
+
92
+ const toggles = useHotkeys(
93
+ {
94
+ local: props.initialMode === "local",
95
+ tunnel: false,
96
+ },
97
+ port,
98
+ props.inspectorPort,
99
+ props.localProtocol
100
+ );
101
+
102
+ useTunnel(toggles.tunnel);
103
+
104
+ return (
105
+ <>
106
+ {toggles.local ? (
107
+ <Local
108
+ name={props.name}
109
+ bundle={bundle}
110
+ format={props.entry.format}
111
+ bindings={props.bindings}
112
+ assetPaths={props.assetPaths}
113
+ public={props.public}
114
+ port={port}
115
+ rules={props.rules}
116
+ inspectorPort={props.inspectorPort}
117
+ enableLocalPersistence={props.enableLocalPersistence}
118
+ />
119
+ ) : (
120
+ <Remote
121
+ name={props.name}
122
+ bundle={bundle}
123
+ format={props.entry.format}
124
+ accountId={props.accountId}
125
+ apiToken={apiToken}
126
+ bindings={props.bindings}
127
+ assetPaths={props.assetPaths}
128
+ public={props.public}
129
+ port={port}
130
+ localProtocol={props.localProtocol}
131
+ inspectorPort={props.inspectorPort}
132
+ compatibilityDate={props.compatibilityDate}
133
+ compatibilityFlags={props.compatibilityFlags}
134
+ usageModel={props.usageModel}
135
+ env={props.env}
136
+ legacyEnv={props.legacyEnv}
137
+ zone={props.zone}
138
+ />
139
+ )}
140
+ <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
141
+ <Text>
142
+ {`B to open a browser, D to open Devtools, L to ${
143
+ toggles.local ? "turn off" : "turn on"
144
+ } local mode, X to exit`}
145
+ </Text>
146
+ </Box>
147
+ </>
148
+ );
149
+ }
150
+
151
+ export interface DirectorySyncResult {
152
+ name: string;
153
+ removeCallback: () => void;
154
+ }
155
+
156
+ function useTmpDir(): string | undefined {
157
+ const [directory, setDirectory] = useState<DirectorySyncResult>();
158
+ const handleError = useErrorHandler();
159
+ useEffect(() => {
160
+ let dir: DirectorySyncResult | undefined;
161
+ try {
162
+ dir = tmp.dirSync({ unsafeCleanup: true });
163
+ setDirectory(dir);
164
+ return;
165
+ } catch (err) {
166
+ console.error("failed to create tmp dir");
167
+ handleError(err);
168
+ }
169
+ return () => {
170
+ dir?.removeCallback();
171
+ };
172
+ }, [handleError]);
173
+ return directory?.name;
174
+ }
175
+
176
+ function useCustomBuild(
177
+ expectedEntry: Entry,
178
+ build: {
179
+ command?: undefined | string;
180
+ cwd?: undefined | string;
181
+ watch_dir?: undefined | string;
182
+ }
183
+ ): void {
184
+ useEffect(() => {
185
+ if (!build.command) return;
186
+ let watcher: ReturnType<typeof watch> | undefined;
187
+ if (build.watch_dir) {
188
+ watcher = watch(build.watch_dir, {
189
+ persistent: true,
190
+ ignoreInitial: true,
191
+ }).on("all", (_event, filePath) => {
192
+ //TODO: we should buffer requests to the proxy until this completes
193
+ console.log(`The file ${filePath} changed, restarting build...`);
194
+ runCustomBuild(expectedEntry.file, build).catch((err) => {
195
+ console.error("Custom build failed:", err);
196
+ });
197
+ });
198
+ }
199
+
200
+ return () => {
201
+ watcher?.close();
202
+ };
203
+ }, [build, expectedEntry.file]);
204
+ }
205
+
206
+ function sleep(period: number) {
207
+ return new Promise((resolve) => setTimeout(resolve, period));
208
+ }
209
+ const SLEEP_DURATION = 2000;
210
+ // really need a first class api for this
211
+ const hostNameRegex = /userHostname="(.*)"/g;
212
+ async function findTunnelHostname() {
213
+ let hostName: string | undefined;
214
+ while (!hostName) {
215
+ try {
216
+ const resp = await fetch("http://localhost:8789/metrics");
217
+ const data = await resp.text();
218
+ const matches = Array.from(data.matchAll(hostNameRegex));
219
+ hostName = matches[0][1];
220
+ } catch (err) {
221
+ await sleep(SLEEP_DURATION);
222
+ }
223
+ }
224
+ return hostName;
225
+ }
226
+
227
+ /**
228
+ * Create a tunnel to the remote worker.
229
+ * We've disabled this for now until we figure out a better user experience.
230
+ */
231
+ function useTunnel(toggle: boolean) {
232
+ const tunnel = useRef<ReturnType<typeof spawn>>();
233
+ const removeSignalExitListener = useRef<() => void>();
234
+ // TODO: test if cloudflared is available, if not
235
+ // point them to a url where they can get docs to install it
236
+ useEffect(() => {
237
+ async function startTunnel() {
238
+ if (toggle) {
239
+ try {
240
+ await commandExists("cloudflared");
241
+ } catch (e) {
242
+ console.error(
243
+ "To share your worker on the Internet, please install `cloudflared` from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"
244
+ );
245
+ return;
246
+ }
247
+ console.log("⎔ Starting a tunnel...");
248
+ tunnel.current = spawn("cloudflared", [
249
+ "tunnel",
250
+ "--url",
251
+ "http://localhost:8787",
252
+ "--metrics",
253
+ "localhost:8789",
254
+ ]);
255
+
256
+ tunnel.current.on("close", (code) => {
257
+ if (code) {
258
+ console.log(`Tunnel process exited with code ${code}`);
259
+ }
260
+ });
261
+
262
+ removeSignalExitListener.current = onExit((_code, _signal) => {
263
+ console.log("⎔ Shutting down local tunnel.");
264
+ tunnel.current?.kill();
265
+ tunnel.current = undefined;
266
+ });
267
+
268
+ const hostName = await findTunnelHostname();
269
+ await clipboardy.write(hostName);
270
+ console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
271
+ }
272
+ }
273
+
274
+ startTunnel().catch(async (err) => {
275
+ console.error("tunnel:", err);
276
+ await reportError(err);
277
+ });
278
+
279
+ return () => {
280
+ if (tunnel.current) {
281
+ console.log("⎔ Shutting down tunnel.");
282
+ tunnel.current?.kill();
283
+ tunnel.current = undefined;
284
+ removeSignalExitListener.current && removeSignalExitListener.current();
285
+ removeSignalExitListener.current = undefined;
286
+ }
287
+ };
288
+ }, [toggle]);
289
+ }
290
+
291
+ type useHotkeysInitialState = {
292
+ local: boolean;
293
+ tunnel: boolean;
294
+ };
295
+ function useHotkeys(
296
+ initial: useHotkeysInitialState,
297
+ port: number,
298
+ inspectorPort: number,
299
+ localProtocol: "http" | "https"
300
+ ) {
301
+ // UGH, we should put port in context instead
302
+ const [toggles, setToggles] = useState(initial);
303
+ useInput(
304
+ async (
305
+ input,
306
+ // eslint-disable-next-line unused-imports/no-unused-vars
307
+ key
308
+ ) => {
309
+ switch (input.toLowerCase()) {
310
+ // open browser
311
+ case "b": {
312
+ await openInBrowser(`${localProtocol}://localhost:${port}`);
313
+ break;
314
+ }
315
+ // toggle inspector
316
+ case "d": {
317
+ await openInBrowser(
318
+ `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:${inspectorPort}/ws`,
319
+ { forceChromium: true }
320
+ );
321
+ break;
322
+ }
323
+ // toggle local
324
+ case "l":
325
+ setToggles((previousToggles) => ({
326
+ ...previousToggles,
327
+ local: !previousToggles.local,
328
+ }));
329
+ break;
330
+ // shut down
331
+ case "q":
332
+ case "x":
333
+ process.exit(0);
334
+ break;
335
+ default:
336
+ // nothing?
337
+ break;
338
+ }
339
+ }
340
+ );
341
+ return toggles;
342
+ }
343
+
344
+ function ErrorFallback(props: { error: Error }) {
345
+ const { exit } = useApp();
346
+ useEffect(() => exit(props.error));
347
+ return (
348
+ <>
349
+ <Text>Something went wrong:</Text>
350
+ <Text>{props.error.stack}</Text>
351
+ </>
352
+ );
353
+ }
354
+
355
+ export default withErrorBoundary(DevImplementation, {
356
+ FallbackComponent: ErrorFallback,
357
+ });