wrangler 0.0.0-ece06ea → 0.0.0-eebdb6c

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 (39) hide show
  1. package/package.json +22 -15
  2. package/pages/functions/buildWorker.ts +57 -0
  3. package/pages/functions/filepath-routing.test.ts +32 -0
  4. package/pages/functions/filepath-routing.ts +225 -0
  5. package/pages/functions/identifiers.ts +78 -0
  6. package/pages/functions/routes.ts +158 -0
  7. package/pages/functions/template-worker.ts +135 -0
  8. package/src/__tests__/index.test.ts +254 -120
  9. package/src/__tests__/jest.setup.ts +22 -0
  10. package/src/__tests__/kv.test.ts +1098 -0
  11. package/src/__tests__/mock-cfetch.ts +171 -0
  12. package/src/__tests__/mock-dialogs.ts +65 -0
  13. package/src/__tests__/run-in-tmp.ts +19 -0
  14. package/src/__tests__/run-wrangler.ts +32 -0
  15. package/src/api/form_data.ts +70 -97
  16. package/src/api/inspect.ts +0 -11
  17. package/src/api/preview.ts +29 -24
  18. package/src/api/worker.ts +34 -40
  19. package/src/cfetch/index.ts +103 -0
  20. package/src/cfetch/internal.ts +69 -0
  21. package/src/cli.ts +6 -7
  22. package/src/config.ts +14 -7
  23. package/src/dev.tsx +274 -131
  24. package/src/dialogs.tsx +4 -12
  25. package/src/index.tsx +304 -245
  26. package/src/kv.tsx +101 -45
  27. package/src/module-collection.ts +64 -0
  28. package/src/pages.tsx +681 -104
  29. package/src/proxy.ts +103 -0
  30. package/src/publish.ts +54 -58
  31. package/src/sites.tsx +5 -6
  32. package/src/tail.tsx +5 -3
  33. package/src/user.tsx +9 -16
  34. package/wrangler-dist/cli.js +36633 -49770
  35. package/wrangler-dist/cli.js.map +3 -3
  36. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  37. package/src/__tests__/mock-cfetch.js +0 -46
  38. package/src/cfetch.ts +0 -72
  39. package/src/util/fetch.ts +0 -74
package/src/dev.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import esbuild from "esbuild";
2
- import httpProxy from "http-proxy";
3
2
  import { readFile } from "fs/promises";
3
+ import { existsSync } from "fs";
4
4
  import type { DirectoryResult } from "tmp-promise";
5
5
  import tmp from "tmp-promise";
6
6
  import type { CfPreviewToken } from "./api/preview";
@@ -9,7 +9,7 @@ import React, { useState, useEffect, useRef } from "react";
9
9
  import path from "path";
10
10
  import open from "open";
11
11
  import { DtInspector } from "./api/inspect";
12
- import type { CfModule, CfVariable } from "./api/worker";
12
+ import type { CfModule } from "./api/worker";
13
13
  import { createWorker } from "./api/worker";
14
14
  import type { CfWorkerInit } from "./api/worker";
15
15
  import { spawn } from "child_process";
@@ -17,13 +17,15 @@ import onExit from "signal-exit";
17
17
  import { syncAssets } from "./sites";
18
18
  import clipboardy from "clipboardy";
19
19
  import http from "node:http";
20
- import serveStatic from "serve-static";
21
20
  import commandExists from "command-exists";
22
21
  import assert from "assert";
23
22
  import { getAPIToken } from "./user";
24
23
  import fetch from "node-fetch";
25
- import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill";
26
- import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill";
24
+ import makeModuleCollector from "./module-collection";
25
+ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
26
+ import { createHttpProxy } from "./proxy";
27
+ import { execa } from "execa";
28
+ import { watch } from "chokidar";
27
29
 
28
30
  type CfScriptFormat = void | "modules" | "service-worker";
29
31
 
@@ -36,16 +38,20 @@ type Props = {
36
38
  initialMode: "local" | "remote";
37
39
  jsxFactory: void | string;
38
40
  jsxFragment: void | string;
39
- variables: { [name: string]: CfVariable };
41
+ bindings: CfWorkerInit["bindings"];
40
42
  public: void | string;
41
43
  site: void | string;
42
44
  compatibilityDate: void | string;
43
45
  compatibilityFlags: void | string[];
44
46
  usageModel: void | "bundled" | "unbound";
45
- polyfillNode: void | boolean;
47
+ buildCommand: {
48
+ command?: undefined | string;
49
+ cwd?: undefined | string;
50
+ watch_dir?: undefined | string;
51
+ };
46
52
  };
47
53
 
48
- export function Dev(props: Props): JSX.Element {
54
+ function Dev(props: Props): JSX.Element {
49
55
  if (props.public && props.format === "service-worker") {
50
56
  throw new Error(
51
57
  "You cannot use the service worker format with a `public` directory."
@@ -55,13 +61,17 @@ export function Dev(props: Props): JSX.Element {
55
61
  const apiToken = getAPIToken();
56
62
  const directory = useTmpDir();
57
63
 
64
+ // if there isn't a build command, we just return the entry immediately
65
+ // ideally there would be a conditional here, but the rules of hooks
66
+ // kinda forbid that, so we thread the entry through useCustomBuild
67
+ const entry = useCustomBuild(props.entry, props.buildCommand);
68
+
58
69
  const bundle = useEsbuild({
59
- entry: props.entry,
70
+ entry,
60
71
  destination: directory,
61
72
  staticRoot: props.public,
62
73
  jsxFactory: props.jsxFactory,
63
74
  jsxFragment: props.jsxFragment,
64
- polyfillNode: props.polyfillNode,
65
75
  });
66
76
  if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
67
77
  throw new Error(
@@ -89,7 +99,7 @@ export function Dev(props: Props): JSX.Element {
89
99
  name={props.name}
90
100
  bundle={bundle}
91
101
  format={props.format}
92
- variables={props.variables}
102
+ bindings={props.bindings}
93
103
  site={props.site}
94
104
  public={props.public}
95
105
  port={props.port}
@@ -101,7 +111,7 @@ export function Dev(props: Props): JSX.Element {
101
111
  format={props.format}
102
112
  accountId={props.accountId}
103
113
  apiToken={apiToken}
104
- variables={props.variables}
114
+ bindings={props.bindings}
105
115
  site={props.site}
106
116
  public={props.public}
107
117
  port={props.port}
@@ -162,7 +172,7 @@ function Remote(props: {
162
172
  port: number;
163
173
  accountId: void | string;
164
174
  apiToken: void | string;
165
- variables: { [name: string]: CfVariable };
175
+ bindings: CfWorkerInit["bindings"];
166
176
  compatibilityDate: string | void;
167
177
  compatibilityFlags: void | string[];
168
178
  usageModel: void | "bundled" | "unbound";
@@ -173,10 +183,10 @@ function Remote(props: {
173
183
  name: props.name,
174
184
  bundle: props.bundle,
175
185
  format: props.format,
176
- modules: [],
186
+ modules: props.bundle ? props.bundle.modules : [],
177
187
  accountId: props.accountId,
178
188
  apiToken: props.apiToken,
179
- variables: props.variables,
189
+ bindings: props.bindings,
180
190
  sitesFolder: props.site,
181
191
  port: props.port,
182
192
  compatibilityDate: props.compatibilityDate,
@@ -193,7 +203,7 @@ function Local(props: {
193
203
  name: void | string;
194
204
  bundle: EsbuildBundle | void;
195
205
  format: CfScriptFormat;
196
- variables: { [name: string]: CfVariable };
206
+ bindings: CfWorkerInit["bindings"];
197
207
  public: void | string;
198
208
  site: void | string;
199
209
  port: number;
@@ -202,7 +212,7 @@ function Local(props: {
202
212
  name: props.name,
203
213
  bundle: props.bundle,
204
214
  format: props.format,
205
- variables: props.variables,
215
+ bindings: props.bindings,
206
216
  port: props.port,
207
217
  });
208
218
  useInspector(inspectorUrl);
@@ -213,11 +223,11 @@ function useLocalWorker(props: {
213
223
  name: void | string;
214
224
  bundle: EsbuildBundle | void;
215
225
  format: CfScriptFormat;
216
- variables: { [name: string]: CfVariable };
226
+ bindings: CfWorkerInit["bindings"];
217
227
  port: number;
218
228
  }) {
219
229
  // TODO: pass vars via command line
220
- const { bundle, format, variables, port } = props;
230
+ const { bundle, format, bindings, port } = props;
221
231
  const local = useRef<ReturnType<typeof spawn>>();
222
232
  const removeSignalExitListener = useRef<() => void>();
223
233
  const [inspectorUrl, setInspectorUrl] = useState<string | void>();
@@ -236,6 +246,7 @@ function useLocalWorker(props: {
236
246
  }
237
247
 
238
248
  console.log("⎔ Starting a local server...");
249
+ // TODO: just use execa for this
239
250
  local.current = spawn("node", [
240
251
  "--experimental-vm-modules",
241
252
  "--inspect",
@@ -253,20 +264,17 @@ function useLocalWorker(props: {
253
264
  "--kv-persist",
254
265
  "--cache-persist",
255
266
  "--do-persist",
256
- ...Object.entries(variables)
257
- .map(([varKey, varVal]) => {
258
- if (typeof varVal === "string") {
259
- return `--binding ${varKey}=${varVal}`;
260
- } else if (
261
- "namespaceId" in varVal &&
262
- typeof varVal.namespaceId === "string"
263
- ) {
264
- return `--kv ${varKey}`;
265
- } else if ("class_name" in varVal) {
266
- return `--do ${varKey}=${varVal.class_name}`;
267
- }
268
- })
269
- .filter(Boolean),
267
+ ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
268
+ return ["--binding", `${key}=${value}`];
269
+ }),
270
+ ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
271
+ return ["--kv", binding];
272
+ }),
273
+ ...(bindings.durable_objects?.bindings || []).flatMap(
274
+ ({ name, class_name }) => {
275
+ return ["--do", `${name}=${class_name}`];
276
+ }
277
+ ),
270
278
  "--modules",
271
279
  format ||
272
280
  (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
@@ -281,21 +289,32 @@ function useLocalWorker(props: {
281
289
  }
282
290
  });
283
291
 
284
- local.current.stdout.on("data", (_data: string) => {
285
- // console.log(`stdout: ${data}`);
292
+ local.current.stdout.on("data", (data: Buffer) => {
293
+ console.log(`${data.toString()}`);
286
294
  });
287
295
 
288
- local.current.stderr.on("data", (data: string) => {
289
- // console.error(`stderr: ${data}`);
296
+ local.current.stderr.on("data", (data: Buffer) => {
297
+ console.error(`${data.toString()}`);
290
298
  const matches =
291
299
  /Debugger listening on (ws:\/\/127\.0\.0\.1:9229\/[A-Za-z0-9-]+)/.exec(
292
- data
300
+ data.toString()
293
301
  );
294
302
  if (matches) {
295
303
  setInspectorUrl(matches[1]);
296
304
  }
297
305
  });
298
306
 
307
+ local.current.on("exit", (code) => {
308
+ if (code !== 0) {
309
+ console.error(`miniflare process exited with code ${code}`);
310
+ }
311
+ });
312
+
313
+ local.current.on("error", (error: Error) => {
314
+ console.error(`miniflare process failed to spawn`);
315
+ console.error(error);
316
+ });
317
+
299
318
  removeSignalExitListener.current = onExit((_code, _signal) => {
300
319
  console.log("⎔ Shutting down local server.");
301
320
  local.current?.kill();
@@ -303,7 +322,9 @@ function useLocalWorker(props: {
303
322
  });
304
323
  }
305
324
 
306
- startLocalWorker();
325
+ startLocalWorker().catch((err) => {
326
+ console.error("local worker:", err);
327
+ });
307
328
 
308
329
  return () => {
309
330
  if (local.current) {
@@ -314,12 +335,20 @@ function useLocalWorker(props: {
314
335
  removeSignalExitListener.current = undefined;
315
336
  }
316
337
  };
317
- }, [bundle, format, port]);
338
+ }, [
339
+ bundle,
340
+ format,
341
+ port,
342
+ bindings.durable_objects?.bindings,
343
+ bindings.kv_namespaces,
344
+ bindings.vars,
345
+ ]);
318
346
  return { inspectorUrl };
319
347
  }
320
348
 
321
349
  function useTmpDir(): string | void {
322
350
  const [directory, setDirectory] = useState<DirectoryResult>();
351
+ const handleError = useErrorHandler();
323
352
  useEffect(() => {
324
353
  let dir: DirectoryResult;
325
354
  async function create() {
@@ -329,52 +358,119 @@ function useTmpDir(): string | void {
329
358
  return;
330
359
  } catch (err) {
331
360
  console.error("failed to create tmp dir");
332
- console.error(err);
333
361
  throw err;
334
362
  }
335
363
  }
336
- create();
364
+ create().catch((err) => {
365
+ // we want to break here
366
+ // we can't do much without a temp dir anyway
367
+ handleError(err);
368
+ });
337
369
  return () => {
338
- dir.cleanup();
370
+ dir.cleanup().catch(() => {
371
+ // extremely unlikely,
372
+ // but it's 2021 after all
373
+ console.error("failed to cleanup tmp dir");
374
+ });
339
375
  };
340
- }, []);
376
+ }, [handleError]);
341
377
  return directory?.path;
342
378
  }
343
379
 
380
+ function useCustomBuild(
381
+ expectedEntry: string,
382
+ props: {
383
+ command?: undefined | string;
384
+ cwd?: undefined | string;
385
+ watch_dir?: undefined | string;
386
+ }
387
+ ): void | string {
388
+ const [entry, setEntry] = useState<string | void>(
389
+ // if there's no build command, just return the expected entry
390
+ props.command ? null : expectedEntry
391
+ );
392
+ const { command, cwd, watch_dir } = props;
393
+ useEffect(() => {
394
+ if (!command) return;
395
+ let cmd, interval;
396
+ console.log("running:", command);
397
+ const commandPieces = command.split(" ");
398
+ cmd = execa(commandPieces[0], commandPieces.slice(1), {
399
+ ...(cwd && { cwd }),
400
+ stderr: "inherit",
401
+ stdout: "inherit",
402
+ });
403
+ if (watch_dir) {
404
+ watch(watch_dir, { persistent: true, ignoreInitial: true }).on(
405
+ "all",
406
+ (_event, _path) => {
407
+ console.log(`The file ${path} changed, restarting build...`);
408
+ cmd.kill();
409
+ cmd = execa(commandPieces[0], commandPieces.slice(1), {
410
+ ...(cwd && { cwd }),
411
+ stderr: "inherit",
412
+ stdout: "inherit",
413
+ });
414
+ }
415
+ );
416
+ }
417
+
418
+ // check every so often whether `expectedEntry` exists
419
+ // if it does, we're done
420
+ const startedAt = Date.now();
421
+ interval = setInterval(() => {
422
+ if (existsSync(expectedEntry)) {
423
+ clearInterval(interval);
424
+ setEntry(expectedEntry);
425
+ } else {
426
+ const elapsed = Date.now() - startedAt;
427
+ // timeout after 30 seconds of waiting
428
+ if (elapsed > 1000 * 60 * 30) {
429
+ console.error("⎔ Build timed out.");
430
+ clearInterval(interval);
431
+ cmd.kill();
432
+ }
433
+ }
434
+ }, 200);
435
+ // TODO: we could probably timeout here after a while
436
+
437
+ return () => {
438
+ if (cmd) {
439
+ cmd.kill();
440
+ cmd = undefined;
441
+ }
442
+ clearInterval(interval);
443
+ interval = undefined;
444
+ };
445
+ }, [command, cwd, expectedEntry, watch_dir]);
446
+ return entry;
447
+ }
448
+
344
449
  type EsbuildBundle = {
345
450
  id: number;
346
451
  path: string;
347
452
  entry: string;
348
453
  type: "esm" | "commonjs";
349
454
  exports: string[];
455
+ modules: CfModule[];
350
456
  };
351
457
 
352
458
  function useEsbuild(props: {
353
- entry: string;
459
+ entry: void | string;
354
460
  destination: string | void;
355
461
  staticRoot: void | string;
356
462
  jsxFactory: string | void;
357
463
  jsxFragment: string | void;
358
- polyfillNode: boolean | void;
359
464
  }): EsbuildBundle | void {
360
- const {
361
- entry,
362
- destination,
363
- staticRoot,
364
- jsxFactory,
365
- jsxFragment,
366
- polyfillNode,
367
- } = props;
465
+ const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
368
466
  const [bundle, setBundle] = useState<EsbuildBundle>();
369
467
  useEffect(() => {
370
468
  let result: esbuild.BuildResult;
371
469
  async function build() {
372
- if (!destination) return;
470
+ if (!destination || !entry) return;
471
+ const moduleCollector = makeModuleCollector();
373
472
  result = await esbuild.build({
374
473
  entryPoints: [entry],
375
- define: {
376
- ...(polyfillNode && { global: "globalThis" }),
377
- },
378
474
  bundle: true,
379
475
  outdir: destination,
380
476
  metafile: true,
@@ -383,12 +479,11 @@ function useEsbuild(props: {
383
479
  loader: {
384
480
  ".js": "jsx",
385
481
  },
386
- plugins: polyfillNode
387
- ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
388
- : undefined,
389
482
  ...(jsxFactory && { jsxFactory }),
390
483
  ...(jsxFragment && { jsxFragment }),
391
484
  external: ["__STATIC_CONTENT_MANIFEST"],
485
+ conditions: ["worker", "browser"],
486
+ plugins: [moduleCollector.plugin],
392
487
  // TODO: import.meta.url
393
488
  watch: {
394
489
  async onRebuild(error) {
@@ -396,7 +491,10 @@ function useEsbuild(props: {
396
491
  else {
397
492
  // nothing really changes here, so let's increment the id
398
493
  // to change the return object's identity
399
- setBundle((bundle) => ({ ...bundle, id: bundle.id + 1 }));
494
+ setBundle((previousBundle) => ({
495
+ ...previousBundle,
496
+ id: previousBundle.id + 1,
497
+ }));
400
498
  }
401
499
  },
402
500
  },
@@ -412,13 +510,19 @@ function useEsbuild(props: {
412
510
  path: chunks[0],
413
511
  type: chunks[1].exports.length > 0 ? "esm" : "commonjs",
414
512
  exports: chunks[1].exports,
513
+ modules: moduleCollector.modules,
415
514
  });
416
515
  }
417
- build();
516
+ build().catch((_err) => {
517
+ // esbuild already logs errors to stderr
518
+ // and we don't want to end the process
519
+ // on build errors anyway
520
+ // so this is a no-op error handler
521
+ });
418
522
  return () => {
419
523
  result?.stop();
420
524
  };
421
- }, [entry, destination, staticRoot, jsxFactory, jsxFragment, polyfillNode]);
525
+ }, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
422
526
  return bundle;
423
527
  }
424
528
 
@@ -429,7 +533,7 @@ function useWorker(props: {
429
533
  modules: CfModule[];
430
534
  accountId: string;
431
535
  apiToken: string;
432
- variables: { [name: string]: CfVariable };
536
+ bindings: CfWorkerInit["bindings"];
433
537
  sitesFolder: void | string;
434
538
  port: number;
435
539
  compatibilityDate: string | void;
@@ -443,7 +547,7 @@ function useWorker(props: {
443
547
  modules,
444
548
  accountId,
445
549
  apiToken,
446
- variables,
550
+ bindings,
447
551
  sitesFolder,
448
552
  compatibilityDate,
449
553
  compatibilityFlags,
@@ -451,6 +555,12 @@ function useWorker(props: {
451
555
  port,
452
556
  } = props;
453
557
  const [token, setToken] = useState<CfPreviewToken>();
558
+
559
+ // This is the most reliable way to detect whether
560
+ // something's "happened" in our system; We make a ref and
561
+ // mark it once we log our initial message. Refs are vars!
562
+ const startedRef = useRef(false);
563
+
454
564
  useEffect(() => {
455
565
  async function start() {
456
566
  if (!bundle) return;
@@ -465,10 +575,11 @@ function useWorker(props: {
465
575
  return;
466
576
  }
467
577
 
468
- if (token) {
469
- console.log("⎔ Detected changes, restarting server...");
470
- } else {
578
+ if (!startedRef.current) {
471
579
  console.log("⎔ Starting server...");
580
+ startedRef.current = true;
581
+ } else {
582
+ console.log("⎔ Detected changes, restarting server...");
472
583
  }
473
584
 
474
585
  const assets = sitesFolder
@@ -486,25 +597,29 @@ function useWorker(props: {
486
597
 
487
598
  const content = await readFile(bundle.path, "utf-8");
488
599
  const init: CfWorkerInit = {
489
- name: name || path.basename(bundle.path),
600
+ name,
490
601
  main: {
491
602
  name: path.basename(bundle.path),
492
603
  type: format || bundle.type === "esm" ? "esm" : "commonjs",
493
604
  content,
494
605
  },
495
- modules: assets.manifest
496
- ? modules.concat({
497
- name: "__STATIC_CONTENT_MANIFEST",
498
- content: JSON.stringify(assets.manifest),
499
- type: "text",
500
- })
501
- : modules,
502
- variables: assets.namespace
503
- ? {
504
- ...variables,
505
- __STATIC_CONTENT: { namespaceId: assets.namespace },
506
- }
507
- : variables,
606
+ modules: modules.concat(
607
+ assets.manifest
608
+ ? {
609
+ name: "__STATIC_CONTENT_MANIFEST",
610
+ content: JSON.stringify(assets.manifest),
611
+ type: "text",
612
+ }
613
+ : []
614
+ ),
615
+ bindings: {
616
+ ...bindings,
617
+ kv_namespaces: (bindings.kv_namespaces || []).concat(
618
+ assets.namespace
619
+ ? { binding: "__STATIC_CONTENT", id: assets.namespace }
620
+ : []
621
+ ),
622
+ },
508
623
  migrations: undefined, // no migrations in dev
509
624
  compatibility_date: compatibilityDate,
510
625
  compatibility_flags: compatibilityFlags,
@@ -516,9 +631,12 @@ function useWorker(props: {
516
631
  apiToken,
517
632
  })
518
633
  );
519
- console.log(`⬣ Listening at http://localhost:${port}`);
520
634
  }
521
- start();
635
+ start().catch((err) => {
636
+ // we want to log the error, but not end the process
637
+ // since it could recover after the developer fixes whatever's wrong
638
+ console.error("remote worker:", err);
639
+ });
522
640
  }, [
523
641
  name,
524
642
  bundle,
@@ -530,6 +648,8 @@ function useWorker(props: {
530
648
  compatibilityDate,
531
649
  compatibilityFlags,
532
650
  usageModel,
651
+ bindings,
652
+ modules,
533
653
  ]);
534
654
  return token;
535
655
  }
@@ -545,43 +665,53 @@ function useProxy({
545
665
  }) {
546
666
  useEffect(() => {
547
667
  if (!token) return;
548
- const proxy = httpProxy.createProxyServer({
549
- secure: false,
550
- changeOrigin: true,
551
- headers: {
552
- "cf-workers-preview-token": token.value,
668
+ // TODO(soon): since headers are added in callbacks, the server
669
+ // does not need to restart when changes are made.
670
+ const host = token.host;
671
+ const proxy = createHttpProxy({
672
+ host,
673
+ assetPath: typeof publicRoot === "string" ? publicRoot : null,
674
+ onRequest: (headers) => {
675
+ headers["cf-workers-preview-token"] = token.value;
676
+ },
677
+ onResponse: (headers) => {
678
+ for (const [name, value] of Object.entries(headers)) {
679
+ // Rewrite the remote host to the local host.
680
+ if (typeof value === "string" && value.includes(host)) {
681
+ headers[name] = value
682
+ .replaceAll(`https://${host}`, `http://localhost:${port}`)
683
+ .replaceAll(host, `localhost:${port}`);
684
+ }
685
+ }
553
686
  },
554
- target: `https://${token.host}`,
555
- // TODO: log websockets too? validate durables, etc
556
687
  });
557
688
 
558
- const servePublic =
559
- publicRoot &&
560
- serveStatic(publicRoot, {
561
- cacheControl: false,
562
- });
563
- const server = http
564
- .createServer((req, res) => {
565
- if (publicRoot) {
566
- servePublic(req, res, () => {
567
- proxy.web(req, res);
568
- });
569
- } else {
570
- proxy.web(req, res);
571
- }
572
- })
573
- .listen(port); // TODO: custom port
689
+ console.log(`⬣ Listening at http://localhost:${port}`);
690
+
691
+ const server = proxy.listen(port);
574
692
 
575
- proxy.on("proxyRes", function (proxyRes, req, res) {
693
+ // TODO(soon): refactor logging format into its own function
694
+ proxy.on("request", function (req, res) {
576
695
  // log all requests
577
696
  console.log(
578
697
  new Date().toLocaleTimeString(),
579
698
  req.method,
580
699
  req.url,
581
- res.statusCode // TODO add a status message like Ok etc?
700
+ res.statusCode
701
+ );
702
+ });
703
+ proxy.on("upgrade", (req) => {
704
+ console.log(
705
+ new Date().toLocaleTimeString(),
706
+ req.method,
707
+ req.url,
708
+ 101,
709
+ "(WebSocket)"
582
710
  );
583
711
  });
584
- // TODO: log errors?
712
+ proxy.on("error", (err) => {
713
+ console.error(new Date().toLocaleTimeString(), err);
714
+ });
585
715
 
586
716
  return () => {
587
717
  proxy.close();
@@ -662,12 +792,14 @@ function useTunnel(toggle: boolean) {
662
792
  });
663
793
 
664
794
  const hostName = await findTunnelHostname();
665
- clipboardy.write(hostName);
795
+ await clipboardy.write(hostName);
666
796
  console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
667
797
  }
668
798
  }
669
799
 
670
- startTunnel();
800
+ startTunnel().catch((err) => {
801
+ console.error("tunnel:", err);
802
+ });
671
803
 
672
804
  return () => {
673
805
  if (tunnel.current) {
@@ -689,38 +821,31 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
689
821
  // UGH, we should put port in context instead
690
822
  const [toggles, setToggles] = useState(initial);
691
823
  useInput(
692
- (
824
+ async (
693
825
  input,
694
826
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
695
827
  key
696
828
  ) => {
697
829
  switch (input) {
698
830
  case "b": // open browser
699
- open(
700
- `http://localhost:${port}/`
701
- // {
702
- // app: {
703
- // name: open.apps.chrome, // TODO: fallback on other browsers
704
- // },
705
- // }
706
- );
831
+ await open(`http://localhost:${port}/`);
707
832
  break;
708
833
  case "d": // toggle inspector
709
- open(
834
+ await open(
710
835
  `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:9229/ws`
711
- // {
712
- // app: {
713
- // name: open.apps.chrome,
714
- // // todo - add firefox and edge fallbacks
715
- // },
716
- // }
717
836
  );
718
837
  break;
719
838
  case "s": // toggle tunnel
720
- setToggles((toggles) => ({ ...toggles, tunnel: !toggles.tunnel }));
839
+ setToggles((previousToggles) => ({
840
+ ...previousToggles,
841
+ tunnel: !previousToggles.tunnel,
842
+ }));
721
843
  break;
722
844
  case "l": // toggle local
723
- setToggles((toggles) => ({ ...toggles, local: !toggles.local }));
845
+ setToggles((previousToggles) => ({
846
+ ...previousToggles,
847
+ local: !previousToggles.local,
848
+ }));
724
849
  break;
725
850
  case "q": // shut down
726
851
  case "x": // shut down
@@ -734,3 +859,21 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
734
859
  );
735
860
  return toggles;
736
861
  }
862
+
863
+ function ErrorFallback(props: {
864
+ error: Error;
865
+ resetErrorBoundary: () => void;
866
+ }) {
867
+ useEffect(() => {
868
+ console.error(props.error);
869
+ process.exit(1);
870
+ });
871
+ return (
872
+ <Box>
873
+ <Text>Something went wrong:</Text>
874
+ <Text>{props.error.message}</Text>
875
+ </Box>
876
+ );
877
+ }
878
+
879
+ export default withErrorBoundary(Dev, { FallbackComponent: ErrorFallback });